0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

feat(core): add OKTA sso connector (#4951)

add OKTA sso connector
This commit is contained in:
simeng-li 2023-11-23 13:35:28 +08:00 committed by GitHub
parent 6785cfcd3e
commit a36ae032e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 100 additions and 0 deletions

View file

@ -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;
}
};

View 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,
};

View file

@ -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([

View file

@ -19,6 +19,7 @@ export enum SsoProviderName {
SAML = 'SAML',
AZURE_AD = 'AzureAD',
GOOGLE_WORKSPACE = 'GoogleWorkspace',
OKTA = 'Okta',
}
export const singleSignOnDomainBlackList = Object.freeze([