diff --git a/packages/core/src/libraries/user.test.ts b/packages/core/src/libraries/user.test.ts index a019911ae..cc6d42f75 100644 --- a/packages/core/src/libraries/user.test.ts +++ b/packages/core/src/libraries/user.test.ts @@ -1,7 +1,7 @@ import { UsersPasswordEncryptionMethod } from '@logto/schemas'; import { createMockPool } from 'slonik'; -import { mockResource, mockScope } from '#src/__mocks__/index.js'; +import { mockResource, mockRole, mockScope } from '#src/__mocks__/index.js'; import { mockUser } from '#src/__mocks__/user.js'; import { MockQueries } from '#src/test-utils/tenant.js'; @@ -16,6 +16,7 @@ const { encryptUserPassword, createUserLibrary } = await import('./user.js'); const hasUserWithId = jest.fn(); const queries = new MockQueries({ users: { hasUserWithId }, + roles: { findRolesByRoleIds: async () => [mockRole] }, scopes: { findScopesByIdsAndResourceId: async () => [mockScope] }, usersRoles: { findUsersRolesByUserId: async () => [] }, rolesScopes: { findRolesScopesByRoleIds: async () => [] }, @@ -80,3 +81,11 @@ describe('findUserScopesForResourceId()', () => { ]); }); }); + +describe('findUserRoles()', () => { + const { findUserRoles } = createUserLibrary(queries); + + it('returns user roles', async () => { + await expect(findUserRoles(mockUser.id)).resolves.toEqual([mockRole]); + }); +}); diff --git a/packages/core/src/libraries/user.ts b/packages/core/src/libraries/user.ts index 219233494..9deee042e 100644 --- a/packages/core/src/libraries/user.ts +++ b/packages/core/src/libraries/user.ts @@ -195,6 +195,13 @@ export const createUserLibrary = (queries: Queries) => { return scopes; }; + const findUserRoles = async (userId: string) => { + const usersRoles = await findUsersRolesByUserId(userId); + const roles = await findRolesByRoleIds(usersRoles.map(({ roleId }) => roleId)); + + return roles; + }; + return { findUserByIdWithRoles, generateUserId, @@ -202,5 +209,6 @@ export const createUserLibrary = (queries: Queries) => { checkIdentifierCollision, findUsersByRoleName, findUserScopesForResourceId, + findUserRoles, }; }; diff --git a/packages/core/src/middleware/koa-auth.ts b/packages/core/src/middleware/koa-auth.ts index bcbcea0a0..6feacba80 100644 --- a/packages/core/src/middleware/koa-auth.ts +++ b/packages/core/src/middleware/koa-auth.ts @@ -43,7 +43,6 @@ type TokenInfo = { sub: string; clientId: unknown; scopes: string[]; - roleNames?: string[]; }; export const verifyBearerTokenFromRequest = async ( diff --git a/packages/core/src/routes/authn.test.ts b/packages/core/src/routes/authn.test.ts index c615132b0..8811545b8 100644 --- a/packages/core/src/routes/authn.test.ts +++ b/packages/core/src/routes/authn.test.ts @@ -1,6 +1,9 @@ import { pickDefault, createMockUtils } from '@logto/shared/esm'; +import { mockRole } from '#src/__mocks__/index.js'; import RequestError from '#src/errors/RequestError/index.js'; +import Libraries from '#src/tenants/Libraries.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; const { jest } = import.meta; const { mockEsmWithActual } = createMockUtils(jest); @@ -12,14 +15,20 @@ const { verifyBearerTokenFromRequest } = await mockEsmWithActual( }) ); +const usersLibraries = { + findUserRoles: jest.fn(async () => [mockRole]), +} satisfies Partial; + +const tenantContext = new MockTenant(undefined, {}, { users: usersLibraries }); const { createRequester } = await import('#src/utils/test-utils.js'); const request = createRequester({ anonymousRoutes: await pickDefault(import('#src/routes/authn.js')), + tenantContext, }); describe('authn route for Hasura', () => { const mockUserId = 'foo'; - const mockExpectedRole = 'some_role'; + const mockExpectedRole = mockRole.name; const mockUnauthorizedRole = 'V'; const keys = Object.freeze({ expectedRole: 'Expected-Role', @@ -32,7 +41,6 @@ describe('authn route for Hasura', () => { verifyBearerTokenFromRequest.mockResolvedValue({ clientId: 'ok', sub: mockUserId, - roleNames: [mockExpectedRole], }); }); diff --git a/packages/core/src/routes/authn.ts b/packages/core/src/routes/authn.ts index f3396afab..147d78a0e 100644 --- a/packages/core/src/routes/authn.ts +++ b/packages/core/src/routes/authn.ts @@ -13,8 +13,10 @@ import type { AnonymousRouter, RouterInitArgs } from './types.js'; * For now, we only implement the API for Hasura authentication. */ export default function authnRoutes( - ...[router, { envSet }]: RouterInitArgs + ...[router, { envSet, libraries }]: RouterInitArgs ) { + const { findUserRoles } = libraries.users; + router.get( '/authn/hasura', koaGuard({ @@ -36,9 +38,11 @@ export default function authnRoutes( } }; - const { sub, roleNames } = await verifyToken(resource); + const { sub } = await verifyToken(resource); + const roles = sub ? await findUserRoles(sub) : []; + const roleNames = new Set(roles.map(({ name }) => name)); - if (unauthorizedRole && (!expectedRole || !roleNames?.includes(expectedRole))) { + if (unauthorizedRole && (!expectedRole || !roleNames.has(expectedRole))) { ctx.body = { 'X-Hasura-User-Id': sub ?? @@ -54,7 +58,7 @@ export default function authnRoutes( if (expectedRole) { assertThat( - roleNames?.includes(expectedRole), + roleNames.has(expectedRole), new RequestError({ code: 'auth.expected_role_not_found', status: 401 }) ); }