Keycloak 기반 인증, 패스키 등록, OAuth 2.1 + PKCE 플로우, OpenFGA를 활용한 ReBAC, 토큰 관리, API 인증, 모니터링과 감사까지 통합하는 실전 프로젝트를 구축합니다.
이 시리즈의 마지막 장에서는 지금까지 다룬 모든 개념을 통합하여 하나의 완결된 인증 시스템을 구축합니다. Keycloak을 IdP로, OAuth 2.1 + PKCE를 인증 플로우로, 패스키를 주요 인증 수단으로, OpenFGA를 권한 엔진으로 사용하는 프로젝트입니다.
전체 시스템을 로컬에서 실행할 수 있는 Docker Compose 구성입니다.
services:
# --- 인증 인프라 ---
keycloak:
image: quay.io/keycloak/keycloak:25.0
command: start-dev --import-realm
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://keycloak-db:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: ${KC_DB_PASSWORD}
KC_HOSTNAME: localhost
KC_HTTP_PORT: 8080
KC_FEATURES: passkeys
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD}
ports:
- "8080:8080"
volumes:
- ./keycloak/realm-export.json:/opt/keycloak/data/import/realm.json
depends_on:
keycloak-db:
condition: service_healthy
keycloak-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: ${KC_DB_PASSWORD}
volumes:
- keycloak-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak"]
interval: 5s
timeout: 5s
retries: 5
# --- 인가 인프라 ---
openfga:
image: openfga/openfga:latest
command: run
environment:
OPENFGA_DATASTORE_ENGINE: postgres
OPENFGA_DATASTORE_URI: postgres://openfga:${FGA_DB_PASSWORD}@openfga-db:5432/openfga?sslmode=disable
ports:
- "8081:8080" # HTTP API
- "8082:8081" # gRPC
- "3000:3000" # Playground
depends_on:
openfga-db:
condition: service_healthy
openfga-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: openfga
POSTGRES_USER: openfga
POSTGRES_PASSWORD: ${FGA_DB_PASSWORD}
volumes:
- openfga-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U openfga"]
interval: 5s
timeout: 5s
retries: 5
# --- 관측성 ---
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
keycloak-pgdata:
openfga-pgdata:이 Docker Compose 구성은 개발 및 학습용입니다. 프로덕션 환경에서는 Keycloak의 start 모드 사용, TLS 설정, 별도의 관리형 데이터베이스, 클러스터링, 시크릿 관리(Vault 등)가 필수입니다.
{
"realm": "modern-auth-demo",
"enabled": true,
"sslRequired": "external",
"registrationAllowed": false,
"loginWithEmailAllowed": true,
"bruteForceProtected": true,
"failureFactor": 5,
"maxFailureWaitSeconds": 900,
"passwordPolicy": "length(8) and notUsername and passwordHistory(4)",
"accessTokenLifespan": 300,
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 36000,
"webAuthnPolicyRpEntityName": "Modern Auth Demo",
"webAuthnPolicyRpId": "localhost",
"webAuthnPolicyUserVerificationRequirement": "required",
"webAuthnPolicyAttestationConveyancePreference": "none",
"webAuthnPolicyAuthenticatorAttachment": "platform",
"webAuthnPolicyResidentKeyRequirement": "required",
"clients": [
{
"clientId": "bff-client",
"name": "BFF Client",
"protocol": "openid-connect",
"publicClient": false,
"secret": "CHANGE_ME_IN_PRODUCTION",
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"redirectUris": ["http://localhost:3001/auth/callback"],
"webOrigins": ["http://localhost:3001"],
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "http://localhost:3001"
},
"defaultClientScopes": [
"openid",
"profile",
"email"
]
},
{
"clientId": "api-gateway",
"name": "API Gateway (Service Account)",
"protocol": "openid-connect",
"publicClient": false,
"secret": "CHANGE_ME_IN_PRODUCTION",
"serviceAccountsEnabled": true,
"standardFlowEnabled": false,
"directAccessGrantsEnabled": false
}
],
"roles": {
"realm": [
{ "name": "user", "description": "일반 사용자" },
{ "name": "editor", "description": "편집자" },
{ "name": "admin", "description": "관리자" }
]
}
}Keycloak은 v23부터 WebAuthn/패스키를 네이티브로 지원합니다. 하지만 커스텀 등록 플로우가 필요한 경우, 별도의 패스키 서비스를 구현할 수 있습니다.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from "@simplewebauthn/server";
import type {
RegistrationResponseJSON,
AuthenticationResponseJSON,
} from "@simplewebauthn/types";
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
const RP_NAME = "Modern Auth Demo";
const RP_ID = process.env.RP_ID ?? "localhost";
const ORIGIN = process.env.ORIGIN ?? "http://localhost:3001";
export class PasskeyService {
// 등록 옵션 생성
async createRegistrationOptions(userId: string, userEmail: string) {
const existingCredentials = await this.getCredentials(userId);
const options = await generateRegistrationOptions({
rpName: RP_NAME,
rpID: RP_ID,
userName: userEmail,
excludeCredentials: existingCredentials.map((c) => ({
id: c.credentialId,
type: "public-key",
transports: c.transports,
})),
authenticatorSelection: {
authenticatorAttachment: "platform",
userVerification: "required",
residentKey: "required",
},
attestationType: "none",
});
// 챌린지를 Redis에 저장 (5분 TTL)
await redis.setex(
`passkey:challenge:${userId}`,
300,
options.challenge,
);
return options;
}
// 등록 검증
async verifyRegistration(
userId: string,
response: RegistrationResponseJSON,
): Promise<boolean> {
const expectedChallenge = await redis.get(
`passkey:challenge:${userId}`,
);
if (!expectedChallenge) {
throw new Error("챌린지가 만료되었습니다.");
}
const verification = await verifyRegistrationResponse({
response,
expectedChallenge,
expectedOrigin: ORIGIN,
expectedRPID: RP_ID,
});
if (verification.verified && verification.registrationInfo) {
const { credential } = verification.registrationInfo;
await this.saveCredential(userId, {
credentialId: credential.id,
publicKey: Buffer.from(credential.publicKey).toString("base64"),
counter: credential.counter,
transports: response.response.transports ?? [],
createdAt: new Date(),
});
// 챌린지 삭제
await redis.del(`passkey:challenge:${userId}`);
}
return verification.verified;
}
// 인증 옵션 생성
async createAuthenticationOptions() {
const options = await generateAuthenticationOptions({
rpID: RP_ID,
userVerification: "required",
});
// 챌린지를 Redis에 저장
await redis.setex(
`passkey:auth-challenge:${options.challenge}`,
300,
"pending",
);
return options;
}
// 인증 검증
async verifyAuthentication(
response: AuthenticationResponseJSON,
expectedChallenge: string,
): Promise<{ verified: boolean; userId: string | null }> {
const credential = await this.findCredentialById(response.id);
if (!credential) {
return { verified: false, userId: null };
}
const verification = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin: ORIGIN,
expectedRPID: RP_ID,
credential: {
id: credential.credentialId,
publicKey: Buffer.from(credential.publicKey, "base64"),
counter: credential.counter,
},
});
if (verification.verified) {
await this.updateCounter(
credential.credentialId,
verification.authenticationInfo.newCounter,
);
await redis.del(`passkey:auth-challenge:${expectedChallenge}`);
}
return {
verified: verification.verified,
userId: credential.userId,
};
}
private async getCredentials(userId: string) {
// DB에서 사용자의 자격 증명 목록 조회
return getCredentialsByUserId(userId);
}
private async saveCredential(userId: string, credential: CredentialData) {
return saveCredentialToDb(userId, credential);
}
private async findCredentialById(credentialId: string) {
return findCredentialInDb(credentialId);
}
private async updateCounter(credentialId: string, newCounter: number) {
return updateCredentialCounter(credentialId, newCounter);
}
}import express from "express";
import session from "express-session";
import RedisStore from "connect-redis";
import { Redis } from "ioredis";
const app = express();
const redis = new Redis(process.env.REDIS_URL);
// 세션 설정
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET ?? "change-me",
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 24 * 60 * 60 * 1000, // 24시간
},
}));
app.use(express.json());
// --- OAuth 2.1 + PKCE 플로우 ---
// 로그인 시작
app.get("/auth/login", (req, res) => {
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
const state = generateRandomString(32);
req.session.oauthState = state;
req.session.codeVerifier = codeVerifier;
const params = new URLSearchParams({
response_type: "code",
client_id: process.env.KC_CLIENT_ID ?? "",
redirect_uri: `${process.env.BFF_URL}/auth/callback`,
scope: "openid profile email",
state,
code_challenge: codeChallenge,
code_challenge_method: "S256",
});
const keycloakAuthUrl = `${process.env.KC_URL}/realms/modern-auth-demo/protocol/openid-connect/auth`;
res.redirect(`${keycloakAuthUrl}?${params.toString()}`);
});
// 콜백 처리
app.get("/auth/callback", async (req, res) => {
const { code, state, error } = req.query;
if (error) {
return res.redirect(`/?error=${error}`);
}
// State 검증
if (state !== req.session.oauthState) {
return res.status(403).json({ error: "State mismatch" });
}
try {
// 토큰 교환
const tokenResponse = await fetch(
`${process.env.KC_URL}/realms/modern-auth-demo/protocol/openid-connect/token`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: process.env.KC_CLIENT_ID ?? "",
client_secret: process.env.KC_CLIENT_SECRET ?? "",
code: code as string,
redirect_uri: `${process.env.BFF_URL}/auth/callback`,
code_verifier: req.session.codeVerifier ?? "",
}),
},
);
if (!tokenResponse.ok) {
throw new Error("토큰 교환 실패");
}
const tokens = await tokenResponse.json();
// 토큰을 서버 세션에 저장
req.session.accessToken = tokens.access_token;
req.session.refreshToken = tokens.refresh_token;
req.session.idToken = tokens.id_token;
req.session.tokenExpiresAt =
Date.now() + tokens.expires_in * 1000;
// PKCE 임시 데이터 정리
delete req.session.oauthState;
delete req.session.codeVerifier;
res.redirect("/");
} catch (error) {
console.error("인증 콜백 오류:", error);
res.redirect("/?error=auth_failed");
}
});
// 세션 정보 조회 (프론트엔드용)
app.get("/auth/session", (req, res) => {
if (!req.session.accessToken) {
return res.json({ authenticated: false });
}
// ID Token에서 사용자 정보 추출 (토큰 자체는 전달하지 않음)
const idPayload = decodeJwtPayload(req.session.idToken ?? "");
res.json({
authenticated: true,
user: {
id: idPayload.sub,
name: idPayload.name,
email: idPayload.email,
},
});
});
// 로그아웃
app.post("/auth/logout", async (req, res) => {
const idToken = req.session.idToken;
// 세션 삭제
req.session.destroy(() => {
// Keycloak 로그아웃
if (idToken) {
const logoutUrl = new URL(
`${process.env.KC_URL}/realms/modern-auth-demo/protocol/openid-connect/logout`,
);
logoutUrl.searchParams.set("id_token_hint", idToken);
logoutUrl.searchParams.set(
"post_logout_redirect_uri",
process.env.BFF_URL ?? "",
);
res.json({ logoutUrl: logoutUrl.toString() });
} else {
res.json({ logoutUrl: "/" });
}
});
});
// --- API 프록시 (토큰 자동 갱신 포함) ---
app.use("/api", async (req, res, next) => {
if (!req.session.accessToken) {
return res.status(401).json({ error: "인증이 필요합니다." });
}
// 토큰 만료 확인 및 자동 갱신
if (Date.now() >= (req.session.tokenExpiresAt ?? 0) - 30000) {
try {
const refreshResponse = await fetch(
`${process.env.KC_URL}/realms/modern-auth-demo/protocol/openid-connect/token`,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: process.env.KC_CLIENT_ID ?? "",
client_secret: process.env.KC_CLIENT_SECRET ?? "",
refresh_token: req.session.refreshToken ?? "",
}),
},
);
if (refreshResponse.ok) {
const newTokens = await refreshResponse.json();
req.session.accessToken = newTokens.access_token;
req.session.refreshToken = newTokens.refresh_token;
req.session.tokenExpiresAt =
Date.now() + newTokens.expires_in * 1000;
} else {
// Refresh 실패 시 재인증 필요
req.session.destroy(() => {});
return res.status(401).json({
error: "세션이 만료되었습니다. 재로그인이 필요합니다.",
});
}
} catch {
return res.status(500).json({ error: "토큰 갱신 실패" });
}
}
// 백엔드로 프록시
next();
});
app.listen(3001, () => {
console.log("BFF 서버가 포트 3001에서 실행 중입니다.");
});import { OpenFgaClient, CredentialsMethod } from "@openfga/sdk";
// OpenFGA 인가 모델
const AUTHORIZATION_MODEL = `
model
schema 1.1
type user
type organization
relations
define admin: [user]
define member: [user] or admin
type team
relations
define org: [organization]
define lead: [user]
define member: [user] or lead
type folder
relations
define org: [organization]
define owner: [user]
define editor: [user, team#member]
define viewer: [user, team#member, organization#member]
define can_edit: owner or editor
define can_view: can_edit or viewer
type document
relations
define parent_folder: [folder]
define owner: [user]
define editor: [user, team#member]
define viewer: [user, team#member]
define can_edit: owner or editor
define can_view: can_edit or viewer or can_view from parent_folder
define can_delete: owner
define can_share: owner or editor
`;
export class AuthorizationService {
private client: OpenFgaClient;
constructor() {
this.client = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL ?? "http://localhost:8081",
storeId: process.env.FGA_STORE_ID ?? "",
authorizationModelId: process.env.FGA_MODEL_ID ?? "",
});
}
// 권한 확인
async check(
userId: string,
relation: string,
objectType: string,
objectId: string,
): Promise<boolean> {
const result = await this.client.check({
user: `user:${userId}`,
relation,
object: `${objectType}:${objectId}`,
});
return result.allowed ?? false;
}
// 문서 접근 권한 확인
async canViewDocument(
userId: string,
documentId: string,
): Promise<boolean> {
return this.check(userId, "can_view", "document", documentId);
}
async canEditDocument(
userId: string,
documentId: string,
): Promise<boolean> {
return this.check(userId, "can_edit", "document", documentId);
}
async canDeleteDocument(
userId: string,
documentId: string,
): Promise<boolean> {
return this.check(userId, "can_delete", "document", documentId);
}
// 관계 추가 (권한 부여)
async grantAccess(
subjectType: string,
subjectId: string,
relation: string,
objectType: string,
objectId: string,
): Promise<void> {
await this.client.write({
writes: [
{
user: `${subjectType}:${subjectId}`,
relation,
object: `${objectType}:${objectId}`,
},
],
});
}
// 관계 제거 (권한 회수)
async revokeAccess(
subjectType: string,
subjectId: string,
relation: string,
objectType: string,
objectId: string,
): Promise<void> {
await this.client.write({
deletes: [
{
user: `${subjectType}:${subjectId}`,
relation,
object: `${objectType}:${objectId}`,
},
],
});
}
// 사용자가 접근 가능한 문서 목록
async listAccessibleDocuments(userId: string): Promise<string[]> {
const result = await this.client.listObjects({
user: `user:${userId}`,
relation: "can_view",
type: "document",
});
return result.objects;
}
// 문서 공유 (다른 사용자에게 권한 부여)
async shareDocument(
sharerId: string,
documentId: string,
targetUserId: string,
permission: "viewer" | "editor",
): Promise<void> {
// 공유 권한 확인
const canShare = await this.check(
sharerId,
"can_share",
"document",
documentId,
);
if (!canShare) {
throw new ForbiddenError("문서를 공유할 권한이 없습니다.");
}
await this.grantAccess(
"user",
targetUserId,
permission,
"document",
documentId,
);
// 감사 로그
await logAuditEvent({
action: "document_shared",
actor: sharerId,
target: targetUserId,
resource: `document:${documentId}`,
permission,
timestamp: new Date(),
});
}
}import type { Request, Response, NextFunction } from "express";
import { createRemoteJWKSet, jwtVerify } from "jose";
import { AuthorizationService } from "../services/authorization-service";
const jwks = createRemoteJWKSet(
new URL(
`${process.env.KC_URL}/realms/modern-auth-demo/protocol/openid-connect/certs`,
),
);
const authzService = new AuthorizationService();
// 인증 미들웨어
export function authenticate() {
return async (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ error: "Bearer 토큰이 필요합니다." });
}
try {
const token = authHeader.substring(7);
const { payload } = await jwtVerify(token, jwks, {
issuer: `${process.env.KC_URL}/realms/modern-auth-demo`,
algorithms: ["RS256"],
});
req.user = {
id: payload.sub as string,
email: payload.email as string,
roles: (payload.realm_access as { roles: string[] })?.roles ?? [],
};
next();
} catch (error) {
return res.status(401).json({ error: "유효하지 않은 토큰입니다." });
}
};
}
// 인가 미들웨어 (ReBAC)
export function authorize(relation: string, getObject: (req: Request) => string) {
return async (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: "인증이 필요합니다." });
}
const object = getObject(req);
const allowed = await authzService.check(
req.user.id,
relation,
object.split(":")[0],
object.split(":")[1],
);
if (!allowed) {
// 감사 로그: 접근 거부
await logAuditEvent({
action: "access_denied",
actor: req.user.id,
resource: object,
relation,
timestamp: new Date(),
});
return res.status(403).json({ error: "접근 권한이 없습니다." });
}
next();
};
}
// 라우트에 적용
// app.get(
// "/api/documents/:id",
// authenticate(),
// authorize("can_view", (req) => `document:${req.params.id}`),
// documentController.getDocument,
// );
//
// app.put(
// "/api/documents/:id",
// authenticate(),
// authorize("can_edit", (req) => `document:${req.params.id}`),
// documentController.updateDocument,
// );
//
// app.delete(
// "/api/documents/:id",
// authenticate(),
// authorize("can_delete", (req) => `document:${req.params.id}`),
// documentController.deleteDocument,
// );import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
interface AuditEvent {
// 이벤트 식별
eventId: string;
eventType: AuditEventType;
timestamp: Date;
// 행위자
actorId: string;
actorType: "user" | "service" | "system";
actorIp?: string;
actorUserAgent?: string;
// 대상
resourceType: string;
resourceId: string;
// 상세
action: string;
outcome: "success" | "failure";
reason?: string;
metadata?: Record<string, unknown>;
}
type AuditEventType =
| "authentication"
| "authorization"
| "user_management"
| "token_management"
| "admin_action"
| "data_access";
export class AuditService {
async log(event: Omit<AuditEvent, "eventId" | "timestamp">): Promise<void> {
const fullEvent: AuditEvent = {
...event,
eventId: crypto.randomUUID(),
timestamp: new Date(),
};
// 1. 영구 저장소에 기록
await this.persistEvent(fullEvent);
// 2. 실시간 스트림에 발행 (모니터링 대시보드용)
await redis.xadd(
"audit:stream",
"*",
"event",
JSON.stringify(fullEvent),
);
// 3. 보안 알림 트리거 확인
await this.checkAlertConditions(fullEvent);
}
private async checkAlertConditions(event: AuditEvent): Promise<void> {
// 연속 인증 실패 감지
if (
event.eventType === "authentication" &&
event.outcome === "failure"
) {
const failureCount = await this.getRecentFailures(
event.actorId,
5 * 60 * 1000, // 5분 이내
);
if (failureCount >= 5) {
await this.triggerAlert({
severity: "high",
type: "brute_force_suspected",
message: `사용자 ${event.actorId}의 5분 내 ${failureCount}회 인증 실패 감지`,
event,
});
}
}
// 비정상 접근 패턴 감지
if (
event.eventType === "authorization" &&
event.outcome === "failure"
) {
const deniedCount = await this.getRecentDenials(
event.actorId,
10 * 60 * 1000,
);
if (deniedCount >= 10) {
await this.triggerAlert({
severity: "medium",
type: "privilege_escalation_suspected",
message: `사용자 ${event.actorId}의 반복적 권한 거부 감지`,
event,
});
}
}
// 관리자 작업 알림
if (event.eventType === "admin_action") {
await this.triggerAlert({
severity: "info",
type: "admin_action_performed",
message: `관리자 ${event.actorId}이(가) ${event.action} 작업 수행`,
event,
});
}
}
private async getRecentFailures(
actorId: string,
windowMs: number,
): Promise<number> {
const key = `audit:failures:${actorId}`;
const now = Date.now();
// 윈도우 밖의 데이터 정리
await redis.zremrangebyscore(key, 0, now - windowMs);
return redis.zcard(key);
}
private async getRecentDenials(
actorId: string,
windowMs: number,
): Promise<number> {
const key = `audit:denials:${actorId}`;
const now = Date.now();
await redis.zremrangebyscore(key, 0, now - windowMs);
return redis.zcard(key);
}
private async triggerAlert(alert: SecurityAlert): Promise<void> {
// 실제 구현: Slack, PagerDuty, 이메일 등으로 알림 전송
console.warn(`[SECURITY ALERT] ${alert.severity}: ${alert.message}`);
await redis.xadd(
"alerts:stream",
"*",
"alert",
JSON.stringify(alert),
);
}
private async persistEvent(event: AuditEvent): Promise<void> {
// 실제 구현: PostgreSQL, Elasticsearch 등에 저장
// 최소 1년 보존 (PCI-DSS 요구)
await saveToAuditStore(event);
}
}
interface SecurityAlert {
severity: "info" | "low" | "medium" | "high" | "critical";
type: string;
message: string;
event: AuditEvent;
}감사 로그는 "나중에 추가하자"가 통하지 않는 영역입니다. 시스템 설계 초기부터 모든 인증/인가 이벤트를 기록하는 구조를 만들어야 합니다. 로그가 없으면 보안 사고 발생 시 원인 분석이 불가능하고, 규제 감사에서 즉시 부적합 판정을 받습니다.
10개 장에 걸쳐 현대 인증과 권한 관리의 전체 지형을 살펴보았습니다. 1장의 역사적 맥락에서 시작하여, OAuth 2.1과 OIDC의 프로토콜, 패스키의 비밀번호 없는 미래, 아이덴티티 플랫폼의 선택, ReBAC의 관계 기반 권한 모델, 토큰 관리 전략, 제로 트러스트 아키텍처, API 인증, 규제 대응까지 다루었습니다.
마지막 장에서 이 모든 것을 하나의 시스템으로 통합했습니다. 핵심 교훈을 정리합니다.
인증과 권한 관리는 끊임없이 진화합니다. 이 시리즈가 현재 시점의 모범 사례를 이해하고, 미래의 변화에 적응할 수 있는 기반이 되기를 바랍니다.
이 글이 도움이 되셨나요?
GDPR, PCI-DSS, SOC 2가 인증 시스템에 부과하는 구체적 요구사항, NIST 800-63B 비밀번호 가이드라인, 감사 로깅 설계, MFA 요구사항, 데이터 거주 전략을 정리합니다.
API 키 관리, OAuth Client Credentials, mTLS를 활용한 서비스 간 인증, 마이크로서비스에서의 JWT 전파, API 게이트웨이 인증 패턴을 실전 코드와 함께 설명합니다.
제로 트러스트의 핵심 원칙과 인증의 관계를 분석하고, 지속적 검증, 디바이스 신뢰, 컨텍스트 기반 접근 제어, BeyondCorp 모델, 아이덴티티 인식 프록시를 설명합니다.