-
-
Notifications
You must be signed in to change notification settings - Fork 473
Commit
implement the enterprise sso verification flow
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}); | ||
} | ||
Check warning on line 55 in packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts Codecov / codecov/patchpackages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts#L50-L55
|
||
|
||
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; | ||
} | ||
Check warning on line 77 in packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts Codecov / codecov/patchpackages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts#L66-L77
|
||
|
||
/** Returns true if the enterprise SSO identity has been verified */ | ||
get isVerified() { | ||
return Boolean(this.enterpriseSsoUserInfo && this.issuer); | ||
} | ||
Check warning on line 82 in packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts Codecov / codecov/patchpackages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts#L81-L82
|
||
|
||
async getConnectorData(connectorId: string) { | ||
this.connectorDataCache ||= await this.libraries.ssoConnectors.getSsoConnectorById(connectorId); | ||
|
||
return this.connectorDataCache; | ||
} | ||
Check warning on line 88 in packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts Codecov / codecov/patchpackages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts#L85-L88
|
||
|
||
/** | ||
* 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); | ||
} | ||
Check warning on line 109 in packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts Codecov / codecov/patchpackages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts#L103-L109
|
||
|
||
/** | ||
* 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; | ||
} | ||
Check warning on line 130 in packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts Codecov / codecov/patchpackages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts#L120-L130
|
||
|
||
async identifyUser(): Promise<User> { | ||
assertThat( | ||
this.isVerified, | ||
new RequestError({ code: 'session.verification_failed', status: 422 }) | ||
); | ||
|
||
// TODO: sync userInfo and link sso identity | ||
Check warning on line 138 in packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts GitHub Actions / ESLint Report Analysispackages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts#L138
|
||
|
||
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 }); | ||
} | ||
Check warning on line 153 in packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts Codecov / codecov/patchpackages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts#L133-L153
|
||
|
||
toJson(): EnterpriseSsoVerificationRecordData { | ||
const { id, connectorId, type, enterpriseSsoUserInfo, issuer } = this; | ||
|
||
return { | ||
id, | ||
connectorId, | ||
type, | ||
enterpriseSsoUserInfo, | ||
issuer, | ||
}; | ||
} | ||
Check warning on line 165 in packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts Codecov / codecov/patchpackages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts#L156-L165
|
||
|
||
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, | ||
}; | ||
} | ||
} | ||
Check warning on line 192 in packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts Codecov / codecov/patchpackages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts#L168-L192
|
||
|
||
/** | ||
* 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; | ||
} | ||
Check warning on line 207 in packages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts Codecov / codecov/patchpackages/core/src/routes/experience/classes/verifications/enterprise-sso-verification.ts#L198-L207
|
||
} |