사전 암묵적 흐름
프로토콜

암묵적 흐름

gabury1고친 사람 github-actions[bot]

암묵적 흐름은 브라우저에서만 도는 앱에 액세스 토큰을 곧바로 건네줍니다. 액세스 토큰은 앱이 남의 서비스에서 사용자의 데이터를 읽을 때 내미는 값입니다. 사용자가 로그인 화면에서 허락하면 토큰이 브라우저 주소 끝에 실려 앱으로 돌아옵니다. 토큰이 주소에 드러나는 탓에 지금은 쓰지 않기를 권합니다.

쉽고 빠른 이해

암묵적 흐름은 웹 앱이 사용자의 허락을 받자마자 입장권을 손에 쥐게 해 줍니다. 입장권은 앱이 남의 서비스에서 내 데이터를 읽을 때 내미는 문자열입니다. 사진 편집 웹 앱에 내 사진첩 읽기를 허락하면, 브라우저가 앱 화면으로 돌아오면서 주소 끝에 입장권을 달고 옵니다.

보통은 앱이 허락의 증표를 먼저 받습니다. 그러면 앱의 뒷단 서버가 그 증표를 비밀값과 함께 내밀어 입장권으로 바꿉니다. 비밀값은 앱이 자기가 누구인지 밝히는 암호입니다.

암묵적 흐름은 이런 뒷단 서버가 없는 앱을 위해 만들었습니다. 화면만 있는 앱은 비밀값을 숨길 곳이 없습니다. 예전 브라우저는 다른 도메인의 서버를 마음대로 부르지 못했습니다.

어떻게 도나:

  1. 앱이 사용자를 로그인 화면으로 보냅니다
  2. 사용자가 앱에 허락을 내립니다
  3. 로그인 화면이 사용자를 앱으로 돌려보내면서 주소 끝에 입장권을 붙입니다
  4. 앱 화면의 스크립트가 주소에서 입장권을 꺼냅니다

대가는 입장권이 주소에 드러난다는 것입니다. 방문 기록에 남습니다. 페이지 안의 다른 스크립트도 읽을 수 있습니다. 그래서 새로 만드는 앱은 증표를 거치는 보통 길, 곧 인가 코드 방식을 씁니다.

상세

공연 입장권을 받는 길이 둘 있다고 해 봅니다. 하나는 문자로 받은 예매 번호를 매표소에 내밀어 입장권으로 바꾸는 길입니다. 다른 하나는 입장권을 집 앞 우편함에 바로 넣어 주는 길입니다. 우편함 쪽이 빠르지만, 우편함은 지나가는 누구든 열어 볼 수 있습니다.

암묵적 흐름이 우편함 쪽입니다. 예매 번호 쪽은 인가 코드를 거치는 방식입니다. 인가 코드는 토큰으로 바꿔야 쓸 수 있는 중간 증표입니다.

이 절은 먼저 암묵적 흐름이 어떤 틀 안에서 나왔는지 봅니다. 이어서 토큰이 오가는 길과 그때 실리는 값을 따라갑니다. 끝으로 왜 이렇게 만들었는지, 무엇이 새는지, 지금은 무엇으로 대신하는지를 가릅니다.

OAuth 와 그랜트

암묵적 흐름은 OAuth 2.0 에서 나옵니다. OAuth(Open Authorization)는 사용자의 비밀번호를 앱에 넘기지 않고도 앱이 그 사용자의 데이터를 쓰게 해 주는 권한 위임 규격입니다. 사진 편집 앱이 내 사진첩을 읽는 경우가 그렇습니다.

이 흐름에는 네 쪽이 등장합니다.

이름 하는 일
사용자 데이터의 임자입니다. 앱에 허락을 내립니다
클라이언트 사용자 대신 데이터를 쓰려는 앱입니다. 이 글에서는 앱이라고 부릅니다
인가 서버 사용자에게 허락을 묻고 액세스 토큰을 내줍니다
자원 서버 데이터를 들고 있습니다. 액세스 토큰을 보고 요청을 받아 줍니다

OAuth 에는 앱이 토큰을 받아 내는 방식이 여럿 있습니다. 그 방식 하나하나를 그랜트라고 합니다. 암묵적 흐름은 그랜트 가운데 하나입니다. 영어로는 implicit grant 라고도 씁니다.

이름의 「암묵적」은 중간 단계가 없다는 뜻입니다. 다른 그랜트는 인가 코드 같은 중간 증표를 먼저 받고 그것을 토큰으로 바꿉니다. 암묵적 흐름에서는 사용자가 허락한 순간 토큰이 바로 나옵니다.

