0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

Merge pull request #81 from logto-io/gao-log-11

feat: validate access token if needed
This commit is contained in:
Gao Sun 2021-08-16 23:11:34 +08:00 committed by GitHub
commit 942ccae25b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 87 additions and 32 deletions

View file

@ -46,7 +46,7 @@
"@types/koa": "^2.13.3", "@types/koa": "^2.13.3",
"@types/koa-logger": "^3.1.1", "@types/koa-logger": "^3.1.1",
"@types/koa-mount": "^4.0.0", "@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/koa-static": "^4.0.2",
"@types/lodash.pick": "^4.4.6", "@types/lodash.pick": "^4.4.6",
"@types/node": "^16.3.1", "@types/node": "^16.3.1",

View file

@ -3,5 +3,4 @@ import { assertEnv, getEnv } from '@/utils/env';
export const signIn = assertEnv('UI_SIGN_IN_ROUTE'); export const signIn = assertEnv('UI_SIGN_IN_ROUTE');
export const isProduction = getEnv('NODE_ENV') === 'production'; export const isProduction = getEnv('NODE_ENV') === 'production';
export const port = Number(getEnv('PORT', '3001')); export const port = Number(getEnv('PORT', '3001'));
export const oidcIssuer = getEnv('OIDC_ISSUER', `http://localhost:${port}/oidc`);
export const mountedApps = Object.freeze(['api', 'oidc']); export const mountedApps = Object.freeze(['api', 'oidc']);

View file

