mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
fix(core): fix azure oidc sso connector authorization error (#5912)
* fix(core): fix azure oidc sso connector authorization error fix azure oidc sso connector authorization error * chore: add changeset add changeset * chore: update changeset update changeset * fix(core): dynamicly verify multi-tenant azure oidc issuer dynamicly verify multi-tenant azure oidc issuer
This commit is contained in:
parent
94b688355c
commit
458746c9ac
5 changed files with 40 additions and 11 deletions
12
.changeset/serious-geese-admire.md
Normal file
12
.changeset/serious-geese-admire.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
"@logto/core": patch
|
||||
---
|
||||
|
||||
fix Microsoft EntraID OIDC SSO connector invalid authorization code response bug
|
||||
|
||||
- For public organizations access EntraID OIDC applications, the token endpoint returns `expires_in` value type in number.
|
||||
- For private organization access only applications, the token endpoint returns `expires_in` value type in string.
|
||||
- Expected `expires_in` value type is number. (See [v2-oauth2-auth-code-flow](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#successful-response-2) for reference)
|
||||
|
||||
String type `expires_in` value is not supported by the current Microsoft EntraID OIDC connector, a invalid authorization response error will be thrown.
|
||||
Update the token response guard to handle both number and string type `expires_in` value. Make the SSO connector more robust.
|
|
@ -79,7 +79,7 @@ export default function singleSignOnRoutes<T extends IRouterParamContext>(
|
|||
connectorId: z.string(),
|
||||
}),
|
||||
body: z.record(z.unknown()),
|
||||
status: [200, 404, 422],
|
||||
status: [200, 404, 422, 500],
|
||||
response: z.object({
|
||||
redirectTo: z.string(),
|
||||
}),
|
||||
|
@ -125,7 +125,7 @@ export default function singleSignOnRoutes<T extends IRouterParamContext>(
|
|||
params: z.object({
|
||||
connectorId: z.string(),
|
||||
}),
|
||||
status: [200, 404, 403],
|
||||
status: [200, 404, 403, 500],
|
||||
response: z.object({
|
||||
redirectTo: z.string(),
|
||||
}),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { SsoProviderName, SsoProviderType } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
import { decodeJwt } from 'jose';
|
||||
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
|
@ -26,6 +27,7 @@ export class AzureOidcSsoConnector extends OidcSsoConnector {
|
|||
* It is unsafe to trust the unverified email and phone number in Logto's context. As we are using the verified email and phone number to identify the user.
|
||||
* Store extra unverified_email and unverified_phone fields in the user SSO identity profile instead.
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
override async getUserInfo(
|
||||
connectorSession: SingleSignOnConnectorSession,
|
||||
data: unknown
|
||||
|
@ -43,8 +45,19 @@ export class AzureOidcSsoConnector extends OidcSsoConnector {
|
|||
})
|
||||
);
|
||||
|
||||
// Need to decode the id token to get the tenant id
|
||||
const decodeToken = decodeJwt(idToken);
|
||||
|
||||
// For multi-tenancy Azure application, the issuer may contain the tenant id placeholder
|
||||
// Replace the placeholder with the tid retrieved from the id token
|
||||
// @see https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens#validation-of-the-signing-key-issuer
|
||||
const jwtVerifyOptions =
|
||||
oidcConfig.issuer.includes('{tenantid}') && typeof decodeToken.tid === 'string'
|
||||
? { issuer: oidcConfig.issuer.replace('{tenantid}', decodeToken.tid) }
|
||||
: {};
|
||||
|
||||
// Verify the id token and get the user id
|
||||
const { sub: id } = await getIdTokenClaims(idToken, oidcConfig, nonce);
|
||||
const { sub: id } = await getIdTokenClaims(idToken, oidcConfig, nonce, jwtVerifyOptions);
|
||||
|
||||
// Fetch user info from the userinfo endpoint
|
||||
const { sub, name, picture, email, email_verified, phone, phone_verified, ...rest } =
|
||||
|
|
|
@ -2,22 +2,22 @@ import { parseJson } from '@logto/connector-kit';
|
|||
import { assert } from '@silverhand/essentials';
|
||||
import camelcaseKeys, { type CamelCaseKeys } from 'camelcase-keys';
|
||||
import { got, HTTPError } from 'got';
|
||||
import { jwtVerify, createRemoteJWKSet } from 'jose';
|
||||
import { createRemoteJWKSet, jwtVerify, type JWTVerifyOptions } from 'jose';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
SsoConnectorConfigErrorCodes,
|
||||
SsoConnectorError,
|
||||
SsoConnectorErrorCodes,
|
||||
SsoConnectorConfigErrorCodes,
|
||||
} from '../types/error.js';
|
||||
import {
|
||||
idTokenProfileStandardClaimsGuard,
|
||||
oidcAuthorizationResponseGuard,
|
||||
oidcConfigResponseGuard,
|
||||
oidcTokenResponseGuard,
|
||||
type BaseOidcConfig,
|
||||
type OidcConfigResponse,
|
||||
oidcConfigResponseGuard,
|
||||
oidcAuthorizationResponseGuard,
|
||||
oidcTokenResponseGuard,
|
||||
type OidcTokenResponse,
|
||||
idTokenProfileStandardClaimsGuard,
|
||||
} from '../types/oidc.js';
|
||||
|
||||
export const fetchOidcConfig = async (
|
||||
|
@ -109,12 +109,15 @@ const issuedAtTimeTolerance = 600; // 10 minutes
|
|||
export const getIdTokenClaims = async (
|
||||
idToken: string,
|
||||
config: BaseOidcConfig,
|
||||
nonceFromSession?: string
|
||||
nonceFromSession?: string,
|
||||
// Allow to pass custom options for jwt.verify
|
||||
jwtVerifyOptions?: JWTVerifyOptions
|
||||
) => {
|
||||
try {
|
||||
const { payload } = await jwtVerify(idToken, createRemoteJWKSet(new URL(config.jwksUri)), {
|
||||
issuer: config.issuer,
|
||||
audience: config.clientId,
|
||||
...jwtVerifyOptions,
|
||||
});
|
||||
|
||||
if (Math.abs((payload.iat ?? 0) - Date.now() / 1000) > issuedAtTimeTolerance) {
|
||||
|
|
|
@ -57,7 +57,8 @@ export const oidcTokenResponseGuard = z.object({
|
|||
id_token: z.string(),
|
||||
access_token: z.string().optional(),
|
||||
token_type: z.string().optional(),
|
||||
expires_in: z.number().optional(),
|
||||
// Microsoft EntraID may return string type for expires_in
|
||||
expires_in: z.number().or(z.string()).optional(),
|
||||
refresh_token: z.string().optional(),
|
||||
scope: z.string().optional(),
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue