Skip to content

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):

  1. client_secret bcrypt 일치
  2. code 존재 + used_at null
  3. expires_at > now (10분)
  4. redirect_uri snapshot 일치
  5. 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=...

동작

  1. 제시된 RT 해시로 DB 조회
  2. revoked_at 존재 → 체인 전체 revoke + 400 invalid_grant (재사용 공격 탐지)
  3. refresh_expires_at 지남 → 해당 레코드만 revoke + 400
  4. 정상 → 기존 레코드 revoke + 새 레코드 issue (refreshed_from_id 체인)

응답 구조는 authorization_code와 동일 — 클라이언트는 동일한 파싱 경로 사용 가능.

4. UserInfo

http
GET /oauth/userinfo
Authorization: Bearer <access_token JWT>

응답은 scope 기반:

scope포함 필드
profile, emailsub, 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

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