mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
feat(core): implement enterprise sso verification flow (#6198)
implement the enterprise sso verification flow
This commit is contained in:
parent
d7fa9f5900
commit
addb528652
10 changed files with 611 additions and 9 deletions
|
@ -59,7 +59,7 @@ export default class ExperienceInteraction {
|
|||
constructor(
|
||||
private readonly ctx: WithLogContext,
|
||||
private readonly tenant: TenantContext,
|
||||
interactionDetails: Interaction
|
||||
public interactionDetails: Interaction
|
||||
) {
|
||||
const { libraries, queries } = tenant;
|
||||
|
||||
|
@ -125,8 +125,12 @@ export default class ExperienceInteraction {
|
|||
switch (verificationRecord.type) {
|
||||
case VerificationType.Password:
|
||||
case VerificationType.VerificationCode:
|
||||
case VerificationType.Social: {
|
||||
case VerificationType.Social:
|
||||
case VerificationType.EnterpriseSso: {
|
||||
// TODO: social sign-in with verified email
|
||||
|
||||
const { id, isSuspended } = await verificationRecord.identifyUser();
|
||||
|
||||
assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||
|
||||
// Throws an 409 error if the current session has already identified a different user
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
import { socialUserInfoGuard, type SocialUserInfo, type ToZodObject } from '@logto/connector-kit';
|
||||
import {
|
||||
VerificationType,
|
||||
type JsonObject,
|
||||
type SocialAuthorizationUrlPayload,
|
||||
type SupportedSsoConnector,
|
||||
type User,
|
||||
type UserSsoIdentity,
|
||||
} from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import {
|
||||
getSsoAuthorizationUrl,
|
||||
verifySsoIdentity,
|
||||
} from '#src/routes/interaction/utils/single-sign-on.js';
|
||||
import type Libraries from '#src/tenants/Libraries.js';
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { type VerificationRecord } from './verification-record.js';
|
||||
|
||||
/** The JSON data type for the EnterpriseSsoVerification record stored in the interaction storage */
|
||||
export type EnterpriseSsoVerificationRecordData = {
|
||||
id: string;
|
||||
connectorId: string;
|
||||
type: VerificationType.EnterpriseSso;
|
||||
/**
|
||||
* The enterprise SSO identity returned by the connector.
|
||||
*/
|
||||
enterpriseSsoUserInfo?: SocialUserInfo;
|
||||
issuer?: string;
|
||||
};
|
||||
|
||||
export const enterPriseSsoVerificationRecordDataGuard = z.object({
|
||||
id: z.string(),
|
||||
connectorId: z.string(),
|
||||
type: z.literal(VerificationType.EnterpriseSso),
|
||||
enterpriseSsoUserInfo: socialUserInfoGuard.optional(),
|
||||
issuer: z.string().optional(),
|
||||
}) satisfies ToZodObject<EnterpriseSsoVerificationRecordData>;
|
||||
|
||||
export class EnterpriseSsoVerification
|
||||
implements VerificationRecord<VerificationType.EnterpriseSso>
|
||||
{
|
||||
static create(libraries: Libraries, queries: Queries, connectorId: string) {
|
||||
return new EnterpriseSsoVerification(libraries, queries, {
|
||||
id: generateStandardId(),
|
||||
connectorId,
|
||||
type: VerificationType.EnterpriseSso,
|
||||
});
|
||||
}
|
||||
|
||||
public readonly id: string;
|
||||
public readonly type = VerificationType.EnterpriseSso;
|
||||
public readonly connectorId: string;
|
||||
public enterpriseSsoUserInfo?: SocialUserInfo;
|
||||
public issuer?: string;
|
||||
|
||||
private connectorDataCache?: SupportedSsoConnector;
|
||||
|
||||
constructor(
|
||||
private readonly libraries: Libraries,
|
||||
private readonly queries: Queries,
|
||||
data: EnterpriseSsoVerificationRecordData
|
||||
) {
|
||||
const { id, connectorId, enterpriseSsoUserInfo, issuer } =
|
||||
enterPriseSsoVerificationRecordDataGuard.parse(data);
|
||||
|
||||
this.id = id;
|
||||
this.connectorId = connectorId;
|
||||
this.enterpriseSsoUserInfo = enterpriseSsoUserInfo;
|
||||
this.issuer = issuer;
|
||||
}
|
||||
|
||||
/** Returns true if the enterprise SSO identity has been verified */
|
||||
get isVerified() {
|
||||
return Boolean(this.enterpriseSsoUserInfo && this.issuer);
|
||||
}
|
||||
|
||||
async getConnectorData(connectorId: string) {
|
||||
this.connectorDataCache ||= await this.libraries.ssoConnectors.getSsoConnectorById(connectorId);
|
||||
|
||||
return this.connectorDataCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the authorization URL for the enterprise SSO connector.
|
||||
*
|
||||
* @remarks
|
||||
* Refers to thr {@link getSsoAuthorizationUrl} function in the interaction/utils/single-sign-on.ts file.
|
||||
* Currently, all the intermediate connector session results are stored in the provider's interactionDetails separately,
|
||||
* apart from the new verification record.
|
||||
* For compatibility reasons, we keep using the old {@link getSsoAuthorizationUrl} method here as a single source of truth.
|
||||
* Especially for the SAML connectors,
|
||||
* SAML ACS endpoint will find the connector session result by the jti and assign it to the interaction storage.
|
||||
* We will need to update the SAML ACS endpoint before move the logic to this new EnterpriseSsoVerification class.
|
||||
*/
|
||||
async createAuthorizationUrl(
|
||||
ctx: WithLogContext,
|
||||
tenantContext: TenantContext,
|
||||
payload: SocialAuthorizationUrlPayload
|
||||
) {
|
||||
const connectorData = await this.getConnectorData(this.connectorId);
|
||||
return getSsoAuthorizationUrl(ctx, tenantContext, connectorData, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the enterprise SSO identity and store the enterprise SSO identity in the verification record.
|
||||
*
|
||||
* @remarks
|
||||
* Refers to the {@link verifySsoIdentity} function in the interaction/utils/single-sign-on.ts file.
|
||||
* For compatibility reasons, we keep using the old {@link verifySsoIdentity} method here as a single source of truth.
|
||||
* See the above {@link createAuthorizationUrl} method for more details.
|
||||
*/
|
||||
async verify(ctx: WithLogContext, tenantContext: TenantContext, callbackData: JsonObject) {
|
||||
const connectorData = await this.getConnectorData(this.connectorId);
|
||||
const { issuer, userInfo } = await verifySsoIdentity(
|
||||
ctx,
|
||||
tenantContext,
|
||||
connectorData,
|
||||
callbackData
|
||||
);
|
||||
|
||||
this.issuer = issuer;
|
||||
this.enterpriseSsoUserInfo = userInfo;
|
||||
}
|
||||
|
||||
async identifyUser(): Promise<User> {
|
||||
assertThat(
|
||||
this.isVerified,
|
||||
new RequestError({ code: 'session.verification_failed', status: 422 })
|
||||
);
|
||||
|
||||
// TODO: sync userInfo and link sso identity
|
||||
|
||||
const userSsoIdentityResult = await this.findUserSsoIdentityByEnterpriseSsoUserInfo();
|
||||
|
||||
if (userSsoIdentityResult) {
|
||||
return userSsoIdentityResult.user;
|
||||
}
|
||||
|
||||
const relatedUser = await this.findRelatedUserSsoIdentity();
|
||||
|
||||
if (relatedUser) {
|
||||
return relatedUser;
|
||||
}
|
||||
|
||||
throw new RequestError({ code: 'user.identity_not_exist', status: 404 });
|
||||
}
|
||||
|
||||
toJson(): EnterpriseSsoVerificationRecordData {
|
||||
const { id, connectorId, type, enterpriseSsoUserInfo, issuer } = this;
|
||||
|
||||
return {
|
||||
id,
|
||||
connectorId,
|
||||
type,
|
||||
enterpriseSsoUserInfo,
|
||||
issuer,
|
||||
};
|
||||
}
|
||||
|
||||
private async findUserSsoIdentityByEnterpriseSsoUserInfo(): Promise<
|
||||
| {
|
||||
user: User;
|
||||
userSsoIdentity: UserSsoIdentity;
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
const { userSsoIdentities: userSsoIdentitiesQueries, users: usersQueries } = this.queries;
|
||||
|
||||
if (!this.issuer || !this.enterpriseSsoUserInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userSsoIdentity = await userSsoIdentitiesQueries.findUserSsoIdentityBySsoIdentityId(
|
||||
this.issuer,
|
||||
this.enterpriseSsoUserInfo.id
|
||||
);
|
||||
|
||||
if (userSsoIdentity) {
|
||||
const user = await usersQueries.findUserById(userSsoIdentity.userId);
|
||||
return {
|
||||
user,
|
||||
userSsoIdentity,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the related user by the enterprise SSO identity's verified email.
|
||||
*/
|
||||
private async findRelatedUserSsoIdentity(): Promise<User | undefined> {
|
||||
const { users: usersQueries } = this.queries;
|
||||
|
||||
if (!this.enterpriseSsoUserInfo?.email) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await usersQueries.findUserByEmail(this.enterpriseSsoUserInfo.email);
|
||||
|
||||
return user ?? undefined;
|
||||
}
|
||||
}
|
|
@ -9,6 +9,11 @@ import {
|
|||
codeVerificationRecordDataGuard,
|
||||
type CodeVerificationRecordData,
|
||||
} from './code-verification.js';
|
||||
import {
|
||||
EnterpriseSsoVerification,
|
||||
enterPriseSsoVerificationRecordDataGuard,
|
||||
type EnterpriseSsoVerificationRecordData,
|
||||
} from './enterprise-sso-verification.js';
|
||||
import {
|
||||
PasswordVerification,
|
||||
passwordVerificationRecordDataGuard,
|
||||
|
@ -23,7 +28,8 @@ import {
|
|||
export type VerificationRecordData =
|
||||
| PasswordVerificationRecordData
|
||||
| CodeVerificationRecordData
|
||||
| SocialVerificationRecordData;
|
||||
| SocialVerificationRecordData
|
||||
| EnterpriseSsoVerificationRecordData;
|
||||
|
||||
/**
|
||||
* Union type for all verification record types
|
||||
|
@ -33,12 +39,17 @@ export type VerificationRecordData =
|
|||
* This union type is used to narrow down the type of the verification record.
|
||||
* Used in the ExperienceInteraction class to store and manage all the verification records. With a given verification type, we can narrow down the type of the verification record.
|
||||
*/
|
||||
export type VerificationRecord = PasswordVerification | CodeVerification | SocialVerification;
|
||||
export type VerificationRecord =
|
||||
| PasswordVerification
|
||||
| CodeVerification
|
||||
| SocialVerification
|
||||
| EnterpriseSsoVerification;
|
||||
|
||||
export const verificationRecordDataGuard = z.discriminatedUnion('type', [
|
||||
passwordVerificationRecordDataGuard,
|
||||
codeVerificationRecordDataGuard,
|
||||
socialVerificationRecordDataGuard,
|
||||
enterPriseSsoVerificationRecordDataGuard,
|
||||
]);
|
||||
|
||||
/**
|
||||
|
@ -59,5 +70,8 @@ export const buildVerificationRecord = (
|
|||
case VerificationType.Social: {
|
||||
return new SocialVerification(libraries, queries, data);
|
||||
}
|
||||
case VerificationType.EnterpriseSso: {
|
||||
return new EnterpriseSsoVerification(libraries, queries, data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -106,9 +106,6 @@ export class SocialVerification implements VerificationRecord<VerificationType.S
|
|||
/**
|
||||
* Verify the social identity and store the social identity in the verification record.
|
||||
*
|
||||
* - Store the social identity in the verification record.
|
||||
* - Find the user by the social identity and store the userId in the verification record if the user exists.
|
||||
*
|
||||
* @remarks
|
||||
* Refer to the {@link verifySocialIdentity} method in the interaction/utils/social-verification.ts file.
|
||||
* For compatibility reasons, we keep using the old {@link verifySocialIdentity} method here as a single source of truth.
|
||||
|
@ -133,9 +130,11 @@ export class SocialVerification implements VerificationRecord<VerificationType.S
|
|||
async identifyUser(): Promise<User> {
|
||||
assertThat(
|
||||
this.isVerified,
|
||||
new RequestError({ code: 'session.verification_failed', status: 400 })
|
||||
new RequestError({ code: 'session.verification_failed', status: 422 })
|
||||
);
|
||||
|
||||
// TODO: sync userInfo and link social identity
|
||||
|
||||
const user = await this.findUserBySocialIdentity();
|
||||
|
||||
if (!user) {
|
||||
|
|
|
@ -23,6 +23,7 @@ import { experienceRoutes } from './const.js';
|
|||
import koaExperienceInteraction, {
|
||||
type WithExperienceInteractionContext,
|
||||
} from './middleware/koa-experience-interaction.js';
|
||||
import enterpriseSsoVerificationRoutes from './verification-routes/enterprise-sso-verification.js';
|
||||
import passwordVerificationRoutes from './verification-routes/password-verification.js';
|
||||
import socialVerificationRoutes from './verification-routes/social-verification.js';
|
||||
import verificationCodeRoutes from './verification-routes/verification-code.js';
|
||||
|
@ -82,4 +83,5 @@ export default function experienceApiRoutes<T extends AnonymousRouter>(
|
|||
passwordVerificationRoutes(router, tenant);
|
||||
verificationCodeRoutes(router, tenant);
|
||||
socialVerificationRoutes(router, tenant);
|
||||
enterpriseSsoVerificationRoutes(router, tenant);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
import {
|
||||
VerificationType,
|
||||
socialAuthorizationUrlPayloadGuard,
|
||||
socialVerificationCallbackPayloadGuard,
|
||||
} from '@logto/schemas';
|
||||
import type Router from 'koa-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { EnterpriseSsoVerification } from '../classes/verifications/enterprise-sso-verification.js';
|
||||
import { experienceRoutes } from '../const.js';
|
||||
import { type WithExperienceInteractionContext } from '../middleware/koa-experience-interaction.js';
|
||||
|
||||
export default function enterpriseSsoVerificationRoutes<T extends WithLogContext>(
|
||||
router: Router<unknown, WithExperienceInteractionContext<T>>,
|
||||
tenantContext: TenantContext
|
||||
) {
|
||||
const { libraries, queries } = tenantContext;
|
||||
|
||||
router.post(
|
||||
`${experienceRoutes.verification}/sso/:connectorId/authorization-uri`,
|
||||
koaGuard({
|
||||
params: z.object({
|
||||
connectorId: z.string(),
|
||||
}),
|
||||
body: socialAuthorizationUrlPayloadGuard,
|
||||
response: z.object({
|
||||
authorizationUri: z.string(),
|
||||
verificationId: z.string(),
|
||||
}),
|
||||
status: [200, 400, 404, 500],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { connectorId } = ctx.guard.params;
|
||||
|
||||
const enterpriseSsoVerification = EnterpriseSsoVerification.create(
|
||||
libraries,
|
||||
queries,
|
||||
connectorId
|
||||
);
|
||||
|
||||
const authorizationUri = await enterpriseSsoVerification.createAuthorizationUrl(
|
||||
ctx,
|
||||
tenantContext,
|
||||
ctx.guard.body
|
||||
);
|
||||
|
||||
ctx.experienceInteraction.setVerificationRecord(enterpriseSsoVerification);
|
||||
|
||||
await ctx.experienceInteraction.save();
|
||||
|
||||
ctx.body = {
|
||||
authorizationUri,
|
||||
verificationId: enterpriseSsoVerification.id,
|
||||
};
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
`${experienceRoutes.verification}/sso/:connectorId/verify`,
|
||||
koaGuard({
|
||||
params: z.object({
|
||||
connectorId: z.string(),
|
||||
}),
|
||||
body: socialVerificationCallbackPayloadGuard,
|
||||
response: z.object({
|
||||
verificationId: z.string(),
|
||||
}),
|
||||
status: [200, 400, 404, 500],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { connectorId } = ctx.params;
|
||||
const { connectorData, verificationId } = ctx.guard.body;
|
||||
|
||||
const enterpriseSsoVerificationRecord =
|
||||
ctx.experienceInteraction.getVerificationRecordById(verificationId);
|
||||
|
||||
assertThat(
|
||||
enterpriseSsoVerificationRecord &&
|
||||
enterpriseSsoVerificationRecord.type === VerificationType.EnterpriseSso &&
|
||||
enterpriseSsoVerificationRecord.connectorId === connectorId,
|
||||
new RequestError({ code: 'session.verification_session_not_found', status: 404 })
|
||||
);
|
||||
|
||||
await enterpriseSsoVerificationRecord.verify(ctx, tenantContext, connectorData);
|
||||
|
||||
await ctx.experienceInteraction.save();
|
||||
|
||||
ctx.body = {
|
||||
verificationId,
|
||||
};
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -88,7 +88,7 @@ type SsoAuthenticationResult = {
|
|||
*
|
||||
* @returns The SSO authentication result
|
||||
*/
|
||||
const verifySsoIdentity = async (
|
||||
export const verifySsoIdentity = async (
|
||||
ctx: WithLogContext,
|
||||
{ provider, id: tenantId }: TenantContext,
|
||||
connectorData: SupportedSsoConnector,
|
||||
|
|
|
@ -102,4 +102,34 @@ export class ExperienceClient extends MockClient {
|
|||
})
|
||||
.json<{ verificationId: string }>();
|
||||
}
|
||||
|
||||
public async getEnterpriseSsoAuthorizationUri(
|
||||
connectorId: string,
|
||||
payload: {
|
||||
redirectUri: string;
|
||||
state: string;
|
||||
}
|
||||
) {
|
||||
return api
|
||||
.post(`${experienceRoutes.verification}/sso/${connectorId}/authorization-uri`, {
|
||||
headers: { cookie: this.interactionCookie },
|
||||
json: payload,
|
||||
})
|
||||
.json<{ authorizationUri: string; verificationId: string }>();
|
||||
}
|
||||
|
||||
public async verifyEnterpriseSsoAuthorization(
|
||||
connectorId: string,
|
||||
payload: {
|
||||
verificationId: string;
|
||||
connectorData: Record<string, unknown>;
|
||||
}
|
||||
) {
|
||||
return api
|
||||
.post(`${experienceRoutes.verification}/sso/${connectorId}/verify`, {
|
||||
headers: { cookie: this.interactionCookie },
|
||||
json: payload,
|
||||
})
|
||||
.json<{ verificationId: string }>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import { type ExperienceClient } from '#src/client/experience/index.js';
|
||||
|
||||
export const successFullyCreateEnterpriseSsoVerification = async (
|
||||
client: ExperienceClient,
|
||||
connectorId: string,
|
||||
payload: {
|
||||
redirectUri: string;
|
||||
state: string;
|
||||
}
|
||||
) => {
|
||||
const { authorizationUri, verificationId } = await client.getEnterpriseSsoAuthorizationUri(
|
||||
connectorId,
|
||||
payload
|
||||
);
|
||||
|
||||
expect(verificationId).toBeTruthy();
|
||||
expect(authorizationUri).toBeTruthy();
|
||||
|
||||
return {
|
||||
verificationId,
|
||||
authorizationUri,
|
||||
};
|
||||
};
|
||||
|
||||
export const successFullyVerifyEnterpriseSsoAuthorization = async (
|
||||
client: ExperienceClient,
|
||||
connectorId: string,
|
||||
payload: {
|
||||
verificationId: string;
|
||||
connectorData: Record<string, unknown>;
|
||||
}
|
||||
) => {
|
||||
const { verificationId: verifiedVerificationId } = await client.verifyEnterpriseSsoAuthorization(
|
||||
connectorId,
|
||||
payload
|
||||
);
|
||||
|
||||
expect(verifiedVerificationId).toBeTruthy();
|
||||
|
||||
return verifiedVerificationId;
|
||||
};
|
|
@ -0,0 +1,201 @@
|
|||
import { ConnectorType } from '@logto/connector-kit';
|
||||
|
||||
import { mockSocialConnectorId } from '#src/__mocks__/connectors-mock.js';
|
||||
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
||||
import { SsoConnectorApi } from '#src/api/sso-connector.js';
|
||||
import { initExperienceClient } from '#src/helpers/client.js';
|
||||
import { clearConnectorsByTypes, setSocialConnector } from '#src/helpers/connector.js';
|
||||
import {
|
||||
successFullyCreateEnterpriseSsoVerification,
|
||||
successFullyVerifyEnterpriseSsoAuthorization,
|
||||
} from '#src/helpers/experience/enterprise-sso-verification.js';
|
||||
import { successFullyCreateSocialVerification } from '#src/helpers/experience/social-verification.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import { devFeatureTest, generateUserId, randomString } from '#src/utils.js';
|
||||
|
||||
devFeatureTest.describe('enterprise sso verification', () => {
|
||||
const state = 'fake_state';
|
||||
const redirectUri = 'http://localhost:3000/redirect';
|
||||
const authorizationCode = 'fake_code';
|
||||
const domain = `foo${randomString()}.com`;
|
||||
const ssoConnectorApi = new SsoConnectorApi();
|
||||
const socialConnectorIdMap = new Map<string, string>();
|
||||
|
||||
beforeAll(async () => {
|
||||
await clearConnectorsByTypes([ConnectorType.Social]);
|
||||
await ssoConnectorApi.createMockOidcConnector([domain]);
|
||||
const { id: socialConnectorId } = await setSocialConnector();
|
||||
|
||||
socialConnectorIdMap.set(mockSocialConnectorId, socialConnectorId);
|
||||
|
||||
// Make sure single sign on is enabled
|
||||
await updateSignInExperience({
|
||||
singleSignOnEnabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await ssoConnectorApi.cleanUp();
|
||||
await clearConnectorsByTypes([ConnectorType.Social]);
|
||||
});
|
||||
|
||||
describe('getSsoAuthorizationUri', () => {
|
||||
it('should throw if the state or redirectUri is empty', async () => {
|
||||
const client = await initExperienceClient();
|
||||
const connectorId = ssoConnectorApi.firstConnectorId!;
|
||||
|
||||
await expectRejects(
|
||||
client.getSocialAuthorizationUri(connectorId, {
|
||||
redirectUri,
|
||||
state: '',
|
||||
}),
|
||||
{
|
||||
code: 'session.insufficient_info',
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
|
||||
await expectRejects(
|
||||
client.getSocialAuthorizationUri(connectorId, {
|
||||
redirectUri: '',
|
||||
state,
|
||||
}),
|
||||
{
|
||||
code: 'session.insufficient_info',
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if the connector is not found', async () => {
|
||||
const client = await initExperienceClient();
|
||||
|
||||
return expectRejects(
|
||||
client.getEnterpriseSsoAuthorizationUri('invalid_connector_id', {
|
||||
redirectUri,
|
||||
state,
|
||||
}),
|
||||
{
|
||||
code: 'entity.not_exists_with_id',
|
||||
status: 404,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the authorization uri', async () => {
|
||||
const client = await initExperienceClient();
|
||||
const connectorId = ssoConnectorApi.firstConnectorId!;
|
||||
|
||||
const { authorizationUri, verificationId } = await client.getEnterpriseSsoAuthorizationUri(
|
||||
connectorId,
|
||||
{
|
||||
redirectUri,
|
||||
state,
|
||||
}
|
||||
);
|
||||
|
||||
expect(verificationId).toBeTruthy();
|
||||
expect(authorizationUri).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyEnterpriseSsoAuthorization', () => {
|
||||
it('should throw if the verification record is not found', async () => {
|
||||
const client = await initExperienceClient();
|
||||
const connectorId = ssoConnectorApi.firstConnectorId!;
|
||||
|
||||
await successFullyCreateEnterpriseSsoVerification(client, connectorId, {
|
||||
redirectUri,
|
||||
state,
|
||||
});
|
||||
|
||||
await expectRejects(
|
||||
client.verifyEnterpriseSsoAuthorization(connectorId, {
|
||||
verificationId: 'invalid_verification_id',
|
||||
connectorData: {
|
||||
authorizationCode,
|
||||
},
|
||||
}),
|
||||
{
|
||||
code: 'session.verification_session_not_found',
|
||||
status: 404,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if the verification type is not enterprise sso', async () => {
|
||||
const client = await initExperienceClient();
|
||||
const connectorId = socialConnectorIdMap.get(mockSocialConnectorId)!;
|
||||
|
||||
const { verificationId } = await successFullyCreateSocialVerification(client, connectorId, {
|
||||
redirectUri,
|
||||
state,
|
||||
});
|
||||
|
||||
await expectRejects(
|
||||
client.verifyEnterpriseSsoAuthorization(connectorId, {
|
||||
verificationId,
|
||||
connectorData: {
|
||||
authorizationCode,
|
||||
},
|
||||
}),
|
||||
{
|
||||
code: 'session.verification_session_not_found',
|
||||
status: 404,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if the connectorId does not match', async () => {
|
||||
const client = await initExperienceClient();
|
||||
const connectorId = ssoConnectorApi.firstConnectorId!;
|
||||
|
||||
const { verificationId } = await successFullyCreateEnterpriseSsoVerification(
|
||||
client,
|
||||
connectorId,
|
||||
{
|
||||
redirectUri,
|
||||
state,
|
||||
}
|
||||
);
|
||||
|
||||
await expectRejects(
|
||||
client.verifyEnterpriseSsoAuthorization('invalid_connector_id', {
|
||||
verificationId,
|
||||
connectorData: {
|
||||
authorizationCode,
|
||||
},
|
||||
}),
|
||||
{
|
||||
code: 'session.verification_session_not_found',
|
||||
status: 404,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should successfully verify the authorization', async () => {
|
||||
const client = await initExperienceClient();
|
||||
const connectorId = ssoConnectorApi.firstConnectorId!;
|
||||
|
||||
const { verificationId } = await successFullyCreateEnterpriseSsoVerification(
|
||||
client,
|
||||
connectorId,
|
||||
{
|
||||
redirectUri,
|
||||
state,
|
||||
}
|
||||
);
|
||||
|
||||
// Pass the sub value as a callback connectorData to mock the SsoConnector.getUserInfo return value.
|
||||
const fakeSsoIdentitySub = generateUserId();
|
||||
|
||||
await successFullyVerifyEnterpriseSsoAuthorization(client, connectorId, {
|
||||
verificationId,
|
||||
connectorData: {
|
||||
authorizationCode,
|
||||
sub: fakeSsoIdentitySub,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue