0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat: validate access token if needed

This commit is contained in:
Gao Sun 2021-08-15 23:39:03 +08:00
parent 0704d21601
commit acd8157a0d
No known key found for this signature in database
GPG key ID: 0F0EFA2E36639F31
12 changed files with 87 additions and 32 deletions

View file

@ -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",

View file

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

View file

@ -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 extends IRouterParamContext = IRouterParamContext> =
ContextT & {
user: UserInfo;
};
const bearerToken = 'Bearer';
export default function koaAuth<StateT, ContextT>(): Middleware<
export default function koaAuth<
StateT,
ContextT,
RequestErrorBody
> {
ContextT extends IRouterParamContext,
ResponseBodyT
>(): MiddlewareType<StateT, WithAuthContext<ContextT>, ResponseBodyT> {
return async (ctx, next) => {
const { authorization } = ctx.request.headers;
assert(
@ -23,6 +33,22 @@ export default function koaAuth<StateT, ContextT>(): 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();
};
}

View file

@ -17,7 +17,7 @@ export type Guarded<QueryT, BodyT, ParametersT> = {
params: ParametersT;
};
export type WithGuarded<
export type WithGuardedContext<
ContextT extends IRouterParamContext,
GuardQueryT,
GuardBodyT,
@ -53,12 +53,12 @@ export default function koaGuard<
params,
}: GuardConfig<GuardQueryT, GuardBodyT, GuardParametersT>): MiddlewareType<
StateT,
WithGuarded<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
WithGuardedContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
ResponseBodyT
> {
const guard: MiddlewareType<
StateT,
WithGuarded<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
WithGuardedContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
ResponseBodyT
> = async (ctx, next) => {
try {
@ -78,7 +78,7 @@ export default function koaGuard<
const guardMiddleware: WithGuardConfig<
MiddlewareType<
StateT,
WithGuarded<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
WithGuardedContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
ResponseBodyT
>
> = 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 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<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(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<Provider> {
revocation: { enabled: true },
introspection: { enabled: true },
devInteractions: { enabled: false },
resourceIndicators: {
enabled: true,
getResourceServerInfo: () => ({ scope: '', accessTokenFormat: 'jwt' }),
},
},
interactions: {
url: (_, interaction) => {

View file

@ -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<StateT, ContextT>(router: Router<StateT, ContextT>) {
router.post(
'/application',
koaGuard({

View file

@ -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<unknown, WithAuthContext>();
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));
}

View file

@ -1,3 +1,4 @@
export * from './foundations';
export * from './db-entries';
export * from './types';
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-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