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:
parent
0704d21601
commit
acd8157a0d
12 changed files with 87 additions and 32 deletions
|
@ -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",
|
||||
|
|
1
packages/core/src/env/consts.ts
vendored
1
packages/core/src/env/consts.ts
vendored
|
@ -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']);
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
11
packages/core/src/oidc/consts.ts
Normal file
11
packages/core/src/oidc/consts.ts
Normal 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');
|
|
@ -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) => {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './foundations';
|
||||
export * from './db-entries';
|
||||
export * from './types';
|
||||
export * from './api';
|
||||
|
|
1
packages/schemas/src/types/index.ts
Normal file
1
packages/schemas/src/types/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './user';
|
13
packages/schemas/src/types/user.ts
Normal file
13
packages/schemas/src/types/user.ts
Normal 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
|
||||
>;
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue