mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 23:53:17 -05:00
feat(oidc-provider): support prompt=create
This commit is contained in:
@@ -349,6 +349,10 @@ export const auth = betterAuth({
|
||||
|
||||
You don't need to handle anything from your side; when a new session is created, the plugin will handle continuing the authorization flow.
|
||||
|
||||
### Initiating Registration (`prompt=create`)
|
||||
|
||||
Better Auth supports the OpenID Connect Prompt Create extension. When an authorization request includes `prompt=create` and the user is not authenticated, Better Auth will redirect the user to `createAccountPage` (if provided) instead of `loginPage`. If `createAccountPage` is not set, the user will be redirected to `loginPage` and you can render a sign-up experience based on the `prompt` query parameter.
|
||||
|
||||
## Configuration
|
||||
|
||||
### OIDC Metadata
|
||||
@@ -620,6 +624,8 @@ Table Name: `oauthConsent`
|
||||
|
||||
**loginPage**: `string` - Path to the custom login page.
|
||||
|
||||
**createAccountPage**: `string` - Path to the custom create account (sign-up) page. When an authorization request includes `prompt=create`, unauthenticated users will be redirected here instead of `loginPage`.
|
||||
|
||||
**consentPage**: `string` - Path to the custom consent page.
|
||||
|
||||
**trustedClients**: `(Client & { skipConsent?: boolean })[]` - Array of trusted clients that are configured directly in the provider options. These clients bypass database lookups and can optionally skip consent screens.
|
||||
|
||||
@@ -12,6 +12,11 @@ function formatErrorURL(url: string, error: string, description: string) {
|
||||
}error=${error}&error_description=${description}`;
|
||||
}
|
||||
|
||||
function appendQuery(url: string, query: string) {
|
||||
if (!query) return url;
|
||||
return `${url}${url.includes("?") ? "&" : "?"}${query}`;
|
||||
}
|
||||
|
||||
function getErrorURL(
|
||||
ctx: GenericEndpointContext,
|
||||
error: string,
|
||||
@@ -86,8 +91,12 @@ export async function authorize(
|
||||
sameSite: "lax",
|
||||
},
|
||||
);
|
||||
const queryFromURL = ctx.request.url?.split("?")[1]!;
|
||||
return handleRedirect(`${options.loginPage}?${queryFromURL}`);
|
||||
const queryFromURL = ctx.request.url?.split("?")[1] || "";
|
||||
const targetPage =
|
||||
promptSet.has("create") && options.createAccountPage
|
||||
? options.createAccountPage
|
||||
: options.loginPage;
|
||||
return handleRedirect(appendQuery(targetPage, queryFromURL));
|
||||
}
|
||||
|
||||
const query = ctx.query as AuthorizationQuery;
|
||||
|
||||
@@ -102,6 +102,13 @@ export const getMetadata = (
|
||||
jwks_uri: `${baseURL}/jwks`,
|
||||
registration_endpoint: `${baseURL}/oauth2/register`,
|
||||
end_session_endpoint: `${baseURL}/oauth2/endsession`,
|
||||
prompt_values_supported: [
|
||||
"none",
|
||||
"login",
|
||||
"consent",
|
||||
"select_account",
|
||||
"create",
|
||||
],
|
||||
scopes_supported: ["openid", "profile", "email", "offline_access"],
|
||||
response_types_supported: ["code"],
|
||||
response_modes_supported: ["query"],
|
||||
|
||||
@@ -105,6 +105,7 @@ describe("oidc", async () => {
|
||||
plugins: [
|
||||
oidcProvider({
|
||||
loginPage: "/login",
|
||||
createAccountPage: "/register",
|
||||
consentPage: "/oauth2/authorize",
|
||||
requirePKCE: true,
|
||||
getAdditionalUserInfoClaim(user) {
|
||||
@@ -499,6 +500,44 @@ describe("oidc", async () => {
|
||||
expect(redirectURI).toContain("none");
|
||||
});
|
||||
|
||||
it("should redirect to createAccountPage when prompt=create and user not authenticated", async ({
|
||||
expect,
|
||||
}) => {
|
||||
const unauthClient = createAuthClient({
|
||||
plugins: [oidcClient()],
|
||||
baseURL: "http://localhost:3000",
|
||||
fetchOptions: {
|
||||
customFetchImpl,
|
||||
},
|
||||
});
|
||||
|
||||
const authUrl = new URL(
|
||||
"http://localhost:3000/api/auth/oauth2/authorize",
|
||||
);
|
||||
authUrl.searchParams.set("client_id", application.clientId);
|
||||
authUrl.searchParams.set(
|
||||
"redirect_uri",
|
||||
application.redirectUrls[0] || "",
|
||||
);
|
||||
authUrl.searchParams.set("response_type", "code");
|
||||
authUrl.searchParams.set("scope", "openid profile email");
|
||||
authUrl.searchParams.set("state", "test-state");
|
||||
authUrl.searchParams.set("prompt", "create");
|
||||
authUrl.searchParams.set("code_challenge", "test-challenge");
|
||||
authUrl.searchParams.set("code_challenge_method", "S256");
|
||||
|
||||
let redirectURI = "";
|
||||
await unauthClient.$fetch(authUrl.toString(), {
|
||||
method: "GET",
|
||||
onError(context) {
|
||||
redirectURI = context.response.headers.get("Location") || "";
|
||||
},
|
||||
});
|
||||
|
||||
expect(redirectURI).toContain("/register");
|
||||
expect(redirectURI).toContain("prompt=create");
|
||||
});
|
||||
|
||||
it("should return consent_required error when prompt=none and consent needed", async ({
|
||||
expect,
|
||||
}) => {
|
||||
@@ -912,6 +951,27 @@ describe("oidc", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should return prompt_values_supported in metadata", async ({
|
||||
expect,
|
||||
}) => {
|
||||
const response: any = await serverClient.$fetch(
|
||||
"/.well-known/openid-configuration",
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
const metadata = response.data || response;
|
||||
expect(metadata.prompt_values_supported).toEqual(
|
||||
expect.arrayContaining([
|
||||
"none",
|
||||
"login",
|
||||
"consent",
|
||||
"select_account",
|
||||
"create",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should logout successfully without parameters", async ({ expect }) => {
|
||||
const response = await serverClient.$fetch("/oauth2/endsession", {
|
||||
method: "GET",
|
||||
|
||||
@@ -88,6 +88,13 @@ export interface OIDCOptions {
|
||||
* prompt.
|
||||
*/
|
||||
loginPage: string;
|
||||
/**
|
||||
* The URL to the account creation page. This is used if the client requests
|
||||
* the `create` prompt.
|
||||
*
|
||||
* If not provided, the `loginPage` will be used.
|
||||
*/
|
||||
createAccountPage?: string | undefined;
|
||||
/**
|
||||
* Whether to require PKCE (proof key code exchange) or not
|
||||
*
|
||||
@@ -206,7 +213,7 @@ export interface AuthorizationQuery {
|
||||
*/
|
||||
prompt?:
|
||||
| (string & {})
|
||||
| ("none" | "consent" | "login" | "select_account")
|
||||
| ("none" | "consent" | "login" | "select_account" | "create")
|
||||
| undefined;
|
||||
/**
|
||||
* The display parameter is used to specify how the authorization server displays the
|
||||
@@ -452,6 +459,12 @@ export interface OIDCMetadata {
|
||||
* @default `/oauth2/register`
|
||||
*/
|
||||
registration_endpoint: string;
|
||||
/**
|
||||
* Supported prompt values.
|
||||
*
|
||||
* When defined, it MUST include all prompt values supported by this OP.
|
||||
*/
|
||||
prompt_values_supported?: string[] | undefined;
|
||||
/**
|
||||
* Supported scopes.
|
||||
*/
|
||||
|
||||
@@ -60,10 +60,11 @@ describe("parsePrompt", () => {
|
||||
});
|
||||
|
||||
it("should handle all valid prompt types", () => {
|
||||
const result = parsePrompt("login consent select_account");
|
||||
const result = parsePrompt("login consent select_account create");
|
||||
expect(result.has("login")).toBe(true);
|
||||
expect(result.has("consent")).toBe(true);
|
||||
expect(result.has("select_account")).toBe(true);
|
||||
expect(result.size).toBe(3);
|
||||
expect(result.has("create")).toBe(true);
|
||||
expect(result.size).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { InvalidRequest } from "../error";
|
||||
|
||||
export type AuthorizePrompt = "login" | "consent" | "select_account" | "none";
|
||||
export type AuthorizePrompt =
|
||||
| "login"
|
||||
| "consent"
|
||||
| "select_account"
|
||||
| "create"
|
||||
| "none";
|
||||
export type AuthorizePromptSet = ReadonlySet<AuthorizePrompt>;
|
||||
|
||||
/**
|
||||
@@ -16,6 +21,7 @@ export function parsePrompt(prompt: string) {
|
||||
p === "login" ||
|
||||
p === "consent" ||
|
||||
p === "select_account" ||
|
||||
p === "create" ||
|
||||
p === "none"
|
||||
) {
|
||||
set.add(p);
|
||||
|
||||
Reference in New Issue
Block a user