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는 별도 페이지 Device Flow 참조.

이 문서 기준

이 페이지는 confidential client (client_secret 보유) 관점에서 흐름을 설명합니다. Public client (PKCE-only, 모바일/SPA) 사용 시 client_secret 단계만 생략됩니다. Public Clients 가이드 참조.

시퀀스 다이어그램

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, ... }

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에 echo
  • provider — 로그인 화면에 노출할 로그인 방법 키 (쉼표로 구분, 예: google 또는 apple,google). 화면 노출만 제어하며 보안 통제가 아닙니다. 잘못된 값은 무시됩니다. 자세한 동작·우선순위는 로그인 방법 제한 참조.
  • prompt — OIDC §3.1.2.1. 지원 값은 login(기존 세션 종료 후 강제 재인증), none(silent auth — UI 절대 미표시, 미로그인이면 error=login_required, 추가 동의가 필요하면 error=consent_required 를 콜백으로 반사). 공백 구분 목록에 둘 다 있으면 loginnone 보다 우선하며, 미지원 값(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 교환 (백엔드 → 백엔드)

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

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