Skip to content

Security Best Practices

점검 체크리스트

  • [ ] 모든 OAuth 엔드포인트 HTTPS (localhost 외)
  • [ ] PKCE code_challenge_method=S256 (plain 거부됨)
  • [ ] redirect_uri 등록값과 완전 일치 (trailing slash/대소문자/query 모두 다른 URI)
  • [ ] state 난수 생성·세션 저장·콜백 검증
  • [ ] JWT 검증에 iss="logi" + aud=LOGI_CLIENT_ID + exp 포함
  • [ ] Refresh token: 웹은 httpOnly Secure SameSite=Strict 쿠키, 모바일은 OS 보안 저장소
  • [ ] client_secret은 env/secret manager 전용 (Git/CI 로그 마스킹)
  • [ ] token.revoked Webhook 구독

1. PKCE S256

code_challenge_method=S256

PKCE 상세

2. redirect_uri 완전 일치

아래는 모두 다른 URI:

https://app.example.com/cb
https://app.example.com/cb/        ← trailing slash
https://APP.example.com/cb         ← 대소문자
https://app.example.com/cb?foo=1   ← query

3. state (CSRF 방어)

ts
const state = base64url(crypto.getRandomValues(new Uint8Array(32)));
sessionStorage.setItem("oauth_state", state);

// 콜백
if (params.get("state") !== sessionStorage.getItem("oauth_state")) {
  throw new Error("CSRF: state mismatch");
}

4. Refresh Token 저장

  • ❌ 브라우저 localStorage/IndexedDB (XSS 취약)
  • ✅ 웹: httpOnly Secure SameSite=Strict 쿠키
  • ✅ 모바일: OS 보안 저장소
플랫폼권장 저장 방식핵심 옵션
iOS NativeKeychain ServiceskSecAttrAccessibleWhenUnlockedThisDeviceOnly (iCloud sync 차단)
Android NativeDataStore + Tink 또는 Ackee GuardiansetUserAuthenticationRequired(true)
Flutterflutter_secure_storage 10.0+KeychainAccessibility.first_unlock_this_device
React Nativereact-native-keychain 10.0+ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY

Android EncryptedSharedPreferences 비권장

androidx.security:security-crypto 1.1.0부터 deprecated. DataStore + Tink 또는 Ackee Guardian 사용.

iOS Keychain iCloud sync

기본 sync 동작. 토큰은 반드시 ...ThisDeviceOnly accessibility 명시. iOS 가이드

5. JWT 검증

ts
await jwtVerify(token, jwks, {
  issuer: "logi",
  audience: process.env.LOGI_CLIENT_ID,
  // exp, nbf 는 jose 가 자동 검증
});

// 민감 API: jti revoke 상태까지 확인
await assertNotRevoked(payload.jti);

aud 검증 누락 시 다른 logi 앱 토큰을 오인 가능.

6. Client Secret

  • 발급 시 1회 노출 → 즉시 secret manager/env 복사
  • Git 커밋 금지, CI 로그 마스킹
  • 유출 시: POST /developer/applications/:id/rotate_secret → 구 secret 즉시 무효

7. HTTPS + HSTS

  • 모든 OAuth 엔드포인트 HTTPS 필수
  • logi 응답: Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  • redirect_uri 등록은 HTTPS만 (localhost 예외)

공격 시나리오 → 방어 매핑 (OWASP)

공격logi 측RP 측
code 탈취 (OWASP A02)PKCE S256verifier sessionStorage 한정
다른 RP code 주입 (A01)code_challenge + client_secret 바인딩aud 검증
CSRF authorize (A01)state 검증state 세션 저장
refresh token 재사용 (A07)rotation + chain revoke쿠키 저장 + Set-Cookie 덮어쓰기
replay authorize (A07)nonce (openid)nonce 검증
open redirect (A01)redirect_uri exact whitelist등록 엄격
브루트포스 (A07)10회/15분 → 30분 lockout + Cloudflare
의심 로그인 (A09)국가/device/burst 탐지 → suspicious=trueuser.locked_until 체크

토큰 유출 대응 매트릭스

유출 대상자동 대응운영자 액션
Refresh token재사용 감지 시 체인 revoke사용자 재로그인 안내
Access tokenjti revoke 확인 시 거부짧은 만료 유지
Client secretrotate_secret 시 구 secret 즉시 무효CI/CD secret 교체
PAKrevoke 시 다음 요청부터 401새 PAK 발급 후 자동화 갱신

2FA (TOTP + 백업 코드)

  • TOTP secret: 보안 설정 → QR 스캔 (Google Authenticator/1Password/Authy 호환)
  • 로그인 검증: TOTP 6자리 / 백업 코드 1회용 / Passkey UV
  • 세션에 otp_verified_at 기록 → 민감 작업 재요구 기준
  • 백업 코드: 발급 직후 1회 노출, 1회 사용, 인증 앱과 다른 장소 보관

Passkey

비밀번호 + OTP 대신 권장 (피싱 불가, 재사용 불가, origin 바인딩). 웹은 navigator.credentials.create/get 으로 OAuth Flow 이후 단계로 도입.

로그/알림 (logi 자동 수행)

  • 모든 로그인 → login_logs (IP/UA/country)
  • login_notification_enabled ON → 푸시/이메일 (Phase 2)
  • suspicious=true → 알림 OFF여도 강제 알림 (Phase 2)
  • auto_lock_on_suspicious_login ON → 의심 시 24시간 자동 잠금
  • RP는 user.unlinked 등 Webhook 구독으로 동기화

Troubleshooting

증상원인조치
invalid_grant on token exchangePKCE verifier 누락/불일치code_verifier 저장 경로 점검
invalid_request redirect_uri mismatchtrailing slash/대소문자/query등록값과 완전 일치
JWT verify 실패 (audience)aud 미설정 또는 다른 client_idLOGI_CLIENT_ID env 확인
refresh 갱신 후 즉시 401토큰 체인 revoke (재사용 감지)재로그인 유도
로그인 lockout10회/15분 초과30분 대기 or 운영자 unlock

Identity가 제품의 신뢰를 만듭니다.