Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core,toolkit): add new sso_identities claim #5955

Merged
merged 4 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/quick-hairs-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@logto/core-kit": minor
"@logto/core": minor
---

define new `sso_identities` user claim to the userinfo endpoint response

- Define a new `sso_identities` user claim that will be used to store the user's SSO identities. The claim will be an array of objects with the following properties:
- `details`: detailed user info returned from the SSO provider.
- `issuer`: the issuer of the SSO provider.
- `identityId`: the user id of the user in the SSO provider.
- The new claims will share the same scope as the social `identities` claim.
- When the user `identities` scope is requested, the new `sso_identities` claim will be returned along with the `identities` claim in the userinfo endpoint response.
20 changes: 9 additions & 11 deletions packages/core/src/libraries/jwt-customizer.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { runScriptFunctionInLocalVm, buildErrorResponse } from '@logto/core-kit/custom-jwt';
import { buildErrorResponse, runScriptFunctionInLocalVm } from '@logto/core-kit/custom-jwt';
import {
userInfoSelectFields,
LogtoJwtTokenKeyType,
jwtCustomizerUserContextGuard,
type LogtoJwtTokenKey,
userInfoSelectFields,
type CustomJwtFetcher,
type JwtCustomizerType,
type JwtCustomizerUserContext,
type CustomJwtFetcher,
LogtoJwtTokenKeyType,
type LogtoJwtTokenKey,
} from '@logto/schemas';
import { type ConsoleLog } from '@logto/shared';
import { deduplicate, pick, pickState, assert } from '@silverhand/essentials';
import { assert, deduplicate, pick, pickState } from '@silverhand/essentials';
import deepmerge from 'deepmerge';
import { z, ZodError } from 'zod';
import { ZodError, z } from 'zod';

import { EnvSet } from '#src/env-set/index.js';
import RequestError from '#src/errors/RequestError/index.js';
Expand All @@ -20,10 +20,10 @@
import { type UserLibrary } from '#src/libraries/user.js';
import type Queries from '#src/tenants/Queries.js';
import {
LocalVmError,
getJwtCustomizerScripts,
type CustomJwtDeployRequestBody,
} from '#src/utils/custom-jwt/index.js';
import { LocalVmError } from '#src/utils/custom-jwt/index.js';

import { type CloudConnectionLibrary } from './cloud-connection.js';

Expand Down Expand Up @@ -59,7 +59,7 @@
}
}

