테마
Device Authorization Grant (RFC 8628)
RFC 8628 — OAuth 2.0 Device Authorization Grant 기반의 device flow. RP 가 자체 페이지에서 QR 을 띄우고 폴링으로 토큰을 수령합니다. 사용자는 logi 앱으로 QR 을 스캔해 승인하면 RP 페이지가 자동으로 다음 단계로 진행됩니다.
언제 쓰나요?
- RP 페이지에서 사용자가 떠나지 않아야 할 때 (네이버 패턴, 결제 직전, 모달 안에서 SSO)
- 브라우저가 없는 환경 (CLI, Smart TV, IoT 디바이스)
- iOS Universal Link 가 작동하지 않는 인앱 브라우저 (카카오톡 / 네이버 인앱)
Public client 도 device flow 사용 가능
RFC 8628 §3.4 — public 과 confidential 모두 device flow 지원. Public RP 는 client_secret 없이 client_id 만으로 device authorization 시작 가능.
페이지 redirect 가 허용되는 일반 웹 RP 라면 표준 Authorization Code Flow 가 더 단순합니다. UI 통제가 필요 없고 빠른 통합이 목표면 Widget SDK 를 보세요.
시퀀스 다이어그램
mermaid
sequenceDiagram
autonumber
participant U as 사용자
participant RP as RP 페이지
participant L as logi 서버 (api.1pass.dev)
participant App as logi 모바일 앱
RP->>L: POST /oauth/device_authorization (client_id, scope)
L-->>RP: { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval }
RP->>RP: QR 렌더 (verification_uri_complete) + interval 초마다 폴링 시작
loop polling (interval=5s 권장)
RP->>L: POST /oauth/token (grant_type=device_code, device_code)
L-->>RP: 400 { error: "authorization_pending" }
end
U->>App: QR 스캔
App->>L: 사용자 인증 + scope 동의
L->>L: device_code → approved 상태 전이
RP->>L: POST /oauth/token (grant_type=device_code, device_code)
L-->>RP: 200 { access_token, refresh_token, id_token?, token_type, expires_in }
RP->>U: 로그인 완료 (페이지 전환 없음)엔드포인트
1. Device Authorization 요청
POST https://api.1pass.dev/oauth/device_authorization
Content-Type: application/x-www-form-urlencoded| 파라미터 | 필수 | 설명 |
|---|---|---|
client_id | ✅ | RP 의 OAuth Application client_id |
client_secret | confidential 만 | confidential client 만 필요 (Basic auth 또는 form). public client 는 미사용 — 보내면 거절 (downgrade 방어) |
scope | ✅ | 공백 구분 scope 목록 (예: openid profile email) |
응답 (200 OK):
json
{
"device_code": "x7Y9aZ-...",
"user_code": "WDJB-MJHT",
"verification_uri": "https://api.1pass.dev/activate",
"verification_uri_complete": "https://api.1pass.dev/activate?user_code=WDJB-MJHT",
"expires_in": 600,
"interval": 5
}| 필드 | 의미 |
|---|---|
device_code | RP 가 이후 폴링에 사용할 비밀 토큰. 사용자에게 노출 금지 |
user_code | 사용자가 직접 입력해야 할 때 표시할 짧은 코드 (QR 사용 시 노출 불필요) |
verification_uri | 사용자가 브라우저로 직접 방문할 짧은 URL |
verification_uri_complete | user_code 가 prefill 된 풀 URL — QR 인코딩에 이 값을 사용 |
expires_in | device_code 의 유효 기간 (초). 기본 600초 (10분) |
interval | 최소 폴링 간격 (초). 기본 5초. slow_down 응답 시 +5초 추가 |
Cache 헤더
응답에는 Cache-Control: no-store, Pragma: no-cache 가 자동 부여됩니다 (RFC 6749 §5.1).
2. 토큰 폴링
POST https://api.1pass.dev/oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)| 파라미터 | 필수 | 설명 |
|---|---|---|
grant_type | ✅ | 정확히 urn:ietf:params:oauth:grant-type:device_code |
device_code | ✅ | 1단계에서 받은 device_code |
client_id | ✅ | (Basic 헤더 미사용 시) form body 에 포함 |
성공 응답 (200 OK):
json
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "8aZk9...urlsafe_base64_32B_no_prefix...",
"scope": "openid profile email"
}scope 에 openid 가 포함됐다면 응답에 id_token 도 함께 반환됩니다. 토큰 검증은 JWKS 참고.
폴링 에러 코드
device flow 의 모든 에러는 400 Bad Request 또는 401 Unauthorized 로 반환되며, body 는 { "error": "...", "error_description": "..." } 형식입니다.
error | 의미 | RP 의 액션 |
|---|---|---|
authorization_pending | 사용자가 아직 승인 안 함 | interval 초 후 다시 폴링 |
slow_down | 폴링이 너무 빠름 | interval 을 +5초 늘려서 다시 폴링 |
access_denied | 사용자가 거절 | 폴링 중단, 사용자에게 안내. 새 device flow 재시작 필요 |
expired_token | device_code 만료 (10분 초과) | 폴링 중단, /oauth/device_authorization 부터 재시작 |
invalid_grant | device_code 가 무효 / 다른 client / 이미 교환됨 | 폴링 중단, 새로 시작 |
invalid_client | client_id / client_secret 인증 실패 | 자격 증명 확인 |
unauthorized_client | RP 가 device flow 미승인 | logi 콘솔에서 application 승인 상태 확인 |
Polling cadence — slow_down (HTTP 400)
RFC 8628 §3.5 의 표준 폴링 cadence 제어입니다. RP 가 응답받은 interval 보다 빠르게 /oauth/token 을 폴링하면 logi 서버는 device_code 단위로 즉시 slow_down 을 내려보냅니다.
| 필드 | 값 |
|---|---|
| HTTP status | 400 Bad Request |
error | slow_down |
error_description | polling too fast; respect the interval value |
Retry-After 헤더 | ❌ 내려가지 않음 |
RP 처리: RFC 8628 §3.5 에 따라 현재 interval 을 +5초 누적 증가시키고 다음 폴링까지 대기. 같은 device flow 안에서 한 번 늘린 값은 reset 하지 마세요.
javascript
// 폴링 루프 안에서 slow_down 처리
const { error } = await res.json();
if (error === "slow_down") {
pollInterval += 5000; // 누적 증가, reset 금지
continue;
}Rate limit — rate_limited (HTTP 429)
logi 서버는 /oauth/device_authorization 과 /oauth/token 양쪽 엔드포인트에 controller 레벨 rate limiter 를 두고 있습니다. client_id (없으면 IP) 기준 한도를 초과하면 device_code 폴링 cadence 와는 별개로 429 가 떨어집니다.
| 필드 | 값 |
|---|---|
| HTTP status | 429 Too Many Requests |
error | rate_limited |
error_description | too many token requests (token endpoint) / too many device authorization requests (device_authorization endpoint) |
Retry-After 헤더 | ❌ 현재 미설정 |
| 적용 한도 | /oauth/token 20 req/min, /oauth/device_authorization 30 req/min (모두 client_id 또는 IP 기준) |
RP 처리: Retry-After 헤더가 없으므로 exponential backoff 으로 후퇴하세요.
- 첫 429: 현재
interval× 2 만큼 대기 후 1회 재시도. - 연속 3회 이상 429 가 반복되면 폴링 중단, 사용자에게 안내 (RP 자격증명/네트워크 점검 신호).
/oauth/device_authorization단계에서 429 가 나면 새 device flow 를 시작하기 전 최소 60초 backoff. 무한 재시도 금지.
javascript
// 폴링 루프 안에서 429 처리 (Retry-After 없음 → 자체 backoff)
if (res.status === 429) {
pollInterval = Math.min(pollInterval * 2, 60_000);
consecutive429 += 1;
if (consecutive429 >= 3) throw new Error("rate_limited");
continue;
}코드 예시
curl
bash
# 1. Device authorization 요청
curl -X POST https://api.1pass.dev/oauth/device_authorization \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "scope=openid profile email"
# → device_code, user_code, verification_uri_complete 수령
# 2. (사용자가 QR 스캔하는 동안) 폴링
curl -X POST https://api.1pass.dev/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
-d "device_code=$DEVICE_CODE"
# → authorization_pending → ... → access_tokenJavaScript (브라우저)
javascript
async function deviceLogin(clientId) {
// 1. Device authorization 요청 — 클라이언트 시크릿이 없는 브라우저는
// 일반적으로 RP 백엔드를 경유합니다 (아래는 예시):
const init = await fetch("/api/auth/1pass/device/start", { method: "POST" });
const { device_code, user_code, verification_uri_complete, interval, expires_in } = await init.json();
// 2. QR 코드 렌더 (verification_uri_complete)
renderQR(verification_uri_complete);
showUserCode(user_code); // QR 못 찍는 사용자용 fallback
// 3. 폴링 루프
const deadline = Date.now() + expires_in * 1000;
let pollInterval = interval * 1000;
while (Date.now() < deadline) {
await sleep(pollInterval);
const res = await fetch("/api/auth/1pass/device/poll", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ device_code }),
});
if (res.ok) {
const { access_token, id_token } = await res.json();
return { access_token, id_token };
}
const { error } = await res.json();
if (error === "slow_down") pollInterval += 5000;
else if (error === "authorization_pending") continue;
else throw new Error(error); // access_denied / expired_token / ...
}
throw new Error("device_code expired");
}Ruby (Rails RP 백엔드)
ruby
# 1. Device authorization 요청 (RP 백엔드 → logi)
require "net/http"
require "json"
def start_device_flow
uri = URI("https://api.1pass.dev/oauth/device_authorization")
req = Net::HTTP::Post.new(uri)
req.basic_auth(ENV["LOGI_CLIENT_ID"], ENV["LOGI_CLIENT_SECRET"])
req.set_form_data(scope: "openid profile email")
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
JSON.parse(res.body)
end
# 2. 폴링 (브라우저가 호출하는 RP 엔드포인트)
def poll_device_flow(device_code)
uri = URI("https://api.1pass.dev/oauth/token")
req = Net::HTTP::Post.new(uri)
req.basic_auth(ENV["LOGI_CLIENT_ID"], ENV["LOGI_CLIENT_SECRET"])
req.set_form_data(
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code: device_code
)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
{ status: res.code.to_i, body: JSON.parse(res.body) }
end보안 가이드
- ✅ HTTPS only. 모든 device flow 트래픽은 TLS 위에서만 동작합니다.
- ✅ client_secret 보호 (confidential). confidential RP 는
client_secret을 RP 백엔드에서만 보관하세요. 브라우저나 모바일 앱 바이너리에 포함시키지 마세요. - ✅ Public client 도 device flow 사용 가능 (RFC 8628 §3.4). Public RP 는
client_id만으로 device authorization 시작 —client_secret보내면 logi 가invalid_client거절 (downgrade 방어). - ✅
device_code는 비밀. 사용자에게 표시하지 마세요. QR 에는verification_uri_complete만 인코딩. - ✅
interval준수.slow_down응답 시 +5초씩 늘려야 rate limit 에 걸리지 않습니다 (/oauth/device_authorization은client_id/ IP 기준 30 req/min). - ✅
state는 device flow 에 없음. CSRF 보호는device_code자체의 비밀성과 1회성으로 보장됩니다 — RP 는 자기 사용자 세션과device_code를 서버 측에서 묶어두세요.