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.revokedWebhook 구독
1. PKCE S256
code_challenge_method=S2562. 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 ← query3. 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 Native | Keychain Services | kSecAttrAccessibleWhenUnlockedThisDeviceOnly (iCloud sync 차단) |
| Android Native | DataStore + Tink 또는 Ackee Guardian | setUserAuthenticationRequired(true) |
| Flutter | flutter_secure_storage 10.0+ | KeychainAccessibility.first_unlock_this_device |
| React Native | react-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 S256 | verifier 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=true | user.locked_until 체크 |
토큰 유출 대응 매트릭스
| 유출 대상 | 자동 대응 | 운영자 액션 |
|---|---|---|
| Refresh token | 재사용 감지 시 체인 revoke | 사용자 재로그인 안내 |
| Access token | jti revoke 확인 시 거부 | 짧은 만료 유지 |
| Client secret | rotate_secret 시 구 secret 즉시 무효 | CI/CD secret 교체 |
| PAK | revoke 시 다음 요청부터 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_enabledON → 푸시/이메일 (Phase 2)suspicious=true→ 알림 OFF여도 강제 알림 (Phase 2)auto_lock_on_suspicious_loginON → 의심 시 24시간 자동 잠금- RP는
user.unlinked등 Webhook 구독으로 동기화
Troubleshooting
| 증상 | 원인 | 조치 |
|---|---|---|
invalid_grant on token exchange | PKCE verifier 누락/불일치 | code_verifier 저장 경로 점검 |
invalid_request redirect_uri mismatch | trailing slash/대소문자/query | 등록값과 완전 일치 |
| JWT verify 실패 (audience) | aud 미설정 또는 다른 client_id | LOGI_CLIENT_ID env 확인 |
| refresh 갱신 후 즉시 401 | 토큰 체인 revoke (재사용 감지) | 재로그인 유도 |
| 로그인 lockout | 10회/15분 초과 | 30분 대기 or 운영자 unlock |