2022-02-23 09:42:29 +08:00
|
|
|
/* istanbul ignore file */
|
|
|
|
|
2022-04-08 18:16:20 +08:00
|
|
|
import { CustomClientMetadataKey } from '@logto/schemas';
|
2022-04-24 13:55:47 +08:00
|
|
|
import { exportJWK } from 'jose';
|
2021-06-27 20:44:05 +08:00
|
|
|
import Koa from 'koa';
|
|
|
|
import mount from 'koa-mount';
|
2021-12-02 14:08:15 +08:00
|
|
|
import { Provider, errors } from 'oidc-provider';
|
2022-06-01 15:00:10 +08:00
|
|
|
import snakecaseKeys from 'snakecase-keys';
|
2021-06-27 20:44:05 +08:00
|
|
|
|
2022-04-20 14:14:37 +08:00
|
|
|
import envSet from '@/env-set';
|
2021-08-30 11:30:54 +08:00
|
|
|
import postgresAdapter from '@/oidc/adapter';
|
2022-04-08 18:16:20 +08:00
|
|
|
import { isOriginAllowed, validateCustomClientMetadata } from '@/oidc/utils';
|
2022-02-15 16:13:41 +08:00
|
|
|
import { findResourceByIndicator } from '@/queries/resource';
|
2021-07-02 22:55:14 +08:00
|
|
|
import { findUserById } from '@/queries/user';
|
2021-08-11 22:37:21 +08:00
|
|
|
import { routes } from '@/routes/consts';
|
2022-05-20 13:54:05 +08:00
|
|
|
import { addOidcEventListeners } from '@/utils/oidc-provider-event-listener';
|
2021-08-30 11:30:54 +08:00
|
|
|
|
2021-07-09 23:25:24 +08:00
|
|
|
export default async function initOidc(app: Koa): Promise<Provider> {
|
2022-05-20 00:08:33 +08:00
|
|
|
const { issuer, cookieKeys, privateKey, defaultIdTokenTtl, defaultRefreshTokenTtl } =
|
|
|
|
envSet.values.oidc;
|
2022-04-20 14:14:37 +08:00
|
|
|
|
2022-04-24 13:55:47 +08:00
|
|
|
const keys = [await exportJWK(privateKey)];
|
2021-07-04 15:01:02 +08:00
|
|
|
const cookieConfig = Object.freeze({
|
|
|
|
sameSite: 'lax',
|
|
|
|
path: '/',
|
|
|
|
signed: true,
|
|
|
|
} as const);
|
2021-08-15 23:39:03 +08:00
|
|
|
const oidc = new Provider(issuer, {
|
2021-06-27 20:44:05 +08:00
|
|
|
adapter: postgresAdapter,
|
2022-06-01 15:00:10 +08:00
|
|
|
renderError: (_ctx, _out, error) => {
|
2021-07-04 15:01:02 +08:00
|
|
|
console.log('OIDC error', error);
|
2021-10-12 17:57:22 +08:00
|
|
|
throw error;
|
2021-06-27 20:44:05 +08:00
|
|
|
},
|
|
|
|
cookies: {
|
2022-05-20 00:08:33 +08:00
|
|
|
keys: cookieKeys,
|
2021-07-04 15:01:02 +08:00
|
|
|
long: cookieConfig,
|
|
|
|
short: cookieConfig,
|
2021-06-27 20:44:05 +08:00
|
|
|
},
|
|
|
|
jwks: {
|
|
|
|
keys,
|
|
|
|
},
|
2021-07-02 22:09:38 +08:00
|
|
|
features: {
|
2022-01-24 10:13:18 +08:00
|
|
|
userinfo: { enabled: true },
|
2021-07-02 22:09:38 +08:00
|
|
|
revocation: { enabled: true },
|
|
|
|
devInteractions: { enabled: false },
|
2021-08-15 23:39:03 +08:00
|
|
|
resourceIndicators: {
|
|
|
|
enabled: true,
|
2022-01-24 10:13:18 +08:00
|
|
|
// Disable the auto use of authorization_code granted resource feature
|
|
|
|
// https://github.com/panva/node-oidc-provider/blob/main/docs/README.md#usegrantedresource
|
|
|
|
useGrantedResource: () => false,
|
2022-04-08 16:07:34 +08:00
|
|
|
getResourceServerInfo: async (_, indicator) => {
|
2022-02-15 16:13:41 +08:00
|
|
|
const resourceServer = await findResourceByIndicator(indicator);
|
2021-12-02 14:08:15 +08:00
|
|
|
|
|
|
|
if (!resourceServer) {
|
|
|
|
throw new errors.InvalidTarget();
|
|
|
|
}
|
|
|
|
|
2022-04-08 16:07:34 +08:00
|
|
|
const { accessTokenTtl: accessTokenTTL } = resourceServer;
|
2021-12-02 14:08:15 +08:00
|
|
|
|
|
|
|
return {
|
2022-02-08 14:06:13 +08:00
|
|
|
accessTokenFormat: 'jwt',
|
2022-04-08 16:07:34 +08:00
|
|
|
scope: '',
|
2022-01-14 11:54:09 +08:00
|
|
|
accessTokenTTL,
|
2021-12-02 14:08:15 +08:00
|
|
|
};
|
|
|
|
},
|
2021-08-15 23:39:03 +08:00
|
|
|
},
|
2021-07-02 22:09:38 +08:00
|
|
|
},
|
|
|
|
interactions: {
|
2021-07-04 15:01:02 +08:00
|
|
|
url: (_, interaction) => {
|
|
|
|
switch (interaction.prompt.name) {
|
|
|
|
case 'login':
|
|
|
|
return routes.signIn.credentials;
|
|
|
|
case 'consent':
|
|
|
|
return routes.signIn.consent;
|
|
|
|
default:
|
|
|
|
throw new Error(`Prompt not supported: ${interaction.prompt.name}`);
|
|
|
|
}
|
|
|
|
},
|
2021-07-02 22:09:38 +08:00
|
|
|
},
|
2022-01-13 14:15:13 +08:00
|
|
|
extraClientMetadata: {
|
2022-04-11 14:22:16 +08:00
|
|
|
properties: Object.values(CustomClientMetadataKey),
|
2022-04-08 18:16:20 +08:00
|
|
|
validator: (_, key, value) => {
|
|
|
|
validateCustomClientMetadata(key, value);
|
2022-01-13 14:15:13 +08:00
|
|
|
},
|
|
|
|
},
|
2022-04-08 18:16:20 +08:00
|
|
|
// https://github.com/panva/node-oidc-provider/blob/main/recipes/client_based_origins.md
|
2022-05-23 19:18:48 +08:00
|
|
|
clientBasedCORS: (ctx, origin, client) =>
|
|
|
|
ctx.request.origin === origin || isOriginAllowed(origin, client.metadata()),
|
2022-06-01 15:00:10 +08:00
|
|
|
// https://github.com/panva/node-oidc-provider/blob/main/recipes/claim_configuration.md
|
|
|
|
claims: {
|
2022-06-01 15:43:16 +08:00
|
|
|
profile: ['username', 'name', 'avatar', 'role_names', 'custom_data'],
|
2022-06-01 15:00:10 +08:00
|
|
|
},
|
|
|
|
// https://github.com/panva/node-oidc-provider/tree/main/docs#findaccount
|
|
|
|
findAccount: async (_ctx, sub) => {
|
|
|
|
const { username, name, avatar, roleNames, customData } = await findUserById(sub);
|
2021-07-02 21:14:18 +08:00
|
|
|
|
2021-06-27 20:44:05 +08:00
|
|
|
return {
|
|
|
|
accountId: sub,
|
2022-06-01 15:00:10 +08:00
|
|
|
claims: async (use) => {
|
|
|
|
return snakecaseKeys({
|
|
|
|
sub,
|
|
|
|
username,
|
|
|
|
name,
|
|
|
|
avatar,
|
2022-06-01 15:43:16 +08:00
|
|
|
roleNames,
|
2022-06-01 15:00:10 +08:00
|
|
|
...(use === 'userinfo' && { customData }),
|
|
|
|
});
|
2022-05-22 11:33:13 +08:00
|
|
|
},
|
2021-06-27 20:44:05 +08:00
|
|
|
};
|
|
|
|
},
|
2022-01-13 14:15:13 +08:00
|
|
|
ttl: {
|
|
|
|
/**
|
|
|
|
* [OIDC Provider Default Settings](https://github.com/panva/node-oidc-provider/blob/main/docs/README.md#ttl)
|
|
|
|
*/
|
2022-06-01 15:00:10 +08:00
|
|
|
IdToken: (_ctx, _token, client) => {
|
2022-01-13 14:15:13 +08:00
|
|
|
const { idTokenTtl } = client.metadata();
|
2022-01-27 19:26:34 +08:00
|
|
|
|
2022-01-13 14:15:13 +08:00
|
|
|
return idTokenTtl ?? defaultIdTokenTtl;
|
|
|
|
},
|
2022-06-01 15:00:10 +08:00
|
|
|
RefreshToken: (_ctx, _token, client) => {
|
2022-01-13 14:15:13 +08:00
|
|
|
const { refreshTokenTtl } = client.metadata();
|
2022-01-27 19:26:34 +08:00
|
|
|
|
2022-01-13 14:15:13 +08:00
|
|
|
return refreshTokenTtl ?? defaultRefreshTokenTtl;
|
|
|
|
},
|
|
|
|
},
|
2022-06-01 15:00:10 +08:00
|
|
|
extraTokenClaims: async (_ctx, token) => {
|
2022-05-24 16:42:28 +08:00
|
|
|
// AccessToken type is not exported by default, need to asset token is AccessToken
|
|
|
|
if (token.kind === 'AccessToken') {
|
|
|
|
const { accountId } = token;
|
|
|
|
const { roleNames } = await findUserById(accountId);
|
|
|
|
|
|
|
|
// Add User Roles to the AccessToken claims. Should be removed once we have RBAC implemented.
|
|
|
|
// User Roles should be hidden and determined by the AccessToken scope only.
|
|
|
|
return {
|
|
|
|
roleNames,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
},
|
2021-06-27 20:44:05 +08:00
|
|
|
});
|
2022-05-19 11:24:26 +08:00
|
|
|
|
2022-05-20 13:54:05 +08:00
|
|
|
addOidcEventListeners(oidc);
|
2022-05-19 11:24:26 +08:00
|
|
|
|
2021-06-27 20:44:05 +08:00
|
|
|
app.use(mount('/oidc', oidc.app));
|
2022-01-27 19:26:34 +08:00
|
|
|
|
2021-07-03 21:19:20 +08:00
|
|
|
return oidc;
|
2021-06-27 20:44:05 +08:00
|
|
|
}
|