OAuth 2.0 Authorization Code + PKCE
logi는 OAuth 2.0 Authorization Code Grant에 PKCE(RFC 7636) S256을 강제합니다. Implicit Flow, Password Grant는 지원하지 않습니다. Device Code Flow는 별도 페이지 Device Flow 참조.
이 문서 기준
이 페이지는 confidential client (client_secret 보유) 관점에서 흐름을 설명합니다. Public client (PKCE-only, 모바일/SPA) 사용 시 client_secret 단계만 생략됩니다. Public Clients 가이드 참조.
시퀀스 다이어그램
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, ... }Step-up 인증과 Passkey UV
민감한 작업(예: 2FA 해제, 비밀번호 변경)에 들어가기 전 logi 는 step-up 인증을 요구합니다. Passkey 로 User Verification (UV — 생체/PIN) 을 통과한 세션은 OTP 코드 추가 입력 없이 step-up 통과로 간주합니다. WebAuthn UV 플래그는 AAL2 와 등가이므로 별도의 OTP/백업코드 입력을 강제하지 않습니다 (PASSKEY_UV_MAX_AGE = 15분 윈도우 내). 자세한 내부 동작은 Security::StepUpVerifier 서비스 참고.
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에 echoprovider— 로그인 화면에 노출할 로그인 방법 키 (쉼표로 구분, 예:google또는apple,google). 화면 노출만 제어하며 보안 통제가 아닙니다. 잘못된 값은 무시됩니다. 자세한 동작·우선순위는 로그인 방법 제한 참조.prompt— OIDC §3.1.2.1. 지원 값은login(기존 세션 종료 후 강제 재인증),none(silent auth — UI 절대 미표시, 미로그인이면error=login_required, 추가 동의가 필요하면error=consent_required를 콜백으로 반사). 공백 구분 목록에 둘 다 있으면login이none보다 우선하며, 미지원 값(consent,select_account등)은 OIDC 권고대로 에러 없이 무시됩니다.resource— RFC 8707 Resource Indicator. fragment 없는 절대 http(s) URI 이면서 등록된 리소스 서버여야 하며, 형식 위반·미등록·소유자 모호 시error=invalid_target콜백 반사. 동의(consent)는 resource 단위로 바인딩됩니다 — 새 resource 나 동일 resource 의 scope 확장은 재동의를 요구합니다.ui_locales— OIDC UI 언어 힌트. 공백 구분 선호 목록에서 첫ko/en매칭을 적용합니다 (지원 목록은 discovery 의ui_locales_supported). 쿠키 기반 UI 전용 힌트로, 사용자 계정의 언어 설정은 변경하지 않으며 OAuth 요청 의미에는 영향이 없습니다.
에러 처리 방침
| 오류 위치 | 응답 |
|---|---|
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 교환 (백엔드 → 백엔드)
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
성공 응답:
{
"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
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
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
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
POST /oauth/introspect
token=<access_token 또는 refresh_token>
&token_type_hint=access_token|refresh_token
&client_id=...&client_secret=...응답 예시:
{
"active": true,
"scope": "profile email",
"client_id": "logi_...",
"token_type": "access_token",
"exp": 1760000000,
"iat": 1760000000,
"sub": "123",
"aud": "logi_...",
"iss": "https://api.1pass.dev",
"jti": "..."
}- 다른 client의 토큰이거나 만료/revoke된 토큰이면
{ "active": false } - access token은 JWT 서명/만료 검증 후 조회
- refresh token은 digest 기반으로 조회
QR 로그인 옵션
logi /oauth/authorize 페이지는 사용자에게 "📱 QR 로 로그인" 버튼을 노출합니다. 사용자가 QR 옵션을 선택하면 logi 모바일 앱으로 승인 후 브라우저가 자동으로 redirect_uri?code=&state= 로 이동합니다 — 표준 Authorization Code 흐름과 100% 동일한 응답입니다.
RP 가 추가로 할 일은 없습니다. 자세한 보안 모델, 시퀀스 다이어그램, 에러 처리는 QR 로그인 가이드를 참고하세요.
레퍼런스
- RFC 6749 §4.1 — Authorization Code Grant
- RFC 7636 — PKCE (S256 필수)
- RFC 9068 — Access Token JWT
- OpenID Connect Core 1.0 §3.1