토큰이 오가는 길

토큰은 리다이렉트를 타고 옵니다. 리다이렉트는 서버가 브라우저에게 다른 주소로 가라고 응답해 브라우저를 옮겨 보내는 일입니다. 앱은 사용자를 인가 서버로 보냅니다. 인가 서버는 허락이 끝나면 사용자를 앱으로 되돌려 보냅니다.

되돌려 보내는 주소에서 토큰은 프래그먼트에 실립니다. 프래그먼트는 주소에서 # 뒤에 오는 부분입니다. 원래는 한 페이지 안의 위치를 가리키는 데 씁니다.

프래그먼트에는 특별한 성질이 하나 있습니다. 브라우저는 페이지를 요청할 때 프래그먼트를 서버로 보내지 않습니다. 그래서 토큰은 브라우저 안에만 머뭅니다. 앱의 화면 파일을 내려 주는 웹 서버에도 닿지 않습니다. 이 웹 서버는 파일만 내려 줄 뿐, 토큰을 받아 처리하는 뒷단 서버가 아닙니다.

sequenceDiagram
    participant 앱 as 앱 화면(브라우저 안 스크립트)
    participant 브라우저
    participant 인가서버 as 인가 서버
    participant 자원서버 as 자원 서버
    앱->>브라우저: 인가 서버로 보낸다
    브라우저->>인가서버: 앱이 청한 허락을 들고 간다
    인가서버->>브라우저: 로그인과 허락을 묻는다
    브라우저->>인가서버: 사용자가 허락을 누른다
    인가서버->>브라우저: 앱으로 가라 · 주소 끝에 토큰을 붙여서
    브라우저->>앱: 주소의 프래그먼트를 넘긴다
    앱->>자원서버: 토큰을 내밀고 데이터를 청한다

그림에서 토큰은 인가 서버에서 나와 브라우저를 지나 앱 화면에 닿습니다. 인가 코드를 거치는 방식과 달리 앱이 인가 서버를 따로 부르는 왕복이 없습니다. 토큰이 오가는 길 전부가 브라우저를 지납니다. 브라우저를 지나는 이런 길이 프런트 채널입니다.

오가는 값

앱이 사용자를 인가 서버로 보낼 때의 주소부터 봅니다. 요청은 인가 서버의 인가 엔드포인트로 갑니다. 인가 엔드포인트는 사용자에게 허락을 묻는 인가 서버의 창구입니다.

GET /authorize          // 허락 창구
  ?response_type=token  // 토큰을 바로
  &client_id=photo-app  // 앱 아이디
  &redirect_uri=…       // 돌아올 주소
  &scope=photos.read    // 허락 범위
  &state=xyz            // 위조 확인값

response_type=token 이 암묵적 흐름을 고르는 값입니다. 인가 코드를 받으려면 이 값이 code 가 됩니다.

돌아올 주소는 리다이렉션 URI 라고 합니다. URI(Uniform Resource Identifier)는 여기서 주소로 읽으면 됩니다. 이 주소는 앱이 인가 서버에 미리 등록해 둔 것이어야 합니다. 그래야 인가 서버가 토큰을 엉뚱한 곳으로 보내지 않습니다.

scope 는 사용자가 허락하는 범위인 스코프입니다. 위 요청은 사진을 읽는 것만 허락해 달라고 청합니다.

state 는 앱이 만든 추측하기 어려운 값입니다. 돌아온 값이 이 값과 같아야 앱이 시작한 요청에 온 응답이라고 믿을 수 있습니다.

사용자가 허락하면 인가 서버는 브라우저를 아래 주소로 돌려보냅니다. 토큰은 ? 가 아니라 # 뒤에 있습니다.

https://app.example/cb  // 돌아올 주소
  #access_token=2YotnF  // 토큰
  &token_type=Bearer    // 토큰 종류
  &expires_in=3600      // 수명(초)
  &state=xyz            // 보낸 값과 같다

token_type=Bearer 는 이 토큰이 Bearer 토큰이라는 뜻입니다. Bearer 토큰은 가진 사람이면 누구든 쓸 수 있는 토큰입니다. 앱이 받았는지 남이 주웠는지를 토큰은 가리지 않습니다.

이 주소가 열리면 앱 화면의 스크립트가 # 뒤를 읽어 토큰을 꺼냅니다.

JavaScript
const q = location.hash.slice(1);
const p = new URLSearchParams(q);
p.get("access_token") // "2YotnF"
p.get("state")        // "xyz"

