Skip to content

Commit 613305e

Browse files
authored
refactor(core): make userinfo endpoint optional for OIDC SSO connectors (#7392)
refactor: make the `userinfo_endpoint` field optional make the `userinfo_endpoint` field optional in OIDC SSO connector configuration
1 parent d839787 commit 613305e

File tree

5 files changed

+31
-7
lines changed

5 files changed

+31
-7
lines changed

.changeset/dirty-mice-fail.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@logto/core": minor
3+
---
4+
5+
refactor: make the `userinfo_endpoint` field optional in the OIDC connector configuration to support providers like Azure AD B2C that do not expose a userinfo endpoint
6+
7+
Azure AD B2C SSO applications do not provide a userinfo_endpoint in their OIDC metadata. This has been a blocker for users attempting to integrate Azure AD B2C SSO with Logto, as our current implementation strictly follows the OIDC spec and relies on the userinfo endpoint to retrieve user claims after authentication.
8+
9+
- Updated the OIDC config response schema to make the userinfo_endpoint optional for OIDC based SSO providers.
10+
- If the `userinfo_endpoint` is missing from the provider's OIDC metadata, the system will now extract user data directly from the `id_token` claims.
11+
- If the `userinfo_endpoint` is present, the system will continue to retrieve user claims by calling the endpoint (existing behavior).
12+
13+
`userinfo_endpoint` is a standard OIDC field that specifies the endpoint for retrieving user information. For most of the OIDC providers, this update will not affect this existing implementation. However, for Azure AD B2C, this change allows users to successfully authenticate and retrieve user claims without the need for a userinfo endpoint.

packages/core/src/saml-application/SamlApplication/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,12 @@ export class SamlApplication {
345345
accessToken: string;
346346
}): Promise<IdTokenProfileStandardClaims & Record<string, unknown>> => {
347347
const { userinfoEndpoint } = await this.fetchOidcConfig();
348+
349+
// We reuse the fetchOidcConfig function from SSO connector to fetch the OIDC config.
350+
// userinfo endpoint is not required in the OIDC config.
351+
// But it is mandatory in Logto OIDC flow. So we should always have a userinfo endpoint.
352+
assertThat(userinfoEndpoint, new Error('Userinfo endpoint is not available'));
353+
348354
const body = await getRawUserInfoResponse(accessToken, userinfoEndpoint);
349355
const result = idTokenProfileStandardClaimsGuard
350356
.catchall(z.unknown())

packages/core/src/sso/AzureOidcSsoConnector/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ export class AzureOidcSsoConnector extends OidcConnector implements SingleSignOn
9797
// Verify the id token and get the claims
9898
const idTokenClaims = await getIdTokenClaims(idToken, oidcConfig, nonce, jwtVerifyOptions);
9999
// Fetch user info from the userinfo endpoint
100-
const userInfoClaims = await getUserInfo(accessToken, oidcConfig.userinfoEndpoint);
100+
const userInfoClaims = oidcConfig.userinfoEndpoint
101+
? await getUserInfo(accessToken, oidcConfig.userinfoEndpoint)
102+
: undefined;
101103

102104
// Merge the claims from id token and userinfo endpoint as in Azure AD, some claims are only available in the userinfo endpoint
103105
const mergedClaims = {

packages/core/src/sso/OidcConnector/index.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,18 @@ class OidcConnector {
121121
})
122122
);
123123

124-
// Verify the id token and get the user id
125-
const { sub: id } = await getIdTokenClaims(idToken, oidcConfig, nonce);
124+
// Verify the id token and get the user claims
125+
const idTokenClaims = await getIdTokenClaims(idToken, oidcConfig, nonce);
126126

127-
// Fetch user info from the userinfo endpoint
127+
// If userinfo endpoint is not provided, use the id token claims as user info,
128+
// otherwise, fetch the user info from the userinfo endpoint
128129
const { sub, name, picture, email, email_verified, phone, phone_verified, ...rest } =
129-
await getUserInfo(accessToken, oidcConfig.userinfoEndpoint);
130+
oidcConfig.userinfoEndpoint
131+
? await getUserInfo(accessToken, oidcConfig.userinfoEndpoint)
132+
: idTokenClaims;
130133

131134
return {
132-
id,
135+
id: sub,
133136
...conditional(name && { name }),
134137
...conditional(picture && { avatar: picture }),
135138
...conditional(email && email_verified && { email }),

packages/core/src/sso/types/oidc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export type BasicOidcConnectorConfig = z.infer<typeof basicOidcConnectorConfigGu
3636
export const oidcConfigResponseGuard = z.object({
3737
authorization_endpoint: z.string(),
3838
token_endpoint: z.string(),
39-
userinfo_endpoint: z.string(),
39+
userinfo_endpoint: z.string().optional(),
4040
jwks_uri: z.string(),
4141
issuer: z.string(),
4242
});

0 commit comments

Comments
 (0)