mirror of
https://github.com/better-auth/better-auth.git
synced 2026-08-18 04:18:21 -05:00
fix(oauth-provider): scope essential ACR checks to OIDC
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
ID tokens now use `acr: "0"`, indicating that authentication did not meet
|
||||
ISO/IEC 29115 level 1, and OpenID discovery advertises only `"0"`. Because
|
||||
`acr_values` is voluntary, requests for other classes continue instead of
|
||||
failing. Essential `claims.id_token.acr` requests still fail when their
|
||||
required `value` or `values` cannot be met.
|
||||
failing. Essential `claims.id_token.acr` requests in OpenID Connect flows still
|
||||
fail when their required `value` or `values` cannot be met.
|
||||
|
||||
`customIdTokenClaims`, extension ID-token claims, and per-issuance `idTokenClaims` can no longer set OIDC/JWT protocol claims such as issuer, subject, audience, token lifetime, nonce, session or hash binding, `auth_time`, `acr`, `amr`, or `azp`. Namespaced custom claims still appear in ID tokens.
|
||||
|
||||
@@ -1721,7 +1721,7 @@ The metadata endpoint can be customized so that the publicized scopes and claims
|
||||
|
||||
All scopes inside the advertisedMetadata section MUST be listed in `scopes` otherwise initialization will fail.
|
||||
|
||||
Better Auth advertises `acr_values_supported: ["0"]`. In OIDC Core, `"0"` means the authentication did not meet ISO/IEC 29115 level 1. Custom ACR policies are not currently supported. Because `acr_values` is voluntary, requests for other classes continue and the ID token reports `acr: "0"`. An essential `claims.id_token.acr` request fails when its `value` or `values` does not include `"0"`.
|
||||
Better Auth advertises `acr_values_supported: ["0"]`. In OIDC Core, `"0"` means the authentication did not meet ISO/IEC 29115 level 1. Custom ACR policies are not currently supported. Because `acr_values` is voluntary, requests for other classes continue and the ID token reports `acr: "0"`. In an OpenID Connect request, an essential `claims.id_token.acr` request fails when its `value` or `values` does not include `"0"`.
|
||||
|
||||
#### Scopes
|
||||
|
||||
|
||||
@@ -538,13 +538,17 @@ describe("oauth authorize - acr_values (OIDC Core 1.0 §3.1.2.1)", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
function authorizeUrl(acrValues: string, acrClaim?: AcrClaimRequest) {
|
||||
function authorizeUrl(
|
||||
acrValues: string,
|
||||
acrClaim?: AcrClaimRequest,
|
||||
scope = "openid",
|
||||
) {
|
||||
if (!oauthClient?.client_id) throw new Error("beforeAll not run properly");
|
||||
const url = new URL(`${authServerBaseUrl}/api/auth/oauth2/authorize`);
|
||||
url.searchParams.set("client_id", oauthClient.client_id);
|
||||
url.searchParams.set("redirect_uri", redirectUri);
|
||||
url.searchParams.set("response_type", "code");
|
||||
url.searchParams.set("scope", "openid");
|
||||
url.searchParams.set("scope", scope);
|
||||
url.searchParams.set("state", "acr-state");
|
||||
url.searchParams.set("code_challenge", generateRandomString(43));
|
||||
url.searchParams.set("code_challenge_method", "S256");
|
||||
@@ -558,9 +562,13 @@ describe("oauth authorize - acr_values (OIDC Core 1.0 §3.1.2.1)", async () => {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async function redirectFor(acrValues: string, acrClaim?: AcrClaimRequest) {
|
||||
async function redirectFor(
|
||||
acrValues: string,
|
||||
acrClaim?: AcrClaimRequest,
|
||||
scope?: string,
|
||||
) {
|
||||
let location = "";
|
||||
await authenticatedClient.$fetch(authorizeUrl(acrValues, acrClaim), {
|
||||
await authenticatedClient.$fetch(authorizeUrl(acrValues, acrClaim, scope), {
|
||||
onError(context) {
|
||||
location = context.response.headers.get("Location") || "";
|
||||
},
|
||||
@@ -594,6 +602,20 @@ describe("oauth authorize - acr_values (OIDC Core 1.0 §3.1.2.1)", async () => {
|
||||
expect(callbackRedirect.searchParams.get("code")).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores essential ID Token ACR claims in OAuth-only requests", async () => {
|
||||
const location = await redirectFor(
|
||||
"1",
|
||||
{ essential: true, value: "1" },
|
||||
"profile",
|
||||
);
|
||||
const callbackRedirect = new URL(location);
|
||||
|
||||
expect(callbackRedirect.searchParams.get("error")).toBeNull();
|
||||
expect(callbackRedirect.searchParams.get("code")).toEqual(
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts the current ACR in an essential values request", async () => {
|
||||
const location = await redirectFor("1", {
|
||||
essential: true,
|
||||
|
||||
@@ -515,19 +515,6 @@ export async function authorizeEndpoint(
|
||||
getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"),
|
||||
);
|
||||
}
|
||||
if (!canSatisfyEssentialAcrRequest(query.claims, LEVEL_0_ACR)) {
|
||||
return handleRedirect(
|
||||
ctx,
|
||||
formatErrorURL(
|
||||
query.redirect_uri,
|
||||
"access_denied",
|
||||
"essential acr requirement cannot be met",
|
||||
query.state,
|
||||
getIssuer(ctx, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Check for invalid scopes if requested from query
|
||||
let requestedScopes = query.scope?.split(" ").filter((s) => s);
|
||||
if (requestedScopes) {
|
||||
@@ -553,6 +540,21 @@ export async function authorizeEndpoint(
|
||||
requestedScopes = client.scopes ?? opts.scopes ?? [];
|
||||
query.scope = requestedScopes.join(" ");
|
||||
}
|
||||
if (
|
||||
requestedScopes.includes("openid") &&
|
||||
!canSatisfyEssentialAcrRequest(query.claims, LEVEL_0_ACR)
|
||||
) {
|
||||
return handleRedirect(
|
||||
ctx,
|
||||
formatErrorURL(
|
||||
query.redirect_uri,
|
||||
"access_denied",
|
||||
"essential acr requirement cannot be met",
|
||||
query.state,
|
||||
getIssuer(ctx, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
const requestedUserInfoClaims = getRequestedUserInfoClaims(
|
||||
query.claims,
|
||||
getSupportedClaims(opts),
|
||||
|
||||
Reference in New Issue
Block a user