NestJS와cognito를 통해 JWT 인증을 실현한 샘플
73748 단어 AuthenticationJWTNestJSCognitotech
공식 사이트에서 인증에 관한 페이지는 다음과 같은 링크입니다.
목표
다음은 app 컨트롤러에 jwt 인증을 추가합니다.이 컨트롤러에 요청을 보내도 Authentication 헤더에 올바른 JWT를 추가하지 않으면 401 오류로 팝업됩니다.
app.controller.ts
@UseGuards(JwtGuard)
@Controller()
export class AppController {
@Get('hello')
testGet(): string {
return 'Hello!';
}
}
설치하다.
먼저 다음을 설치하십시오.
$ npm install --save @nestjs/passport passport jwks-rsa passport-jwt
$ npm install --save-dev @types/passport-jwt
@nestjs/passport
는 NestJS에서 여권 모듈을 처리하기 위한 것이다.jwks-rsa
cognito 서버에서 키를 가져오고 서명이 있는지 확인하는 데 사용됩니다.이곳 자체도 설치할 수 있으니 잠시 후 이 라이브러리를 사용하지 않는 방법을 소개하겠습니다.passport-jwt
JWT 정책을 작성하는 데 사용됩니다.만든 물건
이번에 만든 것은 주로 세 가지가 있다.
디렉토리 구성은 다음과 같습니다.
.
├──app.module.ts
├──app.controller.ts
└──auth
├──auth.module.ts
├──guard
│ └──jwt.guard.ts
└──strategy
└──jwt.strategy.ts
하나하나 설명하고 싶습니다.app.module.ts
루트 부분의 모듈입니다.이번에는 jwt 인증 코드가 없습니다.
app.module.ts
import {
Module,
NestModule,
MiddlewareConsumer,
RequestMethod,
} from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { AppController } from './app.controller';
@Module({
imports: [AuthModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
app.controller.ts
루트 부분의 컨트롤러.이 컨트롤러에 대해 JWT 인증을 하고 싶습니다.
app.controller.ts
import {
Controller,
Get,
UseGuards,
} from '@nestjs/common';
import { JwtGuard } from './auth/guard/jwt.guard';
// JWT認証をこのコントローラー全体に適応
@UseGuards(JwtGuard)
@Controller()
export class AppController {
@Get('hello')
testGet(): string {
return 'Hello!';
}
}
auth/auth.module.ts
드디어 메인 메뉴네요.auth 모듈.NestJS를 위한 맞춤형 NodeJS 대표 인증 라이브러리Passport.js
@nestjs/passport
를 사용합니다.auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './strategy/jwt.strategy';
// パターン2を使いたいときはコメントインする。
// import { ManualJwtStrategy } from './strategy/jwt.manual.strategy';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
],
providers: [
JwtStrategy,
// ManualJwtStrategy
],
})
export class AuthModule {}
auth/guard/jwt.guard.ts
NestJS를 JWT처럼 보호합니다.컨트롤러
@UseGuards
로 인테리어와 함께 사용하면 무효 인증을 받을 수 있다.auth/guard/jwt.guard.ts
import { AuthGuard } from '@nestjs/passport';
// AuthGuard('jwt')の引数の値は
// jwt.strategy.tsのPassportStrategy(Strategy, 'jwt')に合わせる。
export class JwtGuard extends AuthGuard('jwt') {
constructor() {
super();
}
}
[모드 1 라이브러리 검증]auth/stratgy/jwt.strategy.ts
는 JWT 인증 엔티티입니다.
passport-jwt
의Strategy
는 구조기가 지정한 secretOrKeyProvider
에 따라 JWT를 검사한다.유효한 JWTvalidate
만 디코딩하여 드립니다.잘못된 JWT 오류가 401개 반환됩니다.그런데 왜 401 오류에 대한 상세한 일지를 남기지 않았는지 영패가 좋지 않았는지, 다른 실시 방식이 좋지 않았는지 모르겠다.이런 상황에서 아래 모델 2의 실시 방식을 통해 디버깅이 더욱 쉬울 수 있다.
auth/strategy/jwt.strategy.ts
import {
Injectable,
Logger,
} from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { passportJwtSecret } from 'jwks-rsa';
import { ExtractJwt, Strategy } from 'passport-jwt';
export interface Claim {
sub: string;
email: string;
token_use: string;
auth_time: number;
iss: string;
exp: number;
username: string;
client_id: string;
}
const COGNITO_CLIENT_ID = 'hogehoge';
const COGNITO_REGION = 'ap-northeast-1';
const COGNITO_USERPOOL_ID = 'fugafuga';
const AUTHORITY = `https://cognito-idp.${COGNITO_REGION}.amazonaws.com/${COGNITO_USERPOOL_ID}`;
// PassportStrategy(Strategy, 'jwt')部分の第一引数は
// jwt.guard.tsでのAuthGuard('jwt')に合わせる。
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
private logger = new Logger(JwtStrategy.name);
constructor() {
super({
// ヘッダからBearerトークンを取得
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
// cognitoのクライアントidを指定
audience: COGNITO_CLIENT_ID,
// jwt発行者。今回はcognito
issuer: AUTHORITY,
algorithms: ['RS256'],
// もし自分がjwt発行してるなら秘密鍵を指定するが、
// cognitoなど外部サービスが発行してるならsecretOrKeyProviderを利用。
secretOrKeyProvider: passportJwtSecret({
// 公開鍵をキャッシュする。これがfalseだと、毎リクエストごとに
// 公開鍵をHTTPリクエストで取得する必要がある。
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `${AUTHORITY}/.well-known/jwks.json`,
}),
// passReqToCallback: true, // これをtrueにするとvalidateの第一引数にRequestを使用できる。
});
}
// JWT検証後、デコードされたpayloadを渡してくる。
// 検証後に実行されることに注意。JWTが向こうであればそもそも実行されない。
// validate自体はPromiseにすることも可能。
public validate(payload: Claim): string {
return payload.email;
}
}
[모드2 자체 검증]auth/stratgy/jwt.strategy.manual.ts
프로그램 라이브러리의 힘을 빌리지 않고 스스로 JWT 검증을 하는 방법.
먼저 다음을 설치하십시오.
$ npm install --save passport-custom jwk-to-pem jsonwebtoken
$ npm install --save-dev @types/jsonwebtoken @types/jwk-to-pem
auth/strategy/jwt.strategy.manual.tsimport {
UnauthorizedException,
Injectable,
Logger,
} from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
import { Strategy } from 'passport-custom'; // passport-jwtでないので注意!
import { handler } from './jwt.verify';
// もしこれを利用したい場合は
// jwt.guard.tsのAuthGuard('jwt')を
// AuthGuard('manualJwt')にすること。
@Injectable()
export class ManualJwtStrategy extends PassportStrategy(Strategy, 'manualJwt') {
private logger = new Logger(ManualJwtStrategy.name);
constructor() {
super();
}
public async validate(req: Request): Promise<string> {
const info = await handler(req.get('Authorization'));
if (!info.isValid) throw new UnauthorizedException(info.error);
return info.email;
}
}
제가'passport-jwt'를 잘 하도록 노력하겠습니다Strategy
.하는 일Authentication
헤더에서 영패를 받습니다.그때는 아주 가벼운 문자로 발리를 세었다.그리고 이걸 쓰고 이런 도서관을 발견했어요.
auth/strategy/jwt.verify.ts
/* eslint-disable camelcase */
import { UnauthorizedException, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import jwkToPem from 'jwk-to-pem';
import request from 'request';
const logger = new Logger('jwt.verify');
// 参考 https://github.com/awslabs/aws-support-tools/blob/master/Cognito/decode-verify-jwt/decode-verify-jwt.ts
const TOKEN_USE_ACCESS = 'access';
const TOKEN_USE_ID = 'id';
const ALLOWED_TOKEN_USES = [TOKEN_USE_ACCESS, TOKEN_USE_ID];
const ISSUER = `https://cognito-idp.${process.env.AWS_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}`;
export interface ClaimVerifyRequest {
readonly token?: string;
}
export interface ClaimVerifyResult {
readonly email: string;
readonly clientId: string;
readonly isValid: boolean;
readonly error?: any;
}
interface TokenHeader {
kid: string;
alg: string;
}
interface PublicKey {
alg: string;
e: string;
kid: string;
kty: 'RSA'; // サンプルはstringだったがjwtToPemの型で弾かれるため、こうした
n: string;
use: string;
}
interface PublicKeyMeta {
instance: PublicKey;
pem: string;
}
interface PublicKeys {
keys: PublicKey[];
}
interface MapOfKidToPublicKey {
[key: string]: PublicKeyMeta;
}
export interface Claim {
sub: string;
email: string;
token_use: string;
auth_time: number;
iss: string;
exp: number;
username: string;
client_id: string;
'cognito:username': string;
}
/** レスポンスの例
"sub": "aaaaaaaa-bbbb-cccc-dddd-example",
"aud": "xxxxxxxxxxxxexample",
"email_verified": true,
"token_use": "id",
"auth_time": 1500009400,
"iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/your_userpool_id",
"cognito:username": "HATOSUKE",
"exp": 1500013000,
"given_name": "HATOSUKE",
"iat": 1500009400,
"email": "[email protected]"
*/
let cacheKeys: MapOfKidToPublicKey | undefined;
// One time initialisation to download the JWK keys and convert to PEM format. Returns a promise.
const getPublicKeys = async (): Promise<MapOfKidToPublicKey> => {
if (!cacheKeys) {
const options = {
url: `${ISSUER}/.well-known/jwks.json`,
json: true,
};
const publicKeys = await new Promise<PublicKeys>((resolve, reject) => {
request.get(options, function (err, resp, body: PublicKeys) {
if (err) {
logger.debug(`Failed to download JWKS data. err: ${err}`);
reject(new Error('Internal debug occurred downloading JWKS data.')); // don't return detailed info to the caller
return;
}
if (!body || !body.keys) {
logger.debug(
`JWKS data is not in expected format. Response was: ${JSON.stringify(
resp,
)}`,
);
reject(new Error('Internal debug occurred downloading JWKS data.')); // don't return detailed info to the caller
return;
}
resolve(body);
});
});
cacheKeys = publicKeys.keys.reduce((agg, current) => {
const pem = jwkToPem(current);
agg[current.kid] = { instance: current, pem };
return agg;
}, {} as MapOfKidToPublicKey);
return cacheKeys;
}
return cacheKeys;
};
// Verify the Authorization header and return a promise.
function verifyProm(keys: MapOfKidToPublicKey, token?: string): Promise<Claim> {
return new Promise((resolve, reject) => {
// Decode the JWT token so we can match it to a key to verify it against
if (!token || token.length < 2) {
reject(
new UnauthorizedException(
"Invalid or missing Authorization header. Expected to be in the format 'Bearer <your_JWT_token>'.",
),
);
return;
}
const decodedNotVerified = jwt.decode(token, {
complete: true,
}) as { header: TokenHeader } | null;
if (!decodedNotVerified) {
logger.debug('Invalid JWT token. jwt.decode() failure.');
reject(
new UnauthorizedException(
'Authorization header contains an invalid JWT token.',
),
); // don't return detailed info to the caller
return;
}
if (
!decodedNotVerified.header.kid ||
!keys[decodedNotVerified.header.kid]
) {
logger.debug(
`Invalid JWT token. Expected a known KID ${JSON.stringify(
Object.keys(keys),
)} but found ${decodedNotVerified.header.kid}.`,
);
reject(
new UnauthorizedException(
'Authorization header contains an invalid JWT token.',
),
); // don't return detailed info to the caller
return;
}
const key = keys[decodedNotVerified.header.kid];
// Now verify the JWT signature matches the relevant key
jwt.verify(
token,
key.pem,
{
issuer: ISSUER,
},
function (err, decodedAndVerified: any) {
if (err) {
logger.debug(`Invalid JWT token. jwt.verify() failed: ${err}.`);
if (err instanceof jwt.TokenExpiredError) {
reject(
new UnauthorizedException(
`Authorization header contains a JWT token that expired at ${err.expiredAt.toISOString()}.`,
),
);
} else {
reject(
new UnauthorizedException(
'Authorization header contains an invalid JWT token.',
),
); // don't return detailed info to the caller
}
return;
}
// The signature matches so we know the JWT token came from our Cognito instance, now just verify the remaining claims in the token
// Verify the token_use matches what we've been configured to allow
if (ALLOWED_TOKEN_USES.indexOf(decodedAndVerified.token_use) === -1) {
logger.debug(
`Invalid JWT token. Expected token_use to be ${JSON.stringify(
ALLOWED_TOKEN_USES,
)} but found ${decodedAndVerified.token_use}.`,
);
reject(
new UnauthorizedException(
'Authorization header contains an invalid JWT token.',
),
); // don't return detailed info to the caller
return;
}
// Verify the client id matches what we expect. Will be in either the aud or the client_id claim depending on whether it's an id or access token.
const clientId = decodedAndVerified.aud || decodedAndVerified.client_id;
if (clientId !== process.env.COGNITO_CLIENT_ID) {
logger.debug(
`Invalid JWT token. Expected client id to be ${process.env.COGNITO_CLIENT_ID} but found ${clientId}.`,
);
reject(
new UnauthorizedException(
'Authorization header contains an invalid JWT token.',
),
); // don't return detailed info to the caller
return;
}
// Done - all JWT token claims can now be trusted
resolve(decodedAndVerified as Claim);
},
);
});
}
// Verify the Authorization header and call the next middleware handler if appropriate
function verifyMiddleWare(
pemsDownloadProm: Promise<MapOfKidToPublicKey>,
token?: string,
) {
return pemsDownloadProm
.then((keys) => {
return verifyProm(keys, token);
})
.then((decoded: Claim) => {
// Caller is authorised - copy some useful attributes into the req object for later use
logger.debug(`Valid JWT token. Decoded: ${JSON.stringify(decoded)}.`);
return {
email: decoded.email,
clientId: decoded.client_id,
isValid: true,
};
})
.catch((err: any) => {
logger.debug(String(err));
return { email: '', clientId: '', error: err, isValid: false };
});
}
// Get the middleware function that will verify the incoming request
const handler = async (auth?: string): Promise<ClaimVerifyResult> => {
// Fetch the JWKS data used to verify the signature of incoming JWT tokens
// Check the format of the auth header string and break out the JWT token part
if (!auth || auth.length < 10) {
throw new UnauthorizedException(
"Invalid or missing Authorization header. Expected to be in the format 'Bearer <your_JWT_token>'.",
);
}
const authPrefix = auth.substring(0, 7).toLowerCase();
if (authPrefix !== 'bearer ') {
throw new UnauthorizedException(
"Authorization header is expected to be in the format 'Bearer <your_JWT_token>'.",
);
}
const token = auth.substring(7);
const pemsDownloadProm = getPublicKeys().catch((err) => {
// Failed to get the JWKS data - all subsequent auth requests will fail
logger.debug(err);
return { err };
});
return verifyMiddleWare(pemsDownloadProm, token);
};
export { handler };
Reference
이 문제에 관하여(NestJS와cognito를 통해 JWT 인증을 실현한 샘플), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/dove/articles/d45f18f6c50f10텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)