fix(oauth-provider): scope essential ACR checks to OIDC

This commit is contained in:
Taesu
2026-08-14 00:03:10 +09:00
parent b554b91076
commit a172ca3abe
4 changed files with 44 additions and 20 deletions
+2 -2
View File
@@ -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.
+1 -1
View File
@@ -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
+26 -4
View File
@@ -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,
+15 -13
View File
@@ -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),