location.hash 는 주소의 # 부터 끝까지입니다. 맨 앞의 # 을 떼고 나면 이름=값 이 & 로 이어진 문자열이 남습니다. 앱은 꺼낸 state 를 처음 보낸 값과 맞춰 본 다음 토큰을 씁니다.

응답에 없는 값이 하나 있습니다. 토큰이 만료됐을 때 새 토큰을 받는 리프레시 토큰은 암묵적 흐름에서 나오지 않습니다. 토큰이 만료되면 앱은 사용자를 인가 서버로 다시 보내야 합니다.

서버 없는 앱을 위해 만든 까닭

암묵적 흐름이 겨냥한 앱은 단일 페이지 애플리케이션입니다. 화면 전체를 브라우저의 JavaScript 로 그리는 웹 앱입니다. 화면 파일을 내려 주는 웹 서버는 있어도, 토큰을 받아 처리할 뒷단 서버는 따로 두지 않습니다.

이런 앱은 비밀값을 숨길 곳이 없습니다. 비밀값은 앱이 인가 서버에 등록할 때 받는 문자열입니다. 인가 서버는 이것으로 어느 앱이 찾아왔는지 확인합니다. 그런데 브라우저로 내려간 코드는 누구든 열어 볼 수 있습니다. 비밀값을 숨길 수 없는 이런 앱이 공개 클라이언트입니다.

인가 코드를 토큰으로 바꾸려면 앱이 인가 서버의 토큰 엔드포인트를 직접 불러야 합니다. 토큰 엔드포인트는 코드를 토큰으로 바꿔 주는 인가 서버의 창구입니다. 교환할 때 앱은 코드와 함께 비밀값을 내서 자기가 누구인지 밝힙니다.

그런데 예전 브라우저의 스크립트는 다른 도메인의 서버로 요청을 보내기 어려웠습니다. 이 제약을 푸는 CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)가 아직 널리 쓰이기 전이었습니다.

이런 앱은 비밀값을 숨기지 못합니다. 토큰 엔드포인트도 부르지 못했습니다. 그래서 암묵적 흐름은 교환 단계를 뺐습니다.

토큰을 프래그먼트에 실은 것도 같은 고민에서 나왔습니다. 화면 파일을 내려 주는 웹 서버의 로그에 토큰이 남지 않게 하려는 것입니다.

토큰을 주소에 실은 대가

토큰을 브라우저 주소에 싣고, 그 토큰을 받은 앱이 누구인지 확인하지 않는다는 두 가지 선택이 대가를 만듭니다. 이 소절은 그 대가를 하나씩 봅니다.

먼저 주소는 여러 곳에 남습니다. 브라우저의 방문 기록이 그렇습니다. 돌아올 주소가 다시 다른 주소로 리다이렉트하면 프래그먼트가 다음 주소에 따라붙기도 합니다. 아무 곳으로나 넘겨 주는 페이지, 곧 오픈 리다이렉터가 끼면 토큰이 공격자의 사이트까지 실려 갑니다.

다음으로 페이지 안의 스크립트는 전부 주소를 읽을 수 있습니다. 광고 스크립트나 분석 스크립트도 같은 페이지에서 돕니다. 공격자가 페이지에 자기 스크립트를 심는 교차 사이트 스크립팅이 하나라도 뚫리면 토큰을 그대로 가져갑니다.

셋째로 앱은 받은 토큰이 자기 몫인지 모릅니다. 인가 코드 방식에서는 앱이 비밀값을 내고 토큰을 직접 받습니다. 그래서 그 토큰은 그 앱에 내준 것입니다. 암묵적 흐름에서는 토큰이 주소에 실려 올 뿐입니다.

공격자가 다른 앱에서 얻은 남의 토큰을 주소에 끼워 넣어도 앱은 구분하지 못합니다. 앱이 그 토큰을 자기 사용자의 것으로 믿으면, 공격자는 토큰 임자인 사람으로 앱에 들어가 그 사람의 데이터를 봅니다. 이런 공격을 토큰 주입이라고 합니다.

대신 쓰는 인가 코드와 PKCE

지금은 암묵적 흐름을 쓰지 않기를 강하게 권합니다. 서버 없는 앱도 인가 코드를 거쳐 토큰을 받습니다. 그 위에 PKCE(Proof Key for Code Exchange, 코드 교환 증명 키)를 얹습니다. OAuth 2.0 을 정리해 다시 쓰는 OAuth 2.1 에서는 암묵적 흐름이 아예 빠졌습니다.

PKCE 는 비밀값 없이도 코드를 가로챈 쪽이 토큰을 못 받게 막는 장치입니다. 앱은 요청마다 무작위 문자열을 하나 만들어 혼자 간직합니다. 인가 서버로 사용자를 보낼 때는 그 문자열을 되돌릴 수 없게 뒤섞은 값만 실어 보냅니다.

