ClientAssertion이란 클라이언트 인증을 위해 필요한 Signed JWT 입니다. IDV Server는 이 Token을 검증하여 정상적인 Client임을 확인합니다. ClientAssertion는 privateKey로 JWTpayload를 서명하여 생성합니다. 이를 위해 JWT payload 생성 과정, PrivateKey 생성, PrivateKey를 이용한 JWT payload 서명 과정이 필요합니다.
JWT payload를 아래와 같이 만듭니다. 이후, privateKey를 생성하여 이 payload에 서명(signJwt(privateKey, payload)) 합니다.
{
iss: "console에서 발급받은 client_id",
sub: "console에서 발급받은 client_id",
aud: "Access token을 발급 받을 때 요청하는 endpoint url",
iat: "Token 생성 시각",
exp: "Token 만료 시각",
jti: "재사용 방지를 위한 Nonce(난수) 값"
}
// git: idv-client-server
// src/app.service.ts
export class AppService {
async issueClientCredentialsToken(): Promise<IssueAccessTokenResult> {
...
const assertion = this.createClientAssertion(
privateKey,
TOMO_IDV_CLIENT_ID,
`${baseUrl}/v1/oauth2/token`,
);
...
}
private createClientAssertion(privateKey: KeyObject, clientId: string, audience: string): string {
const now = Math.floor(Date.now() / 1000);
const jti = crypto.randomUUID();
const payload = {
iss: clientId,
sub: clientId,
aud: audience,
iat: now,
exp: now + 300,
jti: jti,
};
return this.signJwt(privateKey, payload);
}
...
}
privateKey는 console에서 발급하는 secret key를 통해 얻을 수 있습니다. secret key는 base64 encoded 형태로 제공됩니다. 이를 decode, JSON parsing 하여 jwk를 얻습니다. 이 jwk로 privateKey를 생성합니다.
import { createPrivateKey } from 'node:crypto';
async issueClientCredentialsToken(): Promise<IssueAccessTokenResult> {
...
const privateJwk = this.decodeBase64UrlToJwk(TOMO_IDV_SECRET);
const privateKey = createPrivateKey({ key: privateJwk, format: 'jwk' });
...
}
private decodeBase64UrlToJwk(encodedJwk: string): JsonWebKey {
try {
const decoded = this.base64UrlDecode(encodedJwk);
const jwk = JSON.parse(decoded.toString('utf8')) as JsonWebKey;
return jwk;
} catch (error) {
throw new Error(`Failed to decode base64url JWK: ${error}`);
}
}
얻어진 privateKey와 아래 signJwt 함수를 이용하여 JWT payload에 서명하여 최종적으로 ClientAssertion을 생성합니다.
import { createSign } from 'node:crypto'
async issueClientCredentialsToken(): Promise<IssueAccessTokenResult> {
...
private signJwt(privateKey: KeyObject, payload: Record<string, unknown>): string {
const header = this.base64UrlEncode(Buffer.from(JSON.stringify({ alg: 'ES256', typ: 'JWT' })));
const body = this.base64UrlEncode(Buffer.from(JSON.stringify(payload)));
const signingInput = `${header}.${body}`;
const signer = createSign('sha256');
signer.update(signingInput);
signer.end();
const signature = signer.sign({ key: privateKey, dsaEncoding: 'ieee-p1363' });
const encodedSignature = this.base64UrlEncode(signature);
return `${signingInput}.${encodedSignature}`;
}
private base64UrlEncode(buffer: Buffer): string {
return buffer
.toString('base64')
.replace(/\\\\+/g, '-')
.replace(/\\\\//g, '_')
.replace(/=+$/u, '');
}
}