0
Fork 0
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:
Gao Sun 2022-08-18 22:10:17 +08:00 committed by GitHub
parent 213df4e1be
commit 87d3a53b65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 90 additions and 28 deletions

View file

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

View file

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

View 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();
}
);
}

View file

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

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {