mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
feat(core): implement oidc getResourceServerInfo (#2875)
This commit is contained in:
parent
10ea2307ad
commit
91a5c64e04
5 changed files with 69 additions and 6 deletions
|
@ -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,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<readonly Scope[]> => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -25,6 +25,15 @@ export const createRolesScopesQueries = (pool: CommonQueryMethods) => {
|
|||
where ${fields.roleId}=${roleId}
|
||||
`);
|
||||
|
||||
const findRolesScopesByRoleIds = async (roleIds: string[]) =>
|
||||
roleIds.length > 0
|
||||
? pool.any<RolesScope>(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 };
|
||||
};
|
||||
|
|
|
@ -71,6 +71,19 @@ export const createScopeQueries = (pool: CommonQueryMethods) => {
|
|||
`)
|
||||
: [];
|
||||
|
||||
const findScopesByIdsAndResourceId = async (
|
||||
scopeIds: string[],
|
||||
resourceId: string
|
||||
): Promise<readonly Scope[]> =>
|
||||
scopeIds.length > 0
|
||||
? pool.any<Scope>(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<Scope>(sql`
|
||||
|
@ -109,6 +122,7 @@ export const createScopeQueries = (pool: CommonQueryMethods) => {
|
|||
findScopesByResourceId,
|
||||
findScopesByResourceIds,
|
||||
findScopesByIds,
|
||||
findScopesByIdsAndResourceId,
|
||||
insertScope,
|
||||
findScopeById,
|
||||
updateScope,
|
||||
|
|
Loading…
Add table
Reference in a new issue