@point3/logto-module
Version:
포인트3 내부 logto Authentication 모듈입니다
92 lines (77 loc) • 3.81 kB
text/typescript
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { jwtVerify, createRemoteJWKSet } from "jose";
import { LogtoVerifierConfig } from "../client/config";
import * as token from "./access-token";
export const LogtoTokenVerifierToken = Symbol.for("LogtoTokenVerifier");
()
export class LogtoTokenVerifier {
constructor(private readonly config: LogtoVerifierConfig) { }
/**
* 토큰을 검증하고 필요에 따라 필수 스코프와 역할을 확인합니다.
* @param token 검증할 토큰입니다.
* @param requiredScopes 선택적으로 확인할 스코프입니다.
* @param requiredRoles 선택적으로 확인할 역할입니다.
* @returns 토큰이 유효한 경우 토큰 페이로드를 반환하는 Promise입니다.
* @throws UnauthorizedException 토큰이 유효하지 않은 경우 발생합니다.
*/
public async verifyToken(token: string): Promise<token.AccessTokenPayload>;
public async verifyToken(token: string, requiredScopes: string[], requiredRoles: string[]): Promise<token.AccessTokenPayload>;
public async verifyToken(token: string, requiredScopes?: string[], requiredRoles?: string[]): Promise<token.AccessTokenPayload> {
if (!token) throw new UnauthorizedException('엑세스 토큰이 존재하지 않습니다.');
const { payload } = await jwtVerify(
token, createRemoteJWKSet(new URL(this.config.jwksUri)),
{ issuer: this.config.issuer }
);
const tokenPayload = payload as token.AccessTokenPayload;
if (requiredScopes || requiredRoles) {
this.shouldContainRequiredPrivileges(
tokenPayload, requiredScopes, requiredRoles);
}
return tokenPayload;
}
/**
* id token을 검증합니다.
* @param token id token 문자열입니다.
* @returns id token 페이로드입니다.
*/
public async verifyIdToken(token: string): Promise<token.IdTokenPayload> {
const { payload } = await jwtVerify(
token,
createRemoteJWKSet(new URL(this.config.jwksUri)),
{ issuer: this.config.issuer }
);
return payload as token.IdTokenPayload;
}
/**
* 토큰 페이로드를 통해 필요한 스코프와 역할 등 추가적인 검사를 수행합니다.
* @param payload 토큰 페이로드입니다.
* @param requiredScopes 선택적으로 확인할 스코프입니다.
* @param requiredRoles 선택적으로 확인할 역할입니다.
*/
private shouldContainRequiredPrivileges(
payload: token.AccessTokenPayload,
requiredScopes?: string[],
requiredRoles?: string[]
): void {
const { userScopes, userRoles } = payload;
const scopes = userScopes?.flat() ?? [];
if (this.hasInsufficientScopes(requiredScopes, scopes)) {
throw new UnauthorizedException(
{ code: 'auth.insufficient_scope', status: 403 },
{ cause: requiredScopes }
);
}
if (this.hasInsufficientRoles(requiredRoles, userRoles)) {
throw new UnauthorizedException(
{ code: 'auth.role_mismatch', status: 403 },
{ cause: requiredRoles }
);
}
}
private hasInsufficientScopes(requiredScopes: string[] | undefined, userScopes: string[]): boolean {
return !!(requiredScopes && requiredScopes.length > 0 && !requiredScopes.every(scope => userScopes.includes(scope)));
}
private hasInsufficientRoles(requiredRoles: string[] | undefined, userRoles: string[]): boolean {
return !!(requiredRoles && requiredRoles.length > 0 && !requiredRoles.some(role => userRoles.includes(role)));
}
}