Skip to content

Commit 7425f72

Browse files
gustavovalverdehimself65
authored andcommitted
fix(oauth-provider): customIdTokenClaims should override standard claims (#7865)
1 parent e3ff2b3 commit 7425f72

File tree

2 files changed

+138
-1
lines changed

2 files changed

+138
-1
lines changed

packages/oauth-provider/src/token.test.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,143 @@ describe("oauth token - client_credentials", async () => {
11341134
});
11351135
});
11361136

1137+
describe("oauth token - customIdTokenClaims precedence", async () => {
1138+
const authServerBaseUrl = "http://localhost:3000";
1139+
const rpBaseUrl = "http://localhost:5000";
1140+
const { auth, signInWithTestUser, customFetchImpl, testUser } =
1141+
await getTestInstance({
1142+
baseURL: authServerBaseUrl,
1143+
plugins: [
1144+
jwt({
1145+
jwt: {
1146+
issuer: authServerBaseUrl,
1147+
},
1148+
}),
1149+
oauthProvider({
1150+
loginPage: "/login",
1151+
consentPage: "/consent",
1152+
silenceWarnings: {
1153+
oauthAuthServerConfig: true,
1154+
openidConfig: true,
1155+
},
1156+
customIdTokenClaims: () => ({
1157+
given_name: "CustomFirst",
1158+
family_name: "CustomLast",
1159+
custom_field: "custom_value",
1160+
}),
1161+
}),
1162+
],
1163+
});
1164+
1165+
const { headers } = await signInWithTestUser();
1166+
const client = createAuthClient({
1167+
plugins: [oauthProviderClient(), jwtClient()],
1168+
baseURL: authServerBaseUrl,
1169+
fetchOptions: {
1170+
customFetchImpl,
1171+
headers,
1172+
},
1173+
});
1174+
1175+
let oauthClient: OAuthClient | null;
1176+
const providerId = "test";
1177+
const redirectUri = `${rpBaseUrl}/api/auth/oauth2/callback/${providerId}`;
1178+
const state = "123";
1179+
let jwks: ReturnType<typeof createLocalJWKSet>;
1180+
1181+
beforeAll(async () => {
1182+
const response = await auth.api.adminCreateOAuthClient({
1183+
headers,
1184+
body: {
1185+
redirect_uris: [redirectUri],
1186+
skip_consent: true,
1187+
},
1188+
});
1189+
expect(response?.client_id).toBeDefined();
1190+
expect(response?.client_secret).toBeDefined();
1191+
expect(response?.redirect_uris).toBeDefined();
1192+
oauthClient = response;
1193+
1194+
const jwksResult = await client.jwks();
1195+
if (!jwksResult.data) {
1196+
throw new Error("Unable to fetch jwks");
1197+
}
1198+
jwks = createLocalJWKSet(jwksResult.data);
1199+
});
1200+
1201+
it("custom claims should override standard profile claims in id_token", async ({
1202+
expect,
1203+
}) => {
1204+
if (!oauthClient?.client_id || !oauthClient?.client_secret) {
1205+
throw Error("beforeAll not run properly");
1206+
}
1207+
1208+
const scopes = ["openid", "profile"];
1209+
const codeVerifier = generateRandomString(32);
1210+
const url = await createAuthorizationURL({
1211+
id: providerId,
1212+
options: {
1213+
clientId: oauthClient.client_id,
1214+
clientSecret: oauthClient.client_secret,
1215+
redirectURI: redirectUri,
1216+
},
1217+
redirectURI: "",
1218+
authorizationEndpoint: `${authServerBaseUrl}/api/auth/oauth2/authorize`,
1219+
state,
1220+
scopes,
1221+
codeVerifier,
1222+
});
1223+
1224+
let callbackRedirectUrl = "";
1225+
await client.$fetch(url.toString(), {
1226+
onError(context) {
1227+
callbackRedirectUrl = context.response.headers.get("Location") || "";
1228+
},
1229+
});
1230+
expect(callbackRedirectUrl).not.toBe("");
1231+
expect(callbackRedirectUrl).toContain(redirectUri);
1232+
1233+
const callbackUrl = new URL(callbackRedirectUrl);
1234+
const code = callbackUrl.searchParams.get("code");
1235+
const returnedState = callbackUrl.searchParams.get("state");
1236+
1237+
expect(code).toBeTruthy();
1238+
expect(returnedState).toBe(state);
1239+
1240+
const { body, headers: reqHeaders } = createAuthorizationCodeRequest({
1241+
code: code!,
1242+
codeVerifier,
1243+
redirectURI: redirectUri,
1244+
options: {
1245+
clientId: oauthClient.client_id,
1246+
clientSecret: oauthClient.client_secret,
1247+
redirectURI: redirectUri,
1248+
},
1249+
});
1250+
1251+
const tokens = await client.$fetch<{
1252+
id_token?: string;
1253+
[key: string]: unknown;
1254+
}>("/oauth2/token", {
1255+
method: "POST",
1256+
body,
1257+
headers: reqHeaders,
1258+
});
1259+
1260+
expect(tokens.data?.id_token).toBeDefined();
1261+
const idToken = await jwtVerify(tokens.data?.id_token!, jwks);
1262+
1263+
// Custom claims must override the auto-derived profile claims
1264+
expect(idToken.payload.given_name).toBe("CustomFirst");
1265+
expect(idToken.payload.family_name).toBe("CustomLast");
1266+
expect(idToken.payload.custom_field).toBe("custom_value");
1267+
1268+
// Standard name should still come from the user record (not overridden)
1269+
expect(idToken.payload.name).toBe(testUser.name);
1270+
expect(idToken.payload.sub).toBeDefined();
1271+
});
1272+
});
1273+
11371274
describe("oauth token - config", async () => {
11381275
const authServerBaseUrl = "http://localhost:3000";
11391276
const rpBaseUrl = "http://localhost:5000";

packages/oauth-provider/src/token.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ async function createIdToken(
152152
: getJwtPlugin(ctx.context).options;
153153

154154
const payload: JWTPayload = {
155-
...customClaims,
156155
...userClaims,
156+
...customClaims,
157157
auth_time: authTimeSec,
158158
acr,
159159
iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,

0 commit comments

Comments
 (0)