OAuth 2.0 Authorization Code + PKCE
logi는 OAuth 2.0 Authorization Code Grant에 PKCE(RFC 7636) S256을 강제합니다. Implicit Flow, Password Grant, Device Code Flow는 지원하지 않습니다.
시퀀스 다이어그램
mermaid
sequenceDiagram
autonumber
participant C as 제휴사 앱
participant L as logi
participant U as 사용자 (브라우저/앱)
C->>C: verifier 생성 (랜덤 32B) · challenge = SHA256(verifier) b64url
C->>L: GET /oauth/authorize?client_id&redirect_uri&state&code_challenge&scope
L->>L: redirect_uri 화이트리스트 검증
L-->>U: 로그인 요구 / Consent 화면
U->>L: 자격증명 + 필요 시 OTP/Passkey
U->>L: "허용" 클릭
L-->>C: 302 redirect_uri?code=<code>&state=<state>
C->>C: state 일치 검증
C->>L: POST /oauth/token (client_id+secret · code · code_verifier · redirect_uri)
L->>L: client_secret bcrypt · code_challenge vs SHA256(verifier) · code 1회 소진
L-->>C: { access_token(JWT), refresh_token, expires_in, scope, id_token? }
C->>L: GET /oauth/userinfo (Authorization: Bearer JWT)
L-->>C: { sub, email?, identity_verified_level, ... }1. Authorization 요청 (브라우저)
GET /oauth/authorize
?client_id=logi_a1b2...
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback
&response_type=code
&scope=profile+email
&state=<32바이트_랜덤>
&code_challenge=<BASE64URL(SHA256(verifier))>
&code_challenge_method=S256
&nonce=<선택, OIDC>필수 파라미터:
client_id,redirect_uri,response_type=code,scope,state,code_challenge,code_challenge_method=S256
선택: nonce (openid scope 사용 시 id_token에 echo)
에러 처리 방침
| 오류 위치 | 응답 |
|---|---|
client_id 미존재 / redirect_uri 불일치 | HTML/JSON 400 (콜백 없이 — open redirect 방지) |
| PKCE 누락, scope 무효, response_type 틀림 | 302 redirect_uri?error=invalid_request&state=... |
| 사용자 "거부" | 302 redirect_uri?error=access_denied&state=... |
2. Token 교환 (백엔드 → 백엔드)
http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret) # 또는 body에 동봉
grant_type=authorization_code
&code=<받은 code>
&redirect_uri=<1단계와 동일>
&code_verifier=<생성한 verifier>검증 순서 (실패 시 즉시 400 invalid_grant):
- client_secret bcrypt 일치
- code 존재 +
used_atnull expires_at > now(10분)redirect_urisnapshot 일치BASE64URL(SHA256(verifier)) == code_challenge
성공 응답:
json
{
"access_token": "<JWT RS256>",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "<opaque 32B urlsafe>",
"scope": "profile email",
"id_token": "<JWT — openid scope 요청 시에만>"
}3. Refresh Token Rotation
http
POST /oauth/token
grant_type=refresh_token
&refresh_token=<이전 응답의 refresh_token>
&client_id=...&client_secret=...동작
- 제시된 RT 해시로 DB 조회
revoked_at존재 → 체인 전체 revoke +400 invalid_grant(재사용 공격 탐지)refresh_expires_at지남 → 해당 레코드만 revoke + 400- 정상 → 기존 레코드 revoke + 새 레코드 issue (
refreshed_from_id체인)
응답 구조는 authorization_code와 동일 — 클라이언트는 동일한 파싱 경로 사용 가능.
4. UserInfo
http
GET /oauth/userinfo
Authorization: Bearer <access_token JWT>응답은 scope 기반:
| scope | 포함 필드 |
|---|---|
profile, email | sub, email, email_verified, identity_verified_level |
openid (id_token에) | sub (필수), nonce (요청 시 echo) |
Revocation
http
POST /oauth/revoke
token=<access_token 또는 refresh_token>
&token_type_hint=access_token|refresh_token
&client_id=...&client_secret=...- 같은 OAuth client가 자기 토큰만 revoke 가능
- access token, refresh token 모두 허용
- 이미 revoke되었거나 존재하지 않는 토큰도 200 OK 반환 (RFC 7009)
- refresh token revoke 시 해당 체인 레코드도 함께 revoke
Introspection
http
POST /oauth/introspect
token=<access_token 또는 refresh_token>
&token_type_hint=access_token|refresh_token
&client_id=...&client_secret=...응답 예시:
json
{
"active": true,
"scope": "profile email",
"client_id": "logi_...",
"token_type": "access_token",
"exp": 1760000000,
"iat": 1760000000,
"sub": "123",
"aud": "logi_...",
"iss": "logi",
"jti": "..."
}- 다른 client의 토큰이거나 만료/revoke된 토큰이면
{ "active": false } - access token은 JWT 서명/만료 검증 후 조회
- refresh token은 digest 기반으로 조회
레퍼런스
- RFC 6749 §4.1 — Authorization Code Grant
- RFC 7636 — PKCE (S256 필수)
- RFC 9068 — Access Token JWT
- OpenID Connect Core 1.0 §3.1