mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-10 04:19:32 +00:00
feat(oidc-provider): allow passing oauth consent code via query params (#3845)
* feat: pass oauth consent code via query params * address cubic comments * fix tests * address comments
This commit is contained in:
@@ -264,17 +264,40 @@ export const auth = betterAuth({
|
||||
})
|
||||
```
|
||||
|
||||
The plugin will redirect the user to the specified path with a `client_id` and `scope` query parameter. You can use this information to display a custom consent screen. Once the user consents, you can call `oauth2.consent` to complete the authorization.
|
||||
The plugin will redirect the user to the specified path with `consent_code`, `client_id` and `scope` query parameters. You can use this information to display a custom consent screen. Once the user consents, you can call `oauth2.consent` to complete the authorization.
|
||||
|
||||
<Endpoint path="/oauth2/consent" method="POST" />
|
||||
|
||||
```ts title="server.ts"
|
||||
The consent endpoint supports two methods for passing the consent code:
|
||||
|
||||
**Method 1: URL Parameter**
|
||||
```ts title="consent-page.ts"
|
||||
// Get the consent code from the URL
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
// Submit consent with the code in the request body
|
||||
const consentCode = params.get('consent_code');
|
||||
if (!consentCode) {
|
||||
throw new Error('Consent code not found in URL parameters');
|
||||
}
|
||||
|
||||
const res = await client.oauth2.consent({
|
||||
accept: true, // or false to deny
|
||||
consent_code: consentCode,
|
||||
});
|
||||
```
|
||||
|
||||
The `client_id` and other necessary information are stored in the browser cookie, so you don't need to pass them in the request. If they don't exist in the cookie, the consent method will return an error.
|
||||
**Method 2: Cookie-Based**
|
||||
```ts title="consent-page.ts"
|
||||
// The consent code is automatically stored in a signed cookie
|
||||
// Just submit the consent decision
|
||||
const res = await client.oauth2.consent({
|
||||
accept: true, // or false to deny
|
||||
// consent_code not needed when using cookie-based flow
|
||||
});
|
||||
```
|
||||
|
||||
Both methods are fully supported. The URL parameter method works well with mobile apps and third-party contexts, while the cookie-based method provides a simpler implementation for web applications.
|
||||
|
||||
### Handling Login
|
||||
|
||||
|
||||
@@ -263,14 +263,19 @@ export async function authorize(
|
||||
}
|
||||
|
||||
if (options?.consentPage) {
|
||||
// Set cookie to support cookie-based consent flows
|
||||
await ctx.setSignedCookie("oidc_consent_prompt", code, ctx.context.secret, {
|
||||
maxAge: 600,
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
});
|
||||
const consentURI = `${options.consentPage}?client_id=${
|
||||
client.clientId
|
||||
}&scope=${requestScope.join(" ")}`;
|
||||
|
||||
// Pass the consent code as a URL parameter to support URL-based consent flows
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.set("consent_code", code);
|
||||
urlParams.set("client_id", client.clientId);
|
||||
urlParams.set("scope", requestScope.join(" "));
|
||||
const consentURI = `${options.consentPage}?${urlParams.toString()}`;
|
||||
|
||||
return handleRedirect(consentURI);
|
||||
}
|
||||
|
||||
@@ -319,11 +319,36 @@ export const oidcProvider = (options: OIDCOptions) => {
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
accept: z.boolean(),
|
||||
consent_code: z.string().optional(),
|
||||
}),
|
||||
use: [sessionMiddleware],
|
||||
metadata: {
|
||||
openapi: {
|
||||
description: "Handle OAuth2 consent",
|
||||
description:
|
||||
"Handle OAuth2 consent. Supports both URL parameter-based flows (consent_code in body) and cookie-based flows (signed cookie).",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
accept: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Whether the user accepts or denies the consent request",
|
||||
},
|
||||
consent_code: {
|
||||
type: "string",
|
||||
description:
|
||||
"The consent code from the authorization request. Optional if using cookie-based flow.",
|
||||
},
|
||||
},
|
||||
required: ["accept"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Consent processed successfully",
|
||||
@@ -349,18 +374,31 @@ export const oidcProvider = (options: OIDCOptions) => {
|
||||
},
|
||||
},
|
||||
async (ctx) => {
|
||||
const storedCode = await ctx.getSignedCookie(
|
||||
// Support both consent flow methods:
|
||||
// 1. URL parameter-based: consent_code in request body (standard OAuth2 pattern)
|
||||
// 2. Cookie-based: using signed cookie for stateful consent flows
|
||||
let consentCode: string | null = ctx.body.consent_code || null;
|
||||
|
||||
if (!consentCode) {
|
||||
// Check for cookie-based consent flow
|
||||
consentCode = await ctx.getSignedCookie(
|
||||
"oidc_consent_prompt",
|
||||
ctx.context.secret,
|
||||
);
|
||||
if (!storedCode) {
|
||||
}
|
||||
|
||||
if (!consentCode) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
error_description: "No consent prompt found",
|
||||
error_description:
|
||||
"consent_code is required (either in body or cookie)",
|
||||
error: "invalid_request",
|
||||
});
|
||||
}
|
||||
|
||||
const verification =
|
||||
await ctx.context.internalAdapter.findVerificationValue(storedCode);
|
||||
await ctx.context.internalAdapter.findVerificationValue(
|
||||
consentCode,
|
||||
);
|
||||
if (!verification) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
error_description: "Invalid code",
|
||||
@@ -373,6 +411,12 @@ export const oidcProvider = (options: OIDCOptions) => {
|
||||
error: "invalid_request",
|
||||
});
|
||||
}
|
||||
|
||||
// Clear the cookie
|
||||
ctx.setCookie("oidc_consent_prompt", "", {
|
||||
maxAge: 0,
|
||||
});
|
||||
|
||||
const value = JSON.parse(verification.value) as CodeVerificationValue;
|
||||
if (!value.requireConsent) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
|
||||
@@ -233,7 +233,9 @@ describe("oidc", async () => {
|
||||
newHeaders.append("Cookie", headers.get("Cookie") || "");
|
||||
},
|
||||
});
|
||||
expect(redirectURI).toContain("/oauth2/authorize?client_id=");
|
||||
expect(redirectURI).toContain("/oauth2/authorize?");
|
||||
expect(redirectURI).toContain("consent_code=");
|
||||
expect(redirectURI).toContain("client_id=");
|
||||
const res = await serverClient.oauth2.consent(
|
||||
{
|
||||
accept: true,
|
||||
|
||||
@@ -52,14 +52,14 @@ export interface OIDCOptions {
|
||||
*
|
||||
* When the server redirects the user to the consent page, it will include the
|
||||
* following query parameters:
|
||||
* authorization code.
|
||||
* - `consent_code` - The consent code to identify the authorization request.
|
||||
* - `client_id` - The ID of the client.
|
||||
* - `scope` - The requested scopes.
|
||||
* - `code` - The authorization code.
|
||||
*
|
||||
* once the user consents, you need to call the `/oauth2/consent` endpoint
|
||||
* with the code and `accept: true` to complete the authorization. Which will
|
||||
* then return the client to the `redirect_uri` with the authorization code.
|
||||
* Once the user consents, you need to call the `/oauth2/consent` endpoint
|
||||
* with `accept: true` and optionally the `consent_code` (if using URL parameter flow)
|
||||
* to complete the authorization. This will return the client to the `redirect_uri`
|
||||
* with the authorization code.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
|
||||
Reference in New Issue
Block a user