mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
api: add sign in / consent
This commit is contained in:
parent
f419a91c5d
commit
928a631bcc
9 changed files with 183 additions and 43 deletions
|
@ -1 +1,8 @@
|
|||
module.exports = {extends: ['@commitlint/config-conventional']}
|
||||
const { rules } = require('@commitlint/config-conventional');
|
||||
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'type-enum': [2, 'always', [...rules['type-enum'][2], 'api']],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"koa-body": "^4.2.0",
|
||||
"koa-logger": "^3.2.1",
|
||||
"koa-mount": "^4.0.0",
|
||||
"koa-proxies": "^0.12.1",
|
||||
"koa-router": "^10.0.0",
|
||||
"module-alias": "^2.2.2",
|
||||
"oidc-provider": "^7.4.1",
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
import { assertEnv } from '@/utils/env';
|
||||
|
||||
export const signInRoute = assertEnv('UI_SIGN_IN_ROUTE');
|
||||
const signIn = assertEnv('UI_SIGN_IN_ROUTE');
|
||||
|
||||
export const routes = Object.freeze({
|
||||
signIn: {
|
||||
credentials: signIn,
|
||||
consent: signIn + '/consent',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -7,22 +7,29 @@ import postgresAdapter from '@/oidc/adapter';
|
|||
import { fromKeyLike } from 'jose/jwk/from_key_like';
|
||||
import { getEnv } from '@/utils/env';
|
||||
import { findUserById } from '@/queries/user';
|
||||
import { signInRoute } from '@/consts';
|
||||
import { routes } from '@/consts';
|
||||
|
||||
export default async function initOidc(app: Koa, port: number): Promise<Provider> {
|
||||
const privateKey = crypto.createPrivateKey(
|
||||
Buffer.from(getEnv('OIDC_PROVIDER_PRIVATE_KEY_BASE64'), 'base64')
|
||||
);
|
||||
const keys = [await fromKeyLike(privateKey)];
|
||||
const cookieConfig = Object.freeze({
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
signed: true,
|
||||
} as const);
|
||||
const oidc = new Provider(`http://localhost:${port}/oidc`, {
|
||||
adapter: postgresAdapter,
|
||||
renderError: (ctx, out, error) => {
|
||||
console.log(error);
|
||||
console.log('OIDC error', error);
|
||||
},
|
||||
cookies: {
|
||||
// V2: Rotate this when necessary
|
||||
// https://github.com/panva/node-oidc-provider/blob/main/docs/README.md#cookieskeys
|
||||
keys: ['LOGTOSEKRIT1'],
|
||||
long: cookieConfig,
|
||||
short: cookieConfig,
|
||||
},
|
||||
jwks: {
|
||||
keys,
|
||||
|
@ -41,7 +48,16 @@ export default async function initOidc(app: Koa, port: number): Promise<Provider
|
|||
devInteractions: { enabled: false },
|
||||
},
|
||||
interactions: {
|
||||
url: (_) => signInRoute,
|
||||
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}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
clientBasedCORS: (_, origin) => {
|
||||
console.log('origin', origin);
|
||||
|
|
|
@ -2,19 +2,18 @@ import Koa from 'koa';
|
|||
import Router from 'koa-router';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import createSignInRoutes from '@/routes/sign-in';
|
||||
import createUIRoutes from '@/routes/ui';
|
||||
import createUIProxy from '@/proxies/ui';
|
||||
|
||||
const createRouter = (provider: Provider): Router => {
|
||||
const router = new Router();
|
||||
|
||||
router.use('/api', createSignInRoutes());
|
||||
router.use(createUIRoutes(provider));
|
||||
router.use('/api', createSignInRoutes(provider));
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
export default function initRouter(app: Koa, provider: Provider): Router {
|
||||
const router = createRouter(provider);
|
||||
app.use(router.routes()).use(router.allowedMethods());
|
||||
app.use(router.routes()).use(createUIProxy()).use(router.allowedMethods());
|
||||
return router;
|
||||
}
|
||||
|
|
10
packages/core/src/proxies/ui.ts
Normal file
10
packages/core/src/proxies/ui.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import proxy from 'koa-proxies';
|
||||
|
||||
// CAUTION: this is for testing only
|
||||
export default function createUIProxy() {
|
||||
return proxy(/^\/(?!api|oidc).*$/, {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
logs: true,
|
||||
});
|
||||
}
|
|
@ -4,26 +4,81 @@ import koaBody from 'koa-body';
|
|||
import { object, string } from 'zod';
|
||||
import { encryptPassword } from '@/utils/password';
|
||||
import { findUserById } from '@/queries/user';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { conditional } from '@logto/essentials';
|
||||
|
||||
export default function createSignInRoutes() {
|
||||
export default function createSignInRoutes(provider: Provider) {
|
||||
const router = new Router();
|
||||
|
||||
router.post('/sign-in', koaBody(), async (ctx) => {
|
||||
const SignInBody = object({
|
||||
id: string().min(1),
|
||||
password: string().min(1),
|
||||
});
|
||||
const { id, password } = SignInBody.parse(ctx.request.body);
|
||||
const { passwordEncrypted, passwordEncryptionMethod, passwordEncryptionSalt } =
|
||||
await findUserById(id);
|
||||
const {
|
||||
prompt: { name },
|
||||
} = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
|
||||
assert(passwordEncrypted && passwordEncryptionMethod && passwordEncryptionSalt);
|
||||
assert(
|
||||
encryptPassword(id, password, passwordEncryptionSalt, passwordEncryptionMethod) ===
|
||||
passwordEncrypted
|
||||
if (name === 'login') {
|
||||
const SignInBody = object({
|
||||
id: string().min(1),
|
||||
password: string().min(1),
|
||||
});
|
||||
const { id, password } = SignInBody.parse(ctx.request.body);
|
||||
|
||||
const { passwordEncrypted, passwordEncryptionMethod, passwordEncryptionSalt } =
|
||||
await findUserById(id);
|
||||
|
||||
assert(passwordEncrypted && passwordEncryptionMethod && passwordEncryptionSalt);
|
||||
assert(
|
||||
encryptPassword(id, password, passwordEncryptionSalt, passwordEncryptionMethod) ===
|
||||
passwordEncrypted
|
||||
);
|
||||
|
||||
const redirectTo = await provider.interactionResult(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
{
|
||||
login: { accountId: id },
|
||||
},
|
||||
{ mergeWithLastSubmission: false }
|
||||
);
|
||||
ctx.body = { redirectTo };
|
||||
} else if (name === 'consent') {
|
||||
ctx.body = { redirectTo: ctx.request.origin + '/sign-in/consent' };
|
||||
} else {
|
||||
throw new Error(`Prompt not supported: ${name}`);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/sign-in/consent', async (ctx) => {
|
||||
const { session, grantId, params, prompt } = await provider.interactionDetails(
|
||||
ctx.req,
|
||||
ctx.res
|
||||
);
|
||||
|
||||
ctx.status = 204;
|
||||
assert(session, 'Session not found');
|
||||
const { accountId } = session;
|
||||
const grant =
|
||||
conditional(grantId && (await provider.Grant.find(grantId))) ??
|
||||
new provider.Grant({ accountId, clientId: String(params.client_id) });
|
||||
|
||||
// V2: fulfill missing claims / resources
|
||||
const PromptDetailsBody = object({
|
||||
missingOIDCScope: string().array().optional(),
|
||||
});
|
||||
const { missingOIDCScope } = PromptDetailsBody.parse(prompt.details);
|
||||
|
||||
if (missingOIDCScope) {
|
||||
grant.addOIDCScope(missingOIDCScope.join(' '));
|
||||
}
|
||||
|
||||
const finalGrantId = await grant.save();
|
||||
|
||||
// V2: configure consent
|
||||
const redirectTo = await provider.interactionResult(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
{ consent: { grantId: finalGrantId } },
|
||||
{ mergeWithLastSubmission: true }
|
||||
);
|
||||
ctx.body = { redirectTo };
|
||||
});
|
||||
|
||||
return router.routes();
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import got from 'got';
|
||||
import Router from 'koa-router';
|
||||
import { promisify } from 'util';
|
||||
import stream from 'stream';
|
||||
import { signInRoute } from '@/consts';
|
||||
import { getEnv } from '@/utils/env';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
export default function createUIRoutes(provider: Provider) {
|
||||
const pipeline = promisify(stream.pipeline);
|
||||
const router = new Router();
|
||||
|
||||
router.get(new RegExp(`^${signInRoute}(?:/|$)`), async (ctx) => {
|
||||
const details = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
console.log('details', details);
|
||||
// CAUTION: this is for dev purpose only, add a switch if needed
|
||||
await pipeline(got.stream.get(getEnv('UI_PLAYGROUND_URL')), ctx.res);
|
||||
});
|
||||
return router.routes();
|
||||
}
|
|
@ -2180,6 +2180,11 @@ event-stream@=3.3.4:
|
|||
stream-combiner "~0.0.4"
|
||||
through "~2.3.1"
|
||||
|
||||
eventemitter3@^4.0.0:
|
||||
version "4.0.7"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
||||
|
||||
execa@^5.0.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
|
||||
|
@ -2386,6 +2391,11 @@ flatted@^3.1.0:
|
|||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469"
|
||||
integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==
|
||||
|
||||
follow-redirects@^1.0.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
|
||||
integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
|
||||
|
||||
for-in@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
||||
|
@ -2755,6 +2765,23 @@ http-errors@^1.6.3, http-errors@^1.7.3:
|
|||
statuses ">= 1.5.0 < 2"
|
||||
toidentifier "1.0.0"
|
||||
|
||||
http-errors@~1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.4.0.tgz#6c0242dea6b3df7afda153c71089b31c6e82aabf"
|
||||
integrity sha1-bAJC3qaz33r9oVPHEImzHG6Cqr8=
|
||||
dependencies:
|
||||
inherits "2.0.1"
|
||||
statuses ">= 1.2.1 < 2"
|
||||
|
||||
http-proxy@^1.18.1:
|
||||
version "1.18.1"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
|
||||
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
|
||||
dependencies:
|
||||
eventemitter3 "^4.0.0"
|
||||
follow-redirects "^1.0.0"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
http2-wrapper@^1.0.0-beta.5.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d"
|
||||
|
@ -2854,6 +2881,11 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4:
|
|||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
inherits@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
|
||||
integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
|
||||
|
||||
ini@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5"
|
||||
|
@ -3219,6 +3251,11 @@ is-yarn-global@^0.3.0:
|
|||
resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
|
||||
integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
|
||||
|
||||
isarray@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
||||
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
|
||||
|
||||
isarray@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
|
@ -3449,6 +3486,14 @@ koa-mount@^4.0.0:
|
|||
debug "^4.0.1"
|
||||
koa-compose "^4.1.0"
|
||||
|
||||
koa-proxies@^0.12.1:
|
||||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/koa-proxies/-/koa-proxies-0.12.1.tgz#c0c5f2332b791f095b5d0d77cea15237514acebd"
|
||||
integrity sha512-qCOGY7Qoe/Ewn2VskP9TdLMZffmsv8JUBWllNlmTJmgl1059nxt5jl7QBWNniqx2BthVSU5TIBuhUULA5d6t+A==
|
||||
dependencies:
|
||||
http-proxy "^1.18.1"
|
||||
path-match "^1.2.4"
|
||||
|
||||
koa-router@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/koa-router/-/koa-router-10.0.0.tgz#7bc76a031085731e61fc92c1683687b2f44de6a4"
|
||||
|
@ -4205,11 +4250,26 @@ path-key@^3.0.0, path-key@^3.1.0:
|
|||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
|
||||
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
|
||||
|
||||
path-match@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/path-match/-/path-match-1.2.4.tgz#a62747f3c7e0c2514762697f24443585b09100ea"
|
||||
integrity sha1-pidH88fgwlFHYml/JEQ1hbCRAOo=
|
||||
dependencies:
|
||||
http-errors "~1.4.0"
|
||||
path-to-regexp "^1.0.0"
|
||||
|
||||
path-parse@^1.0.6:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||
|
||||
path-to-regexp@^1.0.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
|
||||
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
|
||||
dependencies:
|
||||
isarray "0.0.1"
|
||||
|
||||
path-to-regexp@^6.1.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38"
|
||||
|
@ -4674,6 +4734,11 @@ require-from-string@^2.0.2:
|
|||
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
||||
|
||||
requires-port@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
||||
|
||||
reserved-words@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1"
|
||||
|
@ -5169,7 +5234,7 @@ static-extend@^0.1.1:
|
|||
define-property "^0.2.5"
|
||||
object-copy "^0.1.0"
|
||||
|
||||
"statuses@>= 1.5.0 < 2", statuses@^1.5.0:
|
||||
"statuses@>= 1.2.1 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
|
||||
|
|
Loading…
Reference in a new issue