constructor(

Check warning on line 62 in packages/core/src/libraries/jwt-customizer.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/core/src/libraries/jwt-customizer.ts#L62

[max-params] Constructor has too many parameters (5). Maximum allowed is 4.
private readonly queries: Queries,
private readonly logtoConfigs: LogtoConfigLibrary,
private readonly cloudConnection: CloudConnectionLibrary,
Expand All @@ -75,9 +75,7 @@
*/
async getUserContext(userId: string): Promise<JwtCustomizerUserContext> {
const user = await this.queries.users.findUserById(userId);
const fullSsoIdentities = await this.queries.userSsoIdentities.findUserSsoIdentitiesByUserId(
userId
);
const fullSsoIdentities = await this.userLibrary.findUserSsoIdentities(userId);

Check warning on line 78 in packages/core/src/libraries/jwt-customizer.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/libraries/jwt-customizer.ts#L78

Added line #L78 was not covered by tests
const roles = await this.userLibrary.findUserRoles(userId);
const rolesScopes = await this.queries.rolesScopes.findRolesScopesByRoleIds(
roles.map(({ id }) => id)
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/libraries/user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { User, CreateUser, Scope, BindMfa, MfaVerification } from '@logto/schemas';
import type { BindMfa, CreateUser, MfaVerification, Scope, User } from '@logto/schemas';
import { MfaFactor, RoleType, Users, UsersPasswordEncryptionMethod } from '@logto/schemas';
import { generateStandardShortId, generateStandardId } from '@logto/shared';
import { generateStandardId, generateStandardShortId } from '@logto/shared';
import { deduplicateByKey, type Nullable } from '@silverhand/essentials';
import { argon2Verify, bcryptVerify, md5, sha1, sha256 } from 'hash-wasm';
import pRetry from 'p-retry';
Expand Down Expand Up @@ -89,6 +89,7 @@
scopes: { findScopesByIdsAndResourceIndicator },
organizations,
oidcModelInstances: { revokeInstanceByUserId },
userSsoIdentities,
} = queries;

const generateUserId = async (retries = 500) =>
Expand Down Expand Up @@ -208,7 +209,7 @@
};

const addUserMfaVerification = async (userId: string, payload: BindMfa) => {
// TODO @sijie use jsonb array append

Check warning on line 212 in packages/core/src/libraries/user.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/core/src/libraries/user.ts#L212

[no-warning-comments] Unexpected 'todo' comment: 'TODO @sijie use jsonb array append'.
const { mfaVerifications } = await findUserById(userId);
await updateUserById(userId, {
mfaVerifications: [...mfaVerifications, converBindMfaToMfaVerification(payload)],
Expand Down Expand Up @@ -282,6 +283,12 @@
]);
};

/**
* Expose the findUserSsoIdentitiesByUserId query method for the user library.
*/
const findUserSsoIdentities = async (userId: string) =>
userSsoIdentities.findUserSsoIdentitiesByUserId(userId);

return {
generateUserId,
insertUser,
Expand All @@ -292,5 +299,6 @@
addUserMfaVerification,
verifyUserPassword,
signOutUser,
findUserSsoIdentities,
};
};
2 changes: 1 addition & 1 deletion packages/core/src/oidc/scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('OIDC getUserClaims()', () => {
it('should return proper Userinfo claims', () => {
expect(
getAcceptedUserClaims(use.userinfo, 'openid profile custom_data identities', {}, [])
).toEqual([...profileExpectation, 'custom_data', 'identities']);
).toEqual([...profileExpectation, 'custom_data', 'identities', 'sso_identities']);
});

// Ignore `_claims` since [Claims Parameter](https://github.com/panva/node-oidc-provider/tree/main/docs#featuresclaimsparameter) is not enabled
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/oidc/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import type { UserClaim } from '@logto/core-kit';
import { idTokenClaims, userinfoClaims, UserScope } from '@logto/core-kit';
import { type UserProfile, type User, userProfileKeys } from '@logto/schemas';
import { pick, type Nullable, cond } from '@silverhand/essentials';
import { userProfileKeys, type User, type UserProfile } from '@logto/schemas';
import { cond, pick, type Nullable } from '@silverhand/essentials';
import type { ClaimsParameterMember } from 'oidc-provider';
import { snakeCase } from 'snake-case';
import { type SnakeCaseKeys } from 'snakecase-keys';
Expand All @@ -24,6 +24,7 @@
| 'organization_data'
| 'organization_roles'
| UserProfileClaimSnakeCase
| 'sso_identities'
>,
keyof User
>
Expand Down Expand Up @@ -113,6 +114,13 @@
organizations.map((element) => pick(element, 'id', 'name', 'description')),
];
}
case 'sso_identities': {
const ssoIdentities = await userLibrary.findUserSsoIdentities(user.id);
return [
claim,
ssoIdentities.map(({ issuer, identityId, detail }) => ({ issuer, identityId, detail })),
];
}

Check warning on line 123 in packages/core/src/oidc/scope.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/oidc/scope.ts#L117-L123

Added lines #L117 - L123 were not covered by tests
default: {
if (isUserProfileClaim(claim)) {
// Unlike other database fields (e.g. `name`), the claims stored in the `profile` field
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/routes/admin-user/basics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
hasUserWithEmail,
hasUserWithPhone,
},
userSsoIdentities,
} = queries;
const {
users: {
Expand All @@ -39,6 +38,7 @@
insertUser,
verifyUserPassword,
signOutUser,
findUserSsoIdentities,
},
} = libraries;

Expand All @@ -63,7 +63,7 @@
...conditional(
includeSsoIdentities &&
yes(includeSsoIdentities) && {
ssoIdentities: await userSsoIdentities.findUserSsoIdentitiesByUserId(userId),
ssoIdentities: await findUserSsoIdentities(userId),

Check warning on line 66 in packages/core/src/routes/admin-user/basics.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/admin-user/basics.ts#L66

Added line #L66 was not covered by tests
}
),
};
Expand Down
5 changes: 3 additions & 2 deletions packages/toolkit/core-kit/src/openid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type UserClaim =
| 'organization_roles'
| 'custom_data'
| 'identities'
| 'sso_identities'
| 'created_at';

/**
Expand Down Expand Up @@ -80,7 +81,7 @@ export enum UserScope {
*/
CustomData = 'custom_data',
/**
* Scope for user's social identity details.
* Scope for user's social and SSO identity details.
*
* See {@link idTokenClaims} for mapped claims in ID Token and {@link userinfoClaims} for additional claims in Userinfo Endpoint.
*/
Expand Down Expand Up @@ -153,7 +154,7 @@ export const userinfoClaims: Readonly<Record<UserScope, UserClaim[]>> = Object.f
[UserScope.Organizations]: ['organization_data'],
[UserScope.OrganizationRoles]: [],
[UserScope.CustomData]: ['custom_data'],
[UserScope.Identities]: ['identities'],
[UserScope.Identities]: ['identities', 'sso_identities'],
});

export const userClaims: Readonly<Record<UserScope, UserClaim[]>> = Object.freeze(
Expand Down
Loading