mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
feat(core): hasura authn (#1790)
* feat(core): hasura authn * refactor(core): fix test error
This commit is contained in:
parent
213df4e1be
commit
87d3a53b65
9 changed files with 90 additions and 28 deletions
|
@ -185,6 +185,8 @@ describe('koaAuth middleware', () => {
|
|||
},
|
||||
};
|
||||
|
||||
await expect(koaAuth()(ctx, next)).rejects.toMatchError(unauthorizedError);
|
||||
await expect(koaAuth()(ctx, next)).rejects.toMatchError(
|
||||
new RequestError({ code: 'auth.unauthorized', status: 401 }, new Error('unknown error'))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -34,12 +34,15 @@ const extractBearerTokenFromHeaders = ({ authorization }: IncomingHttpHeaders) =
|
|||
return authorization.slice(bearerTokenIdentifier.length + 1);
|
||||
};
|
||||
|
||||
type UserInfo = {
|
||||
type TokenInfo = {
|
||||
sub: string;
|
||||
roleNames?: string[];
|
||||
};
|
||||
|
||||
const getUserInfoFromRequest = async (request: Request): Promise<UserInfo> => {
|
||||
export const verifyBearerTokenFromRequest = async (
|
||||
request: Request,
|
||||
resourceIndicator = managementResource.indicator
|
||||
): Promise<TokenInfo> => {
|
||||
const { isProduction, isIntegrationTest, developmentUserId, oidc } = envSet.values;
|
||||
const userId = request.headers['development-user-id']?.toString() ?? developmentUserId;
|
||||
|
||||
|
@ -47,41 +50,42 @@ const getUserInfoFromRequest = async (request: Request): Promise<UserInfo> => {
|
|||
return { sub: userId, roleNames: [UserRole.Admin] };
|
||||
}
|
||||
|
||||
const { localJWKSet, issuer } = oidc;
|
||||
const {
|
||||
payload: { sub, role_names: roleNames },
|
||||
} = await jwtVerify(extractBearerTokenFromHeaders(request.headers), localJWKSet, {
|
||||
issuer,
|
||||
audience: managementResource.indicator,
|
||||
});
|
||||
try {
|
||||
const { localJWKSet, issuer } = oidc;
|
||||
const {
|
||||
payload: { sub, role_names: roleNames },
|
||||
} = await jwtVerify(extractBearerTokenFromHeaders(request.headers), localJWKSet, {
|
||||
issuer,
|
||||
audience: resourceIndicator,
|
||||
});
|
||||
|
||||
assertThat(sub, new RequestError({ code: 'auth.jwt_sub_missing', status: 401 }));
|
||||
assertThat(sub, new RequestError({ code: 'auth.jwt_sub_missing', status: 401 }));
|
||||
|
||||
return { sub, roleNames: conditional(Array.isArray(roleNames) && roleNames) };
|
||||
return { sub, roleNames: conditional(Array.isArray(roleNames) && roleNames) };
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof RequestError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new RequestError({ code: 'auth.unauthorized', status: 401 }, error);
|
||||
}
|
||||
};
|
||||
|
||||
export default function koaAuth<StateT, ContextT extends IRouterParamContext, ResponseBodyT>(
|
||||
forRole?: UserRole
|
||||
): MiddlewareType<StateT, WithAuthContext<ContextT>, ResponseBodyT> {
|
||||
return async (ctx, next) => {
|
||||
try {
|
||||
const { sub, roleNames } = await getUserInfoFromRequest(ctx.request);
|
||||
const { sub, roleNames } = await verifyBearerTokenFromRequest(ctx.request);
|
||||
|
||||
if (forRole) {
|
||||
assertThat(
|
||||
roleNames?.includes(forRole),
|
||||
new RequestError({ code: 'auth.forbidden', status: 403 })
|
||||
);
|
||||
}
|
||||
|
||||
ctx.auth = sub;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof RequestError) {
|
||||
throw error;
|
||||
}
|
||||
throw new RequestError({ code: 'auth.unauthorized', status: 401 });
|
||||
if (forRole) {
|
||||
assertThat(
|
||||
roleNames?.includes(forRole),
|
||||
new RequestError({ code: 'auth.forbidden', status: 403 })
|
||||
);
|
||||
}
|
||||
|
||||
ctx.auth = sub;
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
|
45
packages/core/src/routes/authn.ts
Normal file
45
packages/core/src/routes/authn.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import koaAuth, { verifyBearerTokenFromRequest } from '@/middleware/koa-auth';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
import { AnonymousRouter } from './types';
|
||||
|
||||
/**
|
||||
* Authn stands for authentication.
|
||||
* This router will have a route `/authn` to authenticate tokens with a general manner.
|
||||
* For now, we only implement the API for Hasura authentication.
|
||||
*/
|
||||
export default function authnRoutes<T extends AnonymousRouter>(router: T) {
|
||||
router.get(
|
||||
'/authn/hasura',
|
||||
koaGuard({
|
||||
query: z.object({ resource: z.string().min(1) }),
|
||||
status: [200, 401],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const expectedRole = ctx.headers['expected-role']?.toString();
|
||||
const { sub, roleNames } = await verifyBearerTokenFromRequest(
|
||||
ctx.request,
|
||||
ctx.guard.query.resource
|
||||
);
|
||||
|
||||
if (expectedRole) {
|
||||
assertThat(
|
||||
roleNames?.includes(expectedRole),
|
||||
new RequestError({ code: 'auth.expected_role_not_found', status: 401 })
|
||||
);
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
'X-Hasura-User-Id': sub,
|
||||
'X-Hasura-Role': expectedRole,
|
||||
};
|
||||
ctx.status = 200;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -8,6 +8,7 @@ import koaAuth from '@/middleware/koa-auth';
|
|||
import koaLogSession from '@/middleware/koa-log-session';
|
||||
import adminUserRoutes from '@/routes/admin-user';
|
||||
import applicationRoutes from '@/routes/application';
|
||||
import authnRoutes from '@/routes/authn';
|
||||
import connectorRoutes from '@/routes/connector';
|
||||
import dashboardRoutes from '@/routes/dashboard';
|
||||
import logRoutes from '@/routes/log';
|
||||
|
@ -46,6 +47,7 @@ const createRouters = (provider: Provider) => {
|
|||
const anonymousRouter: AnonymousRouter = new Router();
|
||||
wellKnownRoutes(anonymousRouter, provider);
|
||||
statusRoutes(anonymousRouter);
|
||||
authnRoutes(anonymousRouter);
|
||||
// The swagger.json should contain all API routers.
|
||||
swaggerRoutes(anonymousRouter, [sessionRouter, managementRouter, anonymousRouter]);
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ const errors = {
|
|||
authorization_token_type_not_supported: 'Authorization type is not supported.',
|
||||
unauthorized: 'Unauthorized. Please check credentials and its scope.',
|
||||
forbidden: 'Forbidden. Please check your user roles and permissions.',
|
||||
expected_role_not_found:
|
||||
'Expected role not found. Please check your user roles and permissions.',
|
||||
jwt_sub_missing: 'Missing `sub` in JWT.',
|
||||
},
|
||||
guard: {
|
||||
|
|
|
@ -5,6 +5,8 @@ const errors = {
|
|||
unauthorized:
|
||||
"Non autorisé. Veuillez vérifier les informations d'identification et son champ d'application.",
|
||||
forbidden: "Interdit. Veuillez vérifier vos rôles et autorisations d'utilisateur.",
|
||||
expected_role_not_found:
|
||||
'Expected role not found. Please check your user roles and permissions.',
|
||||
jwt_sub_missing: '`sub` manquant dans JWT.',
|
||||
},
|
||||
guard: {
|
||||
|
|
|
@ -4,6 +4,8 @@ const errors = {
|
|||
authorization_token_type_not_supported: '해당 인증 방법을 지원하지 않아요.',
|
||||
unauthorized: '인증되지 않았어요. 로그인 정보와 범위를 확인해주세요.',
|
||||
forbidden: '접근이 금지되었어요. 로그인 권한와 직책을 확인해주세요.',
|
||||
expected_role_not_found:
|
||||
'Expected role not found. Please check your user roles and permissions.',
|
||||
jwt_sub_missing: 'JWT에서 `sub`를 찾을 수 없어요.',
|
||||
},
|
||||
guard: {
|
||||
|
|
|
@ -4,6 +4,8 @@ const errors = {
|
|||
authorization_token_type_not_supported: 'Yetkilendirme tipi desteklenmiyor.',
|
||||
unauthorized: 'Yetki yok. Lütfen kimlik bilgilerini ve kapsamını kontrol edin.',
|
||||
forbidden: 'Yasak. Lütfen kullanıcı rollerinizi ve izinlerinizi kontrol edin.',
|
||||
expected_role_not_found:
|
||||
'Expected role not found. Please check your user roles and permissions.',
|
||||
jwt_sub_missing: 'JWTde `sub` eksik.',
|
||||
},
|
||||
guard: {
|
||||
|
|
|
@ -3,7 +3,8 @@ const errors = {
|
|||
authorization_header_missing: '缺少权限标题',
|
||||
authorization_token_type_not_supported: '权限类型不支持',
|
||||
unauthorized: '未经授权。请检查凭据及其范围。',
|
||||
forbidden: '禁止访问。请检查用户权限。',
|
||||
forbidden: '禁止访问。请检查用户 role 与权限。',
|
||||
expected_role_not_found: '未找到期望的 role。请检查用户 role 与权限。',
|
||||
jwt_sub_missing: 'JWT 缺失 `sub`',
|
||||
},
|
||||
guard: {
|
||||
|
|
Loading…
Add table
Reference in a new issue