diff --git a/packages/core/package.json b/packages/core/package.json index 4c5721ac5..bef60d062 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,18 +13,20 @@ }, "dependencies": { "@logto/essentials": "^1.0.5", - "@logto/schemas": "^1.0.1", + "@logto/schemas": "^1.0.3", "dayjs": "^1.10.5", "dotenv": "^10.0.0", "got": "^11.8.2", "koa": "^2.13.1", + "koa-body": "^4.2.0", "koa-logger": "^3.2.1", "koa-mount": "^4.0.0", "koa-router": "^10.0.0", "module-alias": "^2.2.2", "oidc-provider": "^7.4.1", "slonik": "^23.8.1", - "slonik-interceptor-preset": "^1.2.10" + "slonik-interceptor-preset": "^1.2.10", + "zod": "^3.2.0" }, "devDependencies": { "@commitlint/cli": "^12.1.4", diff --git a/packages/core/src/consts.ts b/packages/core/src/consts.ts index 8aa908d72..f0c46b673 100644 --- a/packages/core/src/consts.ts +++ b/packages/core/src/consts.ts @@ -1,3 +1,3 @@ -import { assertEnv } from './utils'; +import { assertEnv } from '@/utils/env'; -export const signInRoute = assertEnv('SIGN_IN_ROUTE'); +export const signInRoute = assertEnv('UI_SIGN_IN_ROUTE'); diff --git a/packages/core/src/database/pool.ts b/packages/core/src/database/pool.ts index 09d3f3e82..cdd826b93 100644 --- a/packages/core/src/database/pool.ts +++ b/packages/core/src/database/pool.ts @@ -1,6 +1,6 @@ import { createPool } from 'slonik'; import { createInterceptors } from 'slonik-interceptor-preset'; -import { getEnv } from '@/utils'; +import { getEnv } from '@/utils/env'; const interceptors = [...createInterceptors()]; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 304e09120..77f47cd8c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,7 +5,7 @@ dotenv.config(); import Koa from 'koa'; import initApp from './init'; -import { getEnv } from './utils'; +import { getEnv } from './utils/env'; const app = new Koa(); const port = Number(getEnv('PORT', '3001')); diff --git a/packages/core/src/init/index.ts b/packages/core/src/init/index.ts index 5688dd3b6..2b6fd92c6 100644 --- a/packages/core/src/init/index.ts +++ b/packages/core/src/init/index.ts @@ -6,8 +6,9 @@ import initRouter from './router'; export default async function initApp(app: Koa, port: number): Promise { app.use(logger()); - await initOidc(app, port); - initRouter(app); + + const provider = await initOidc(app, port); + initRouter(app, provider); app.listen(port, () => { console.log(`App is listening on port ${port}`); diff --git a/packages/core/src/init/oidc.ts b/packages/core/src/init/oidc.ts index a8195aacc..c499ea497 100644 --- a/packages/core/src/init/oidc.ts +++ b/packages/core/src/init/oidc.ts @@ -5,11 +5,11 @@ import { Provider } from 'oidc-provider'; import postgresAdapter from '@/oidc/adapter'; import { fromKeyLike } from 'jose/jwk/from_key_like'; -import { getEnv } from '@/utils'; +import { getEnv } from '@/utils/env'; import { findUserById } from '@/queries/user'; import { signInRoute } from '@/consts'; -export default async function initOidc(app: Koa, port: number): Promise { +export default async function initOidc(app: Koa, port: number): Promise { const privateKey = crypto.createPrivateKey( Buffer.from(getEnv('OIDC_PROVIDER_PRIVATE_KEY_BASE64'), 'base64') ); @@ -41,7 +41,7 @@ export default async function initOidc(app: Koa, port: number): Promise { devInteractions: { enabled: false }, }, interactions: { - url: (_, interaction) => `${signInRoute}?uid=${interaction.uid}`, + url: (_) => signInRoute, }, clientBasedCORS: (_, origin) => { console.log('origin', origin); @@ -63,4 +63,5 @@ export default async function initOidc(app: Koa, port: number): Promise { }, }); app.use(mount('/oidc', oidc.app)); + return oidc; } diff --git a/packages/core/src/init/router.ts b/packages/core/src/init/router.ts index fbe8a995d..379548630 100644 --- a/packages/core/src/init/router.ts +++ b/packages/core/src/init/router.ts @@ -1,19 +1,20 @@ -import got from 'got'; import Koa from 'koa'; import Router from 'koa-router'; -import { promisify } from 'util'; -import stream from 'stream'; -import { signInRoute } from '@/consts'; -import { getEnv } from '@/utils'; +import { Provider } from 'oidc-provider'; +import createSignInRoutes from '@/routes/sign-in'; +import createUIRoutes from '@/routes/ui'; -const pipeline = promisify(stream.pipeline); -const router = new Router(); +const createRouter = (provider: Provider): Router => { + const router = new Router(); -router.get(new RegExp(`^${signInRoute}(?:/|$)`), async (ctx) => { - // CAUTION: this is for dev purpose only, add a switch if needed - await pipeline(got.stream.get(getEnv('PLAYGROUND_URL')), ctx.res); -}); + router.use('/api', createSignInRoutes()); + router.use(createUIRoutes(provider)); -export default function initRouter(app: Koa): void { + return router; +}; + +export default function initRouter(app: Koa, provider: Provider): Router { + const router = createRouter(provider); app.use(router.routes()).use(router.allowedMethods()); + return router; } diff --git a/packages/core/src/routes/sign-in.ts b/packages/core/src/routes/sign-in.ts new file mode 100644 index 000000000..87312973b --- /dev/null +++ b/packages/core/src/routes/sign-in.ts @@ -0,0 +1,30 @@ +import assert from 'assert'; +import Router from 'koa-router'; +import koaBody from 'koa-body'; +import { object, string } from 'zod'; +import { encryptPassword } from '@/utils/password'; +import { findUserById } from '@/queries/user'; + +export default function createSignInRoutes() { + 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); + + assert(passwordEncrypted && passwordEncryptionMethod && passwordEncryptionSalt); + assert( + encryptPassword(id, password, passwordEncryptionSalt, passwordEncryptionMethod) === + passwordEncrypted + ); + + ctx.status = 204; + }); + + return router.routes(); +} diff --git a/packages/core/src/routes/ui.ts b/packages/core/src/routes/ui.ts new file mode 100644 index 000000000..29a490b25 --- /dev/null +++ b/packages/core/src/routes/ui.ts @@ -0,0 +1,20 @@ +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(); +} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils/env.ts similarity index 100% rename from packages/core/src/utils.ts rename to packages/core/src/utils/env.ts diff --git a/packages/core/src/utils/password.ts b/packages/core/src/utils/password.ts new file mode 100644 index 000000000..ced5b1c83 --- /dev/null +++ b/packages/core/src/utils/password.ts @@ -0,0 +1,39 @@ +import assert from 'assert'; +import { createHash } from 'crypto'; +import { PasswordEncryptionMethod } from '@logto/schemas'; +import { number, string } from 'zod'; +import { assertEnv } from './env'; + +const peppers = string() + .array() + .parse(JSON.parse(assertEnv('PASSWORD_PEPPERS'))); +const iterationCount = number() + .min(100) + .parse(Number(assertEnv('PASSWORD_INTERATION_COUNT'))); + +export const encryptPassword = ( + id: string, + password: string, + salt: string, + method: PasswordEncryptionMethod +): string => { + assert( + method === PasswordEncryptionMethod.SaltAndPepper, + 'Unsupported password encryption method' + ); + + const sum = [...id].reduce((acc, current) => acc + current.charCodeAt(0), 0); + const pepper = peppers[sum % peppers.length]; + + assert(pepper, 'Password pepper not found'); + + let result = password; + + for (let i = 0; i < iterationCount; ++i) { + result = createHash('sha256') + .update(salt + result + pepper) + .digest('hex'); + } + + return result; +}; diff --git a/packages/core/yarn.lock b/packages/core/yarn.lock index 97ed8d583..e2e35032c 100644 --- a/packages/core/yarn.lock +++ b/packages/core/yarn.lock @@ -396,10 +396,10 @@ lodash.orderby "^4.6.0" lodash.pick "^4.4.0" -"@logto/schemas@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@logto/schemas/-/schemas-1.0.1.tgz#786866a280568b275b9c55c2b884b5864dc42a06" - integrity sha512-SwAz/rOE61RDBU3TzsDqO30vkLRgD/Jcc3H3tC40gl39wTfxWiTrXgGtWTxAhrhdTwstEMa5+5ozczP/LvopwQ== +"@logto/schemas@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@logto/schemas/-/schemas-1.0.3.tgz#6a596bb9d1b99460857eaec5d643040ae384e037" + integrity sha512-inCo/PQUQl9OZFkjehjcB/4PBzYBW9FVn/3nZ2CPr/+3dJKDF3B0qxu8gyQICGJuWYx1BZPygc/NvV0F1klR3g== "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" @@ -538,6 +538,13 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/formidable@^1.0.31": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-1.2.2.tgz#e690d60732ee9d3f0a441bc572c17409785b283c" + integrity sha512-8RDAMnMHOh7QrY1xuQ7s6/Xre9pMvJ2zT2VgATiz5cIE71Q/6N3+P8sr3z/dNWNmvX5/aX9x8uJlG0MZiMZXoA== + dependencies: + "@types/node" "*" + "@types/glob@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" @@ -1276,6 +1283,16 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +co-body@^5.1.1: + version "5.2.0" + resolved "https://registry.yarnpkg.com/co-body/-/co-body-5.2.0.tgz#5a0a658c46029131e0e3a306f67647302f71c124" + integrity sha512-sX/LQ7LqUhgyaxzbe7IqwPeTr2yfpfUIQ/dgpKo6ZI4y4lpQA0YxAomWIY+7I7rHWcG02PG+OuPREzMW/5tszQ== + dependencies: + inflation "^2.0.0" + qs "^6.4.0" + raw-body "^2.2.0" + type-is "^1.6.14" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -2374,6 +2391,11 @@ for-in@^1.0.2: resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= +formidable@^1.1.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" + integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -2814,6 +2836,11 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +inflation@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f" + integrity sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8= + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -3375,6 +3402,15 @@ kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +koa-body@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/koa-body/-/koa-body-4.2.0.tgz#37229208b820761aca5822d14c5fc55cee31b26f" + integrity sha512-wdGu7b9amk4Fnk/ytH8GuWwfs4fsB5iNkY8kZPpgQVb04QZSv85T0M8reb+cJmvLE8cjPYvBzRikD3s6qz8OoA== + dependencies: + "@types/formidable" "^1.0.31" + co-body "^5.1.1" + formidable "^1.1.1" + koa-compose@^3.0.0: version "3.2.1" resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" @@ -3882,7 +3918,7 @@ object-hash@^2.0.3: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== -object-inspect@^1.10.3: +object-inspect@^1.10.3, object-inspect@^1.9.0: version "1.10.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw== @@ -4486,6 +4522,13 @@ q@^1.5.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qs@^6.4.0: + version "6.10.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" + integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== + dependencies: + side-channel "^1.0.4" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -4501,7 +4544,7 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -raw-body@^2.4.1: +raw-body@^2.2.0, raw-body@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== @@ -4863,6 +4906,15 @@ shell-quote@^1.7.2: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -5452,7 +5504,7 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@^1.6.16: +type-is@^1.6.14, type-is@^1.6.16: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -5792,3 +5844,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.2.0.tgz#4f06fac3c74e56902eae43a47a1687bf49c9b70b" + integrity sha512-yvcO3FZ8URR+LliMGqaW7tlVOOTzmup3vzKEe9Ds7twyJtdhvYa7dIYr0FbD1wVfWC1OuS83vZfHtCKslPuRhA==