mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
parent
6785cfcd3e
commit
a36ae032e3
4 changed files with 100 additions and 0 deletions
|
@ -3,6 +3,7 @@ import { assert } from '@silverhand/essentials';
|
|||
import camelcaseKeys, { type CamelCaseKeys } from 'camelcase-keys';
|
||||
import { got, HTTPError } from 'got';
|
||||
import { jwtVerify, createRemoteJWKSet } from 'jose';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type BaseOidcConfig,
|
||||
|
@ -117,3 +118,34 @@ export const getIdTokenClaims = async (
|
|||
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid, error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the user info from the userinfo endpoint incase id token does not contain sufficient user claims.
|
||||
*/
|
||||
export const getUserInfo = async (accessToken: string, userinfoEndpoint: string) => {
|
||||
try {
|
||||
const httpResponse = await got.get(userinfoEndpoint, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
responseType: 'json',
|
||||
});
|
||||
|
||||
const result = idTokenProfileStandardClaimsGuard
|
||||
.catchall(z.unknown())
|
||||
.safeParse(httpResponse.body);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
||||
}
|
||||
|
||||
const { data } = result;
|
||||
|
||||
return data;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof HTTPError) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, error.response.body);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
63
packages/core/src/sso/OktaSsoConnector/index.ts
Normal file
63
packages/core/src/sso/OktaSsoConnector/index.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
|
||||
import { SsoProviderName } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { fetchToken, getUserInfo, getIdTokenClaims } from '../OidcConnector/utils.js';
|
||||
import { OidcSsoConnector } from '../OidcSsoConnector/index.js';
|
||||
import { type SingleSignOnFactory } from '../index.js';
|
||||
import { basicOidcConnectorConfigGuard } from '../types/oidc.js';
|
||||
import { type ExtendedSocialUserInfo } from '../types/saml.js';
|
||||
import { type SingleSignOnConnectorSession } from '../types/session.js';
|
||||
|
||||
export class OktaSsoConnector extends OidcSsoConnector {
|
||||
/**
|
||||
* Override the getUserInfo method from the OidcSsoConnector class
|
||||
*
|
||||
* @remark Okta's IdToken does not include the sufficient user claims like email_verified, phone_verified, etc. {@link https://devforum.okta.com/t/email-verified-claim/3516/2}
|
||||
* This method will fetch the user info from the userinfo endpoint instead.
|
||||
*/
|
||||
override async getUserInfo(
|
||||
connectorSession: SingleSignOnConnectorSession,
|
||||
data: unknown
|
||||
): Promise<ExtendedSocialUserInfo> {
|
||||
const oidcConfig = await this.getOidcConfig();
|
||||
const { nonce, redirectUri } = connectorSession;
|
||||
|
||||
// Fetch token from the OIDC provider using authorization code
|
||||
const { idToken, accessToken } = await fetchToken(oidcConfig, data, redirectUri);
|
||||
|
||||
assertThat(
|
||||
accessToken,
|
||||
new ConnectorError(ConnectorErrorCodes.AuthorizationFailed, 'access_token is empty.')
|
||||
);
|
||||
|
||||
// Verify the id token and get the user id
|
||||
const { sub: id } = await getIdTokenClaims(idToken, oidcConfig, nonce);
|
||||
|
||||
// Fetch user info from the userinfo endpoint
|
||||
const { sub, name, picture, email, email_verified, phone, phone_verified, ...rest } =
|
||||
await getUserInfo(accessToken, oidcConfig.userinfoEndpoint);
|
||||
|
||||
return {
|
||||
id,
|
||||
...conditional(name && { name }),
|
||||
...conditional(picture && { avatar: picture }),
|
||||
...conditional(email && email_verified && { email }),
|
||||
...conditional(phone && phone_verified && { phone }),
|
||||
...camelcaseKeys(rest),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const oktaSsoConnectorFactory: SingleSignOnFactory<SsoProviderName.OKTA> = {
|
||||
providerName: SsoProviderName.OKTA,
|
||||
logo: 'https://logtodev.blob.core.windows.net/public-blobs/admin/r2a6qctI3lmG/2023/11/22/8bvg68e7/OKTA.D.png',
|
||||
description: {
|
||||
en: 'This connector is used to connect with Okta Single Sign-On.',
|
||||
},
|
||||
configGuard: basicOidcConnectorConfigGuard,
|
||||
constructor: OktaSsoConnector,
|
||||
};
|
|
@ -11,6 +11,7 @@ import {
|
|||
type googleWorkspaceSsoConnectorConfigGuard,
|
||||
} from './GoogleWorkspaceSsoConnector/index.js';
|
||||
import { oidcSsoConnectorFactory, type OidcSsoConnector } from './OidcSsoConnector/index.js';
|
||||
import { oktaSsoConnectorFactory, type OktaSsoConnector } from './OktaSsoConnector/index.js';
|
||||
import { type SamlSsoConnector, samlSsoConnectorFactory } from './SamlSsoConnector/index.js';
|
||||
import { type basicOidcConnectorConfigGuard } from './types/oidc.js';
|
||||
import { type samlConnectorConfigGuard } from './types/saml.js';
|
||||
|
@ -20,6 +21,7 @@ type SingleSignOnConstructor = {
|
|||
[SsoProviderName.SAML]: typeof SamlSsoConnector;
|
||||
[SsoProviderName.AZURE_AD]: typeof AzureAdSsoConnector;
|
||||
[SsoProviderName.GOOGLE_WORKSPACE]: typeof GoogleWorkspaceSsoConnector;
|
||||
[SsoProviderName.OKTA]: typeof OktaSsoConnector;
|
||||
};
|
||||
|
||||
type SingleSignOnConnectorConfig = {
|
||||
|
@ -27,6 +29,7 @@ type SingleSignOnConnectorConfig = {
|
|||
[SsoProviderName.SAML]: typeof samlConnectorConfigGuard;
|
||||
[SsoProviderName.AZURE_AD]: typeof samlConnectorConfigGuard;
|
||||
[SsoProviderName.GOOGLE_WORKSPACE]: typeof googleWorkspaceSsoConnectorConfigGuard;
|
||||
[SsoProviderName.OKTA]: typeof basicOidcConnectorConfigGuard;
|
||||
};
|
||||
|
||||
export type SingleSignOnFactory<T extends SsoProviderName> = {
|
||||
|
@ -44,6 +47,7 @@ export const ssoConnectorFactories: {
|
|||
[SsoProviderName.SAML]: samlSsoConnectorFactory,
|
||||
[SsoProviderName.AZURE_AD]: azureAdSsoConnectorFactory,
|
||||
[SsoProviderName.GOOGLE_WORKSPACE]: googleWorkSpaceSsoConnectorFactory,
|
||||
[SsoProviderName.OKTA]: oktaSsoConnectorFactory,
|
||||
};
|
||||
|
||||
export const standardSsoConnectorProviders = Object.freeze([
|
||||
|
|
|
@ -19,6 +19,7 @@ export enum SsoProviderName {
|
|||
SAML = 'SAML',
|
||||
AZURE_AD = 'AzureAD',
|
||||
GOOGLE_WORKSPACE = 'GoogleWorkspace',
|
||||
OKTA = 'Okta',
|
||||
}
|
||||
|
||||
export const singleSignOnDomainBlackList = Object.freeze([
|
||||
|
|
Loading…
Add table
Reference in a new issue