@ -1,15 +1,25 @@
import assert from 'assert'; import assert from 'assert';
import RequestError from '@/errors/RequestError'; import RequestError from '@/errors/RequestError';
import { RequestErrorBody } from '@logto/schemas'; import { MiddlewareType } from 'koa';
import { Middleware } 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 extends IRouterParamContext = IRouterParamContext> =
ContextT & {
user: UserInfo;
};
const bearerToken = 'Bearer'; const bearerToken = 'Bearer';
export default function koaAuth<StateT, ContextT>(): Middleware< export default function koaAuth<
StateT, StateT,
ContextT, ContextT extends IRouterParamContext,
RequestErrorBody ResponseBodyT
> { >(): MiddlewareType<StateT, WithAuthContext<ContextT>, ResponseBodyT> {
return async (ctx, next) => { return async (ctx, next) => {
const { authorization } = ctx.request.headers; const { authorization } = ctx.request.headers;
assert( assert(
@ -23,6 +33,22 @@ export default function koaAuth<StateT, ContextT>(): Middleware<
{ supportedTypes: [bearerToken] } { 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(); return next();
}; };
} }

View file

@ -17,7 +17,7 @@ export type Guarded<QueryT, BodyT, ParametersT> = {
params: ParametersT; params: ParametersT;
}; };
export type WithGuarded< export type WithGuardedContext<
ContextT extends IRouterParamContext, ContextT extends IRouterParamContext,
GuardQueryT, GuardQueryT,
GuardBodyT, GuardBodyT,
@ -53,12 +53,12 @@ export default function koaGuard<
params, params,
}: GuardConfig<GuardQueryT, GuardBodyT, GuardParametersT>): MiddlewareType< }: GuardConfig<GuardQueryT, GuardBodyT, GuardParametersT>): MiddlewareType<
StateT, StateT,
WithGuarded<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>, WithGuardedContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
ResponseBodyT ResponseBodyT
> { > {
const guard: MiddlewareType< const guard: MiddlewareType<
StateT, StateT,
WithGuarded<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>, WithGuardedContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
ResponseBodyT ResponseBodyT
> = async (ctx, next) => { > = async (ctx, next) => {
try { try {
@ -78,7 +78,7 @@ export default function koaGuard<
const guardMiddleware: WithGuardConfig< const guardMiddleware: WithGuardConfig<
MiddlewareType< MiddlewareType<
StateT, StateT,
WithGuarded<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>, WithGuardedContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
ResponseBodyT ResponseBodyT
> >
> = async function (ctx, next) { > = async function (ctx, next) {

View file

@ -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');

View file

@ -1,26 +1,21 @@
import crypto from 'crypto';
import Koa from 'koa'; import Koa from 'koa';
import mount from 'koa-mount'; import mount from 'koa-mount';
import { Provider } from 'oidc-provider'; import { Provider } from 'oidc-provider';
import postgresAdapter from '@/oidc/adapter'; import postgresAdapter from '@/oidc/adapter';
import { fromKeyLike } from 'jose/jwk/from_key_like'; import { fromKeyLike } from 'jose/jwk/from_key_like';
import { getEnv } from '@/utils/env';
import { findUserById } from '@/queries/user'; import { findUserById } from '@/queries/user';
import { oidcIssuer } from '@/env/consts';
import { routes } from '@/routes/consts'; import { routes } from '@/routes/consts';
import { issuer, privateKey } from './consts';
export default async function initOidc(app: Koa): Promise<Provider> { export default async function initOidc(app: Koa): Promise<Provider> {
const privateKey = crypto.createPrivateKey(
Buffer.from(getEnv('OIDC_PROVIDER_PRIVATE_KEY_BASE64'), 'base64')
);
const keys = [await fromKeyLike(privateKey)]; const keys = [await fromKeyLike(privateKey)];
const cookieConfig = Object.freeze({ const cookieConfig = Object.freeze({
sameSite: 'lax', sameSite: 'lax',
path: '/', path: '/',
signed: true, signed: true,
} as const); } as const);
const oidc = new Provider(oidcIssuer, { const oidc = new Provider(issuer, {
adapter: postgresAdapter, adapter: postgresAdapter,
renderError: (ctx, out, error) => { renderError: (ctx, out, error) => {
console.log('OIDC error', error); console.log('OIDC error', error);
@ -39,6 +34,10 @@ export default async function initOidc(app: Koa): Promise<Provider> {
revocation: { enabled: true }, revocation: { enabled: true },
introspection: { enabled: true }, introspection: { enabled: true },
devInteractions: { enabled: false }, devInteractions: { enabled: false },
resourceIndicators: {
enabled: true,
getResourceServerInfo: () => ({ scope: '', accessTokenFormat: 'jwt' }),
},
}, },
interactions: { interactions: {
url: (_, interaction) => { url: (_, interaction) => {

View file

@ -2,10 +2,8 @@ import Router from 'koa-router';
import { nativeEnum, object, string } from 'zod'; import { nativeEnum, object, string } from 'zod';
import { ApplicationType } from '@logto/schemas'; import { ApplicationType } from '@logto/schemas';
import koaGuard from '@/middleware/koa-guard'; import koaGuard from '@/middleware/koa-guard';
import koaAuth from '@/middleware/koa-auth';
export default function applicationRoutes(router: Router) { export default function applicationRoutes<StateT, ContextT>(router: Router<StateT, ContextT>) {
router.use('/application', koaAuth());
router.post( router.post(
'/application', '/application',
koaGuard({ koaGuard({

View file

@ -5,22 +5,29 @@ import sessionRoutes from '@/routes/session';
import userRoutes from '@/routes/user'; import userRoutes from '@/routes/user';
import swaggerRoutes from '@/routes/swagger'; import swaggerRoutes from '@/routes/swagger';
import mount from 'koa-mount'; 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 createRouters = (provider: Provider) => {
const router = new Router(); const anonymousRouter = new Router();
sessionRoutes(router, provider); sessionRoutes(anonymousRouter, provider);
userRoutes(router); userRoutes(anonymousRouter);
swaggerRoutes(anonymousRouter);
const router = new Router<unknown, WithAuthContext>();
router.use(koaAuth());
applicationRoutes(router); applicationRoutes(router);
swaggerRoutes(router);
return router; return [anonymousRouter, router];
}; };
export default function initRouter(app: Koa, provider: Provider) { export default function initRouter(app: Koa, provider: Provider) {
const router = createRouter(provider); const apisApp = new Koa();
const apisApp = new Koa().use(router.routes()).use(router.allowedMethods());
for (const router of createRouters(provider)) {
apisApp.use(router.routes()).use(router.allowedMethods());
}
app.use(mount('/api', apisApp)); app.use(mount('/api', apisApp));
} }

View file

@ -1,3 +1,4 @@
export * from './foundations'; export * from './foundations';
export * from './db-entries'; export * from './db-entries';
export * from './types';
export * from './api'; export * from './api';

View file

@ -0,0 +1 @@
export * from './user';

View file

@ -0,0 +1,13 @@
import { UserDBEntry } from '../db-entries';
export const userInfoSelectFields = Object.freeze([
'id',
'username',
'primaryEmail',
'primaryPhone',
] as const);
export type UserInfo<Keys extends keyof UserDBEntry = typeof userInfoSelectFields[number]> = Pick<
UserDBEntry,
Keys
>;

View file

@ -26,7 +26,7 @@ importers:
'@types/koa': ^2.13.3 '@types/koa': ^2.13.3
'@types/koa-logger': ^3.1.1 '@types/koa-logger': ^3.1.1
'@types/koa-mount': ^4.0.0 '@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/koa-static': ^4.0.2
'@types/lodash.pick': ^4.4.6 '@types/lodash.pick': ^4.4.6
'@types/node': ^16.3.1 '@types/node': ^16.3.1