2022-02-22 20:42:29 -05:00
|
|
|
/* istanbul ignore file */
|
|
|
|
|
2022-07-02 08:55:22 -05:00
|
|
|
import { readFileSync } from 'fs';
|
|
|
|
|
2022-09-16 06:25:43 -05:00
|
|
|
import { userClaims } from '@logto/core-kit';
|
2022-09-14 22:12:33 -05:00
|
|
|
import { CustomClientMetadataKey } from '@logto/schemas';
|
2022-11-08 07:30:51 -05:00
|
|
|
import { tryThat } from '@logto/shared';
|
2022-10-21 00:14:17 -05:00
|
|
|
import type Koa from 'koa';
|
2021-06-27 07:44:05 -05:00
|
|
|
import mount from 'koa-mount';
|
2021-12-02 01:08:15 -05:00
|
|
|
import { Provider, errors } from 'oidc-provider';
|
2022-06-01 02:00:10 -05:00
|
|
|
import snakecaseKeys from 'snakecase-keys';
|
2021-06-27 07:44:05 -05:00
|
|
|
|
2022-11-21 03:38:24 -05:00
|
|
|
import envSet from '#src/env-set/index.js';
|
2022-12-17 06:05:28 -05:00
|
|
|
import { addOidcEventListeners } from '#src/event-listeners/index.js';
|
2022-12-17 09:59:10 -05:00
|
|
|
import koaAuditLog from '#src/middleware/koa-audit-log.js';
|
2022-11-21 03:38:24 -05:00
|
|
|
import postgresAdapter from '#src/oidc/adapter.js';
|
|
|
|
import { isOriginAllowed, validateCustomClientMetadata } from '#src/oidc/utils.js';
|
|
|
|
import { findApplicationById } from '#src/queries/application.js';
|
|
|
|
import { findResourceByIndicator } from '#src/queries/resource.js';
|
|
|
|
import { findUserById } from '#src/queries/user.js';
|
|
|
|
import { routes } from '#src/routes/consts.js';
|
|
|
|
import assertThat from '#src/utils/assert-that.js';
|
2021-08-29 22:30:54 -05:00
|
|
|
|
2022-11-21 03:38:24 -05:00
|
|
|
import { claimToUserKey, getUserClaims } from './scope.js';
|
2022-09-14 22:12:33 -05:00
|
|
|
|
2021-07-09 10:25:24 -05:00
|
|
|
export default async function initOidc(app: Koa): Promise<Provider> {
|
2022-08-08 01:00:24 -05:00
|
|
|
const { issuer, cookieKeys, privateJwks, defaultIdTokenTtl, defaultRefreshTokenTtl } =
|
2022-10-09 04:22:34 -05:00
|
|
|
envSet.oidc;
|
2022-08-05 00:58:31 -05:00
|
|
|
const logoutSource = readFileSync('static/html/logout.html', 'utf8');
|
2022-04-20 01:14:37 -05:00
|
|
|
|
2021-07-04 02:01:02 -05:00
|
|
|
const cookieConfig = Object.freeze({
|
|
|
|
sameSite: 'lax',
|
|
|
|
path: '/',
|
|
|
|
signed: true,
|
|
|
|
} as const);
|
2021-08-15 10:39:03 -05:00
|
|
|
const oidc = new Provider(issuer, {
|
2021-06-27 07:44:05 -05:00
|
|
|
adapter: postgresAdapter,
|
2022-06-01 02:00:10 -05:00
|
|
|
renderError: (_ctx, _out, error) => {
|
2021-07-04 02:01:02 -05:00
|
|
|
console.log('OIDC error', error);
|
2021-10-12 04:57:22 -05:00
|
|
|
throw error;
|
2021-06-27 07:44:05 -05:00
|
|
|
},
|
|
|
|
cookies: {
|
2022-05-19 11:08:33 -05:00
|
|
|
keys: cookieKeys,
|
2021-07-04 02:01:02 -05:00
|
|
|
long: cookieConfig,
|
|
|
|
short: cookieConfig,
|
2021-06-27 07:44:05 -05:00
|
|
|
},
|
|
|
|
jwks: {
|
2022-08-08 01:00:24 -05:00
|
|
|
keys: privateJwks,
|
2021-06-27 07:44:05 -05:00
|
|
|
},
|
2022-08-15 07:45:13 -05:00
|
|
|
conformIdTokenClaims: false,
|
2021-07-02 09:09:38 -05:00
|
|
|
features: {
|
2022-08-15 07:45:13 -05:00
|
|
|
userinfo: { enabled: true },
|
2021-07-02 09:09:38 -05:00
|
|
|
revocation: { enabled: true },
|
|
|
|
devInteractions: { enabled: false },
|
2022-09-21 00:06:56 -05:00
|
|
|
clientCredentials: { enabled: true },
|
2022-07-02 08:55:22 -05:00
|
|
|
rpInitiatedLogout: {
|
|
|
|
logoutSource: (ctx, form) => {
|
|
|
|
// eslint-disable-next-line no-template-curly-in-string
|
|
|
|
ctx.body = logoutSource.replace('${form}', form);
|
|
|
|
},
|
|
|
|
},
|
2022-06-29 21:29:29 -05:00
|
|
|
// https://github.com/panva/node-oidc-provider/blob/main/docs/README.md#featuresresourceindicators
|
2021-08-15 10:39:03 -05:00
|
|
|
resourceIndicators: {
|
|
|
|
enabled: true,
|
2022-06-29 21:29:29 -05:00
|
|
|
defaultResource: () => '',
|
2022-01-23 21:13:18 -05:00
|
|
|
// Disable the auto use of authorization_code granted resource feature
|
|
|
|
useGrantedResource: () => false,
|
2022-04-08 03:07:34 -05:00
|
|
|
getResourceServerInfo: async (_, indicator) => {
|
2022-02-15 03:13:41 -05:00
|
|
|
const resourceServer = await findResourceByIndicator(indicator);
|
2021-12-02 01:08:15 -05:00
|
|
|
|
|
|
|
if (!resourceServer) {
|
|
|
|
throw new errors.InvalidTarget();
|
|
|
|
}
|
|
|
|
|
2022-04-08 03:07:34 -05:00
|
|
|
const { accessTokenTtl: accessTokenTTL } = resourceServer;
|
2021-12-02 01:08:15 -05:00
|
|
|
|
|
|
|
return {
|
2022-02-08 01:06:13 -05:00
|
|
|
accessTokenFormat: 'jwt',
|
2022-04-08 03:07:34 -05:00
|
|
|
scope: '',
|
2022-01-13 22:54:09 -05:00
|
|
|
accessTokenTTL,
|
2021-12-02 01:08:15 -05:00
|
|
|
};
|
|
|
|
},
|
2021-08-15 10:39:03 -05:00
|
|
|
},
|
2021-07-02 09:09:38 -05:00
|
|
|
},
|
|
|
|
interactions: {
|
2021-07-04 02:01:02 -05: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 09:09:38 -05:00
|
|
|
},
|
2022-01-13 01:15:13 -05:00
|
|
|
extraClientMetadata: {
|
2022-04-11 01:22:16 -05:00
|
|
|
properties: Object.values(CustomClientMetadataKey),
|
2022-04-08 05:16:20 -05:00
|
|
|
validator: (_, key, value) => {
|
|
|
|
validateCustomClientMetadata(key, value);
|
2022-01-13 01:15:13 -05:00
|
|
|
},
|
|
|
|
},
|
2022-04-08 05:16:20 -05:00
|
|
|
// https://github.com/panva/node-oidc-provider/blob/main/recipes/client_based_origins.md
|
2022-05-23 06:18:48 -05:00
|
|
|
clientBasedCORS: (ctx, origin, client) =>
|
2022-07-05 04:36:43 -05:00
|
|
|
ctx.request.origin === origin ||
|
|
|
|
isOriginAllowed(origin, client.metadata(), client.redirectUris),
|
2022-06-01 02:00:10 -05:00
|
|
|
// https://github.com/panva/node-oidc-provider/blob/main/recipes/claim_configuration.md
|
2022-09-14 22:12:33 -05:00
|
|
|
// Note node-provider will append `claims` here to the default claims instead of overriding
|
|
|
|
claims: userClaims,
|
2022-06-01 02:00:10 -05:00
|
|
|
// https://github.com/panva/node-oidc-provider/tree/main/docs#findaccount
|
|
|
|
findAccount: async (_ctx, sub) => {
|
2022-08-15 07:45:13 -05:00
|
|
|
const user = await findUserById(sub);
|
2021-07-02 08:14:18 -05:00
|
|
|
|
2021-06-27 07:44:05 -05:00
|
|
|
return {
|
|
|
|
accountId: sub,
|
2022-09-14 22:12:33 -05:00
|
|
|
claims: async (use, scope, claims, rejected) => {
|
2022-08-15 07:45:13 -05:00
|
|
|
return snakecaseKeys(
|
|
|
|
{
|
2022-09-14 22:12:33 -05:00
|
|
|
/**
|
|
|
|
* This line is required because:
|
|
|
|
* 1. TypeScript will complain since `Object.fromEntries()` has a fixed key type `string`
|
|
|
|
* 2. Scope `openid` is removed from `UserScope` enum
|
|
|
|
*/
|
2022-08-15 07:45:13 -05:00
|
|
|
sub,
|
2022-09-14 22:12:33 -05:00
|
|
|
...Object.fromEntries(
|
|
|
|
getUserClaims(use, scope, claims, rejected).map((claim) => [
|
|
|
|
claim,
|
|
|
|
user[claimToUserKey[claim]],
|
|
|
|
])
|
|
|
|
),
|
2022-08-15 07:45:13 -05:00
|
|
|
},
|
2022-09-14 22:12:33 -05:00
|
|
|
{
|
|
|
|
deep: false,
|
|
|
|
}
|
2022-08-15 07:45:13 -05:00
|
|
|
);
|
2022-05-21 22:33:13 -05:00
|
|
|
},
|
2021-06-27 07:44:05 -05:00
|
|
|
};
|
|
|
|
},
|
2022-01-13 01:15:13 -05:00
|
|
|
ttl: {
|
|
|
|
/**
|
|
|
|
* [OIDC Provider Default Settings](https://github.com/panva/node-oidc-provider/blob/main/docs/README.md#ttl)
|
|
|
|
*/
|
2022-06-01 02:00:10 -05:00
|
|
|
IdToken: (_ctx, _token, client) => {
|
2022-01-13 01:15:13 -05:00
|
|
|
const { idTokenTtl } = client.metadata();
|
2022-01-27 06:26:34 -05:00
|
|
|
|
2022-01-13 01:15:13 -05:00
|
|
|
return idTokenTtl ?? defaultIdTokenTtl;
|
|
|
|
},
|
2022-06-01 02:00:10 -05:00
|
|
|
RefreshToken: (_ctx, _token, client) => {
|
2022-01-13 01:15:13 -05:00
|
|
|
const { refreshTokenTtl } = client.metadata();
|
2022-01-27 06:26:34 -05:00
|
|
|
|
2022-01-13 01:15:13 -05:00
|
|
|
return refreshTokenTtl ?? defaultRefreshTokenTtl;
|
|
|
|
},
|
2022-07-06 07:33:25 -05:00
|
|
|
AccessToken: (ctx, token) => {
|
|
|
|
if (token.resourceServer) {
|
|
|
|
return token.resourceServer.accessTokenTTL ?? 60 * 60; // 1 hour in seconds
|
|
|
|
}
|
|
|
|
|
|
|
|
return 60 * 60; // 1 hour in seconds
|
|
|
|
},
|
|
|
|
Interaction: 3600 /* 1 hour in seconds */,
|
|
|
|
Session: 1_209_600 /* 14 days in seconds */,
|
|
|
|
Grant: 1_209_600 /* 14 days in seconds */,
|
2022-01-13 01:15:13 -05:00
|
|
|
},
|
2022-06-01 02:00:10 -05:00
|
|
|
extraTokenClaims: async (_ctx, token) => {
|
2022-05-24 03:42:28 -05:00
|
|
|
if (token.kind === 'AccessToken') {
|
|
|
|
const { accountId } = token;
|
2022-11-08 07:30:51 -05:00
|
|
|
const { roleNames } = await tryThat(
|
|
|
|
findUserById(accountId),
|
|
|
|
new errors.InvalidClient(`invalid user ${accountId}`)
|
|
|
|
);
|
2022-05-24 03:42:28 -05:00
|
|
|
|
2022-06-01 03:02:23 -05:00
|
|
|
return snakecaseKeys({
|
2022-05-24 03:42:28 -05:00
|
|
|
roleNames,
|
2022-06-01 03:02:23 -05:00
|
|
|
});
|
2022-05-24 03:42:28 -05:00
|
|
|
}
|
2022-09-21 00:06:56 -05:00
|
|
|
|
|
|
|
// `token.kind === 'ClientCredentials'`
|
|
|
|
const { clientId } = token;
|
|
|
|
assertThat(clientId, 'oidc.invalid_grant');
|
2022-11-08 07:30:51 -05:00
|
|
|
|
|
|
|
const { roleNames } = await tryThat(
|
|
|
|
findApplicationById(clientId),
|
|
|
|
new errors.InvalidClient(`invalid client ${clientId}`)
|
|
|
|
);
|
2022-09-21 00:06:56 -05:00
|
|
|
|
|
|
|
return snakecaseKeys({ roleNames });
|
2022-05-24 03:42:28 -05:00
|
|
|
},
|
2021-06-27 07:44:05 -05:00
|
|
|
});
|
2022-05-18 22:24:26 -05:00
|
|
|
|
2022-05-20 00:54:05 -05:00
|
|
|
addOidcEventListeners(oidc);
|
2022-05-18 22:24:26 -05:00
|
|
|
|
2022-12-17 09:59:10 -05:00
|
|
|
// Provide audit log context for event listeners
|
|
|
|
oidc.use(koaAuditLog());
|
|
|
|
|
2021-06-27 07:44:05 -05:00
|
|
|
app.use(mount('/oidc', oidc.app));
|
2022-01-27 06:26:34 -05:00
|
|
|
|
2021-07-03 08:19:20 -05:00
|
|
|
return oidc;
|
2021-06-27 07:44:05 -05:00
|
|
|
}
|