코드를 토큰으로 바꿀 때 앱은 간직한 원래 문자열을 냅니다. 인가 서버는 그 문자열을 같은 방식으로 뒤섞어, 처음 받은 값과 같은지 봅니다. 코드만 가로챈 쪽은 원래 문자열을 모르니 토큰을 못 받습니다.

이제는 브라우저가 CORS 로 토큰 엔드포인트를 부를 수 있습니다. 그래서 암묵적 흐름이 교환 단계를 뺐던 까닭이 사라졌습니다. 두 방식을 견주면 이렇습니다.

암묵적 흐름 인가 코드 + PKCE
브라우저 주소에 실리는 것 액세스 토큰 한 번 쓰고 버리는 코드
토큰을 받은 쪽 확인 안 한다 간직한 문자열로 확인한다
리프레시 토큰 안 나온다 나올 수 있다
왕복 인가 서버를 한 번 들른다 토큰 엔드포인트 호출이 하나 더 있다

표의 마지막 줄이 인가 코드 쪽이 치르는 대가입니다. 앱이 토큰 엔드포인트를 한 번 더 불러야 합니다. 그 왕복 하나로 토큰이 주소에 드러나지 않게 됩니다.

쓰는 경우와 안 쓰는 경우

새로 만드는 앱에는 쓰지 않습니다. 서버가 없는 웹 앱이든 휴대폰 앱이든 인가 코드에 PKCE 를 얹습니다.

암묵적 흐름을 만나는 것은 대개 오래된 코드에서입니다. 로그인 요청에 response_type=token 이 보이면 이 흐름입니다. 옮길 때는 그 값을 code 로 바꿉니다. 그리고 토큰 엔드포인트를 부르는 단계와 PKCE 를 더합니다.

OpenID Connect 는 OAuth 위에 「이 사람이 누구인가」를 얹어 로그인에 쓰는 규격입니다. OAuth 의 토큰은 앱이 무엇을 해도 되는지만 알려 줍니다. 사용자가 누구인지는 따로 알려 주지 않습니다.

OpenID Connect 에도 암묵적 흐름이라는 이름의 방식이 있습니다. 이쪽은 로그인한 사람이 누구인지 적은 ID 토큰을 프래그먼트에 실어 보냅니다. 토큰이 주소에 드러나는 대가도 같습니다.

관련 항목

암묵적 흐름에 등장하는 참여자

인가 서버 · 클라이언트 · 자원 서버 · 자원 소유자 · 사용자 에이전트 · 공개 클라이언트 · 기밀 클라이언트

암묵적 흐름이 내주는 토큰

액세스 토큰 · Bearer 토큰 · ID 토큰 · 리프레시 토큰 · 토큰 · JWT

토큰이 지나가는 창구와 주소

인가 엔드포인트 · 토큰 엔드포인트 · 리다이렉션 URI · 리다이렉트 · 프래그먼트 · 프런트 채널 · 백 채널 · 동의 화면

암묵적 흐름을 정의하는 표준·프로토콜

OAuth 2.0 · OAuth 2.1 · OpenID Connect · 인가 그랜트 · OAuth 2.0 보안 모범 사례 · 브라우저 기반 앱을 위한 OAuth 2.0

암묵적 흐름을 대신하는 그랜트

인가 코드 · 인가 코드 그랜트 · 인가 코드 흐름 · PKCE · 클라이언트 자격증명 그랜트 · 기기 인가 그랜트 · 리소스 소유자 비밀번호 자격증명 그랜트

암묵적 흐름에 덧대는 보호 장치

state 파라미터 · nonce · 스코프 · 클라이언트 시크릿 · 클라이언트 인증 · 송신자 제약 토큰

암묵적 흐름의 토큰을 노리는 공격

토큰 주입 · 토큰 탈취 · 교차 사이트 스크립팅 · 오픈 리다이렉터 · 크로스 사이트 요청 위조 · 재전송 공격

암묵적 흐름이 기대는 브라우저 기능

브라우저 · 단일 페이지 애플리케이션 · JavaScript · CORS · 서드파티 쿠키 · 방문 기록 · 동일 출처 정책

암묵적 흐름이 속하는 상위 분류

인가 · 인증과 인가 · 권한 위임 · API 보안 · API 설계 · 싱글 사인온

다른 이름: implicit flow · implicit grant · 암묵적 그랜트 · 임플리싯 그랜트 · 임플리싯 플로우 · 묵시적 그랜트