From acd8157a0d2e13014c55b73c349832d9b2d425c5 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Sun, 15 Aug 2021 23:39:03 +0800 Subject: [PATCH] feat: validate access token if needed --- packages/core/package.json | 2 +- packages/core/src/env/consts.ts | 1 - packages/core/src/middleware/koa-auth.ts | 38 +++++++++++++++++++---- packages/core/src/middleware/koa-guard.ts | 8 ++--- packages/core/src/oidc/consts.ts | 11 +++++++ packages/core/src/oidc/init.ts | 13 ++++---- packages/core/src/routes/application.ts | 4 +-- packages/core/src/routes/init.ts | 25 +++++++++------ packages/schemas/src/index.ts | 1 + packages/schemas/src/types/index.ts | 1 + packages/schemas/src/types/user.ts | 13 ++++++++ pnpm-lock.yaml | 2 +- 12 files changed, 87 insertions(+), 32 deletions(-) create mode 100644 packages/core/src/oidc/consts.ts create mode 100644 packages/schemas/src/types/index.ts create mode 100644 packages/schemas/src/types/user.ts diff --git a/packages/core/package.json b/packages/core/package.json index 4516b351e..02ca46d4e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -46,7 +46,7 @@ "@types/koa": "^2.13.3", "@types/koa-logger": "^3.1.1", "@types/koa-mount": "^4.0.0", - "@types/koa-router": "^7.4.2", + "@types/koa-router": "^7.4.4", "@types/koa-static": "^4.0.2", "@types/lodash.pick": "^4.4.6", "@types/node": "^16.3.1", diff --git a/packages/core/src/env/consts.ts b/packages/core/src/env/consts.ts index bcb91a874..750028e69 100644 --- a/packages/core/src/env/consts.ts +++ b/packages/core/src/env/consts.ts @@ -3,5 +3,4 @@ import { assertEnv, getEnv } from '@/utils/env'; export const signIn = assertEnv('UI_SIGN_IN_ROUTE'); export const isProduction = getEnv('NODE_ENV') === 'production'; export const port = Number(getEnv('PORT', '3001')); -export const oidcIssuer = getEnv('OIDC_ISSUER', `http://localhost:${port}/oidc`); export const mountedApps = Object.freeze(['api', 'oidc']); diff --git a/packages/core/src/middleware/koa-auth.ts b/packages/core/src/middleware/koa-auth.ts index eaab60ce4..6e7d091ab 100644 --- a/packages/core/src/middleware/koa-auth.ts +++ b/packages/core/src/middleware/koa-auth.ts @@ -1,15 +1,25 @@ import assert from 'assert'; import RequestError from '@/errors/RequestError'; -import { RequestErrorBody } from '@logto/schemas'; -import { Middleware } from 'koa'; +import { MiddlewareType } from 'koa'; +import { jwtVerify } from 'jose/jwt/verify'; +import { publicKey, issuer, adminResource } from '@/oidc/consts'; +import { IRouterParamContext } from 'koa-router'; +import { UserInfo, userInfoSelectFields } from '@logto/schemas'; +import { findUserById } from '@/queries/user'; +import pick from 'lodash.pick'; + +export type WithAuthContext = + ContextT & { + user: UserInfo; + }; const bearerToken = 'Bearer'; -export default function koaAuth(): Middleware< +export default function koaAuth< StateT, - ContextT, - RequestErrorBody -> { + ContextT extends IRouterParamContext, + ResponseBodyT +>(): MiddlewareType, ResponseBodyT> { return async (ctx, next) => { const { authorization } = ctx.request.headers; assert( @@ -23,6 +33,22 @@ export default function koaAuth(): Middleware< { supportedTypes: [bearerToken] } ) ); + const jwt = authorization.slice(bearerToken.length + 1); + + try { + const { + payload: { sub }, + } = await jwtVerify(jwt, publicKey, { + issuer, + audience: adminResource, + }); + assert(sub); + const user = await findUserById(sub); + ctx.user = pick(user, ...userInfoSelectFields); + } catch { + throw new RequestError({ code: 'auth.unauthorized', status: 401 }); + } + return next(); }; } diff --git a/packages/core/src/middleware/koa-guard.ts b/packages/core/src/middleware/koa-guard.ts index a397c8c3c..d5aae3f9c 100644 --- a/packages/core/src/middleware/koa-guard.ts +++ b/packages/core/src/middleware/koa-guard.ts @@ -17,7 +17,7 @@ export type Guarded = { params: ParametersT; }; -export type WithGuarded< +export type WithGuardedContext< ContextT extends IRouterParamContext, GuardQueryT, GuardBodyT, @@ -53,12 +53,12 @@ export default function koaGuard< params, }: GuardConfig): MiddlewareType< StateT, - WithGuarded, + WithGuardedContext, ResponseBodyT > { const guard: MiddlewareType< StateT, - WithGuarded, + WithGuardedContext, ResponseBodyT > = async (ctx, next) => { try { @@ -78,7 +78,7 @@ export default function koaGuard< const guardMiddleware: WithGuardConfig< MiddlewareType< StateT, - WithGuarded, + WithGuardedContext, ResponseBodyT > > = async function (ctx, next) { diff --git a/packages/core/src/oidc/consts.ts b/packages/core/src/oidc/consts.ts new file mode 100644 index 000000000..a4a41944b --- /dev/null +++ b/packages/core/src/oidc/consts.ts @@ -0,0 +1,11 @@ +import crypto from 'crypto'; +import { getEnv } from '@/utils/env'; +import { port } from '@/env/consts'; + +export const privateKey = crypto.createPrivateKey( + Buffer.from(getEnv('OIDC_PROVIDER_PRIVATE_KEY_BASE64'), 'base64') +); +export const publicKey = crypto.createPublicKey(privateKey); + +export const issuer = getEnv('OIDC_ISSUER', `http://localhost:${port}/oidc`); +export const adminResource = getEnv('ADMIN_RESOURCE', 'https://api.logto.io'); diff --git a/packages/core/src/oidc/init.ts b/packages/core/src/oidc/init.ts index e8b680945..08b7a8022 100644 --- a/packages/core/src/oidc/init.ts +++ b/packages/core/src/oidc/init.ts @@ -1,26 +1,21 @@ -import crypto from 'crypto'; import Koa from 'koa'; import mount from 'koa-mount'; import { Provider } from 'oidc-provider'; import postgresAdapter from '@/oidc/adapter'; import { fromKeyLike } from 'jose/jwk/from_key_like'; -import { getEnv } from '@/utils/env'; import { findUserById } from '@/queries/user'; -import { oidcIssuer } from '@/env/consts'; import { routes } from '@/routes/consts'; +import { issuer, privateKey } from './consts'; export default async function initOidc(app: Koa): Promise { - 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(oidcIssuer, { + const oidc = new Provider(issuer, { adapter: postgresAdapter, renderError: (ctx, out, error) => { console.log('OIDC error', error); @@ -39,6 +34,10 @@ export default async function initOidc(app: Koa): Promise { revocation: { enabled: true }, introspection: { enabled: true }, devInteractions: { enabled: false }, + resourceIndicators: { + enabled: true, + getResourceServerInfo: () => ({ scope: '', accessTokenFormat: 'jwt' }), + }, }, interactions: { url: (_, interaction) => { diff --git a/packages/core/src/routes/application.ts b/packages/core/src/routes/application.ts index 29d4ee9b1..fa916e838 100644 --- a/packages/core/src/routes/application.ts +++ b/packages/core/src/routes/application.ts @@ -2,10 +2,8 @@ import Router from 'koa-router'; import { nativeEnum, object, string } from 'zod'; import { ApplicationType } from '@logto/schemas'; import koaGuard from '@/middleware/koa-guard'; -import koaAuth from '@/middleware/koa-auth'; -export default function applicationRoutes(router: Router) { - router.use('/application', koaAuth()); +export default function applicationRoutes(router: Router) { router.post( '/application', koaGuard({ diff --git a/packages/core/src/routes/init.ts b/packages/core/src/routes/init.ts index 281e74d89..6b24cd228 100644 --- a/packages/core/src/routes/init.ts +++ b/packages/core/src/routes/init.ts @@ -5,22 +5,29 @@ import sessionRoutes from '@/routes/session'; import userRoutes from '@/routes/user'; import swaggerRoutes from '@/routes/swagger'; import mount from 'koa-mount'; -import applicationRoutes from './application'; +import koaAuth, { WithAuthContext } from '@/middleware/koa-auth'; +import applicationRoutes from '@/routes/application'; -const createRouter = (provider: Provider): Router => { - const router = new Router(); +const createRouters = (provider: Provider) => { + const anonymousRouter = new Router(); - sessionRoutes(router, provider); - userRoutes(router); + sessionRoutes(anonymousRouter, provider); + userRoutes(anonymousRouter); + swaggerRoutes(anonymousRouter); + + const router = new Router(); + router.use(koaAuth()); applicationRoutes(router); - swaggerRoutes(router); - return router; + return [anonymousRouter, router]; }; export default function initRouter(app: Koa, provider: Provider) { - const router = createRouter(provider); - const apisApp = new Koa().use(router.routes()).use(router.allowedMethods()); + const apisApp = new Koa(); + + for (const router of createRouters(provider)) { + apisApp.use(router.routes()).use(router.allowedMethods()); + } app.use(mount('/api', apisApp)); } diff --git a/packages/schemas/src/index.ts b/packages/schemas/src/index.ts index 1d7a54cb3..1ad4f4d41 100644 --- a/packages/schemas/src/index.ts +++ b/packages/schemas/src/index.ts @@ -1,3 +1,4 @@ export * from './foundations'; export * from './db-entries'; +export * from './types'; export * from './api'; diff --git a/packages/schemas/src/types/index.ts b/packages/schemas/src/types/index.ts new file mode 100644 index 000000000..e5abc8565 --- /dev/null +++ b/packages/schemas/src/types/index.ts @@ -0,0 +1 @@ +export * from './user'; diff --git a/packages/schemas/src/types/user.ts b/packages/schemas/src/types/user.ts new file mode 100644 index 000000000..b58c756b8 --- /dev/null +++ b/packages/schemas/src/types/user.ts @@ -0,0 +1,13 @@ +import { UserDBEntry } from '../db-entries'; + +export const userInfoSelectFields = Object.freeze([ + 'id', + 'username', + 'primaryEmail', + 'primaryPhone', +] as const); + +export type UserInfo = Pick< + UserDBEntry, + Keys +>; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a22e005e4..154ef1d5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,7 +26,7 @@ importers: '@types/koa': ^2.13.3 '@types/koa-logger': ^3.1.1 '@types/koa-mount': ^4.0.0 - '@types/koa-router': ^7.4.2 + '@types/koa-router': ^7.4.4 '@types/koa-static': ^4.0.2 '@types/lodash.pick': ^4.4.6 '@types/node': ^16.3.1