From 91a5c64e04c05d5e524bc730676e2702cf86dbf7 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Thu, 12 Jan 2023 11:49:40 +0800 Subject: [PATCH] feat(core): implement oidc getResourceServerInfo (#2875) --- packages/core/src/libraries/user.test.ts | 19 ++++++++++++++++++- packages/core/src/libraries/user.ts | 21 +++++++++++++++++++-- packages/core/src/oidc/init.ts | 10 ++++++++-- packages/core/src/queries/roles-scopes.ts | 11 ++++++++++- packages/core/src/queries/scope.ts | 14 ++++++++++++++ 5 files changed, 69 insertions(+), 6 deletions(-) diff --git a/packages/core/src/libraries/user.test.ts b/packages/core/src/libraries/user.test.ts index 943d995a2..a019911ae 100644 --- a/packages/core/src/libraries/user.test.ts +++ b/packages/core/src/libraries/user.test.ts @@ -1,6 +1,8 @@ import { UsersPasswordEncryptionMethod } from '@logto/schemas'; import { createMockPool } from 'slonik'; +import { mockResource, mockScope } from '#src/__mocks__/index.js'; +import { mockUser } from '#src/__mocks__/user.js'; import { MockQueries } from '#src/test-utils/tenant.js'; const { jest } = import.meta; @@ -12,7 +14,12 @@ const pool = createMockPool({ const { encryptUserPassword, createUserLibrary } = await import('./user.js'); const hasUserWithId = jest.fn(); -const queries = new MockQueries({ users: { hasUserWithId } }); +const queries = new MockQueries({ + users: { hasUserWithId }, + scopes: { findScopesByIdsAndResourceId: async () => [mockScope] }, + usersRoles: { findUsersRolesByUserId: async () => [] }, + rolesScopes: { findRolesScopesByRoleIds: async () => [] }, +}); describe('generateUserId()', () => { const { generateUserId } = createUserLibrary(queries); @@ -63,3 +70,13 @@ describe('encryptUserPassword()', () => { expect(passwordEncrypted).toContain('argon2'); }); }); + +describe('findUserScopesForResourceId()', () => { + const { findUserScopesForResourceId } = createUserLibrary(queries); + + it('returns scopes that the user has access', async () => { + await expect(findUserScopesForResourceId(mockUser.id, mockResource.id)).resolves.toEqual([ + mockScope, + ]); + }); +}); diff --git a/packages/core/src/libraries/user.ts b/packages/core/src/libraries/user.ts index 5ff96453b..6fe1f5503 100644 --- a/packages/core/src/libraries/user.ts +++ b/packages/core/src/libraries/user.ts @@ -1,5 +1,5 @@ import { buildIdGenerator } from '@logto/core-kit'; -import type { User, CreateUser } from '@logto/schemas'; +import type { User, CreateUser, Scope } from '@logto/schemas'; import { Users, UsersPasswordEncryptionMethod } from '@logto/schemas'; import type { OmitAutoSetFields } from '@logto/shared'; import type { Nullable } from '@silverhand/essentials'; @@ -51,7 +51,9 @@ export const createUserLibrary = (queries: Queries) => { pool, roles: { findRolesByRoleNames, insertRoles, findRoleByRoleName }, users: { hasUser, hasUserWithEmail, hasUserWithId, hasUserWithPhone, findUsersByIds }, - usersRoles: { insertUsersRoles, findUsersRolesByRoleId }, + usersRoles: { insertUsersRoles, findUsersRolesByRoleId, findUsersRolesByUserId }, + rolesScopes: { findRolesScopesByRoleIds }, + scopes: { findScopesByIdsAndResourceId }, } = queries; const generateUserId = async (retries = 500) => @@ -156,10 +158,25 @@ export const createUserLibrary = (queries: Queries) => { return findUsersByIds(usersRoles.map(({ userId }) => userId)); }; + const findUserScopesForResourceId = async ( + userId: string, + resourceId: string + ): Promise => { + const usersRoles = await findUsersRolesByUserId(userId); + const rolesScopes = await findRolesScopesByRoleIds(usersRoles.map(({ roleId }) => roleId)); + const scopes = await findScopesByIdsAndResourceId( + rolesScopes.map(({ scopeId }) => scopeId), + resourceId + ); + + return scopes; + }; + return { generateUserId, insertUser, checkIdentifierCollision, findUsersByRoleName, + findUserScopesForResourceId, }; }; diff --git a/packages/core/src/oidc/init.ts b/packages/core/src/oidc/init.ts index 4ef0fca46..4ed523ac9 100644 --- a/packages/core/src/oidc/init.ts +++ b/packages/core/src/oidc/init.ts @@ -10,6 +10,7 @@ import snakecaseKeys from 'snakecase-keys'; import envSet from '#src/env-set/index.js'; import { addOidcEventListeners } from '#src/event-listeners/index.js'; +import { createUserLibrary } from '#src/libraries/user.js'; import koaAuditLog from '#src/middleware/koa-audit-log.js'; import postgresAdapter from '#src/oidc/adapter.js'; import { isOriginAllowed, validateCustomClientMetadata } from '#src/oidc/utils.js'; @@ -36,6 +37,7 @@ export default function initOidc(queries: Queries): Provider { defaultIdTokenTtl, defaultRefreshTokenTtl, } = envSet.oidc; + const { findUserScopesForResourceId } = createUserLibrary(queries); const logoutSource = readFileSync('static/html/logout.html', 'utf8'); const cookieConfig = Object.freeze({ @@ -83,18 +85,22 @@ export default function initOidc(queries: Queries): Provider { defaultResource: () => '', // Disable the auto use of authorization_code granted resource feature useGrantedResource: () => false, - getResourceServerInfo: async (_, indicator) => { + getResourceServerInfo: async (ctx, indicator) => { const resourceServer = await findResourceByIndicator(indicator); if (!resourceServer) { throw new errors.InvalidTarget(); } + const scopes = ctx.oidc.account + ? await findUserScopesForResourceId(ctx.oidc.account.accountId, resourceServer.id) + : []; + const { accessTokenTtl: accessTokenTTL } = resourceServer; return { accessTokenFormat: 'jwt', - scope: '', + scope: scopes.map(({ name }) => name).join(' '), accessTokenTTL, jwt: { sign: { alg: jwkSigningAlg }, diff --git a/packages/core/src/queries/roles-scopes.ts b/packages/core/src/queries/roles-scopes.ts index f4391a263..23d75290f 100644 --- a/packages/core/src/queries/roles-scopes.ts +++ b/packages/core/src/queries/roles-scopes.ts @@ -25,6 +25,15 @@ export const createRolesScopesQueries = (pool: CommonQueryMethods) => { where ${fields.roleId}=${roleId} `); + const findRolesScopesByRoleIds = async (roleIds: string[]) => + roleIds.length > 0 + ? pool.any(sql` + select ${sql.join(Object.values(fields), sql`,`)} + from ${table} + where ${fields.roleId} in (${sql.join(roleIds, sql`, `)}) + `) + : []; + const deleteRolesScope = async (roleId: string, scopeId: string) => { const { rowCount } = await pool.query(sql` delete from ${table} @@ -36,5 +45,5 @@ export const createRolesScopesQueries = (pool: CommonQueryMethods) => { } }; - return { insertRolesScopes, findRolesScopesByRoleId, deleteRolesScope }; + return { insertRolesScopes, findRolesScopesByRoleId, findRolesScopesByRoleIds, deleteRolesScope }; }; diff --git a/packages/core/src/queries/scope.ts b/packages/core/src/queries/scope.ts index 96c662845..b1e76bb60 100644 --- a/packages/core/src/queries/scope.ts +++ b/packages/core/src/queries/scope.ts @@ -71,6 +71,19 @@ export const createScopeQueries = (pool: CommonQueryMethods) => { `) : []; + const findScopesByIdsAndResourceId = async ( + scopeIds: string[], + resourceId: string + ): Promise => + scopeIds.length > 0 + ? pool.any(sql` + select ${sql.join(Object.values(fields), sql`, `)} + from ${table} + where ${fields.id} in (${sql.join(scopeIds, sql`, `)}) + and ${fields.resourceId} = ${resourceId} + `) + : []; + const findScopesByIds = async (scopeIds: string[]) => scopeIds.length > 0 ? pool.any(sql` @@ -109,6 +122,7 @@ export const createScopeQueries = (pool: CommonQueryMethods) => { findScopesByResourceId, findScopesByResourceIds, findScopesByIds, + findScopesByIdsAndResourceId, insertScope, findScopeById, updateScope,