사전 OpenID Connect 디스커버리
프로토콜

OpenID Connect 디스커버리

gabury1고친 사람 github-actions[bot]

OpenID Connect 디스커버리는 앱이 로그인에 필요한 주소들을 스스로 찾아내게 해 줍니다. 로그인을 맡아 주는 인증 서버는 자기 주소와 지원 기능을 적은 설정 문서를 약속된 경로에 올려 둡니다. 앱은 인증 서버의 주소 하나만 알면 그 문서를 읽어 나머지 주소를 전부 얻습니다.

쉽고 빠른 이해

이 방식은 인증 서버가 자기 사용법을 문서 한 장으로 내걸게 합니다. 사내 도구에 회사 로그인을 붙일 때 개발자는 인증 서버 주소 한 줄만 설정에 넣습니다. 로그인 화면 주소와 토큰 받는 주소, 서명 확인용 키가 있는 주소는 앱이 그 문서에서 읽어 옵니다.

이게 없으면 이 주소들을 사람이 하나하나 옮겨 적어야 합니다. 한 글자만 틀려도 로그인이 안 됩니다. 인증 서버가 주소나 키를 바꾸면 앱은 그 사실을 모른 채 멈춥니다.

어떻게 도나:

  1. 앱이 인증 서버 주소 뒤에 정해진 경로를 붙여 설정 문서를 받습니다
  2. 문서에 적힌 인증 서버 주소가 내가 아는 주소와 같은지 봅니다
  3. 문서에 적힌 주소로 로그인을 보냅니다. 로그인 결과에 붙은 서명을 확인할 키도 받아 둡니다

대가는 앱이 인증 서버의 말을 믿고 따른다는 것입니다. 문서를 받는 길이 뚫리면 가짜 키를 믿게 됩니다. 앱이 뜰 때 문서를 읽게 해 두면 인증 서버가 멈춘 동안 앱도 뜨지 못합니다.

로그인을 직접 받는 서비스에는 필요 없습니다. 설정 문서를 내걸지 않는 인증 서버에도 쓸 수 없습니다.

상세

이 절은 앱이 인증 서버 주소 한 줄에서 출발해 로그인에 필요한 것을 모두 얻는 과정을 따라갑니다. 먼저 앱이 무엇을 미리 알아야 하는지 봅니다. 그다음 설정 문서가 어디에 있고 무엇이 적혀 있는지, 앱이 그 문서를 어떤 순서로 읽고 무엇을 맞춰 보는지를 봅니다.

부르는 이름부터 정해 둡니다. 로그인을 시켜 주는 서버는 이 편에서 인증 서버입니다. 로그인을 맡기는 내 서비스는 앱입니다. 영어로는 인증 서버를 OpenID Provider, 앱을 Relying Party라고 부릅니다.

앱이 미리 알아야 하는 주소

OpenID Connect 는 로그인을 남의 서버에 맡기고 그 결과만 받아 오는 프로토콜입니다. 앱은 사용자를 인증 서버로 보내 로그인시킵니다. 그러면 로그인한 사람이 누구인지 적힌 ID 토큰을 돌려받습니다.

이 한 번의 로그인에서 앱은 인증 서버의 주소를 여럿 씁니다. 서버가 한 가지 일을 받으려고 열어 둔 주소가 엔드포인트입니다. 사용자를 보내는 로그인 엔드포인트가 있고, 토큰을 내주는 엔드포인트가 따로 있습니다.

주소만 있는 것도 아닙니다. 앱은 ID 토큰의 서명을 확인하려고 인증 서버의 공개 키를 갖고 있어야 합니다. 공개 키는 인증 서버가 서명에 쓴 개인 키의 짝입니다. 이 키로 서명이 확인되면 그 서버가 서명한 토큰입니다.

이 값들은 인증 서버마다 다릅니다. 사람이 설정 파일에 옮겨 적으면 오타가 끼어듭니다. 인증 서버가 주소를 옮기거나 키를 바꾸면 앱은 알 길이 없습니다. 디스커버리는 이 값들을 인증 서버가 직접 내걸고 앱이 읽어 가게 해서 이 문제를 풉니다.

발급자 주소

앱이 끝까지 손으로 적는 값은 하나뿐입니다. 인증 서버를 가리키는 이 주소가 발급자 주소입니다. ID 토큰을 발급하는 쪽이라서 붙은 이름입니다.

발급자 주소는 https://id.example 처럼 생겼습니다. 한 인증 서버가 여러 조직을 따로 받으면 https://id.example/tenant-a 처럼 경로가 붙기도 합니다. 이 값은 ID 토큰 안에도 iss 라는 이름으로 적힙니다.

설정 문서가 놓이는 경로

설정 문서의 주소는 발급자 주소에서 계산됩니다. 발급자 주소 끝에 /.well-known/openid-configuration 을 붙이면 됩니다. 발급자 주소가 빗금으로 끝나면 그 빗금을 떼고 붙입니다.

발급자 주소 설정 문서 주소
https://id.example https://id.example/.well-known/openid-configuration
https://id.example/tenant-a https://id.example/tenant-a/.well-known/openid-configuration

/.well-known/ 은 웹 서버들이 이런 설정 문서를 올려 두기로 약속한 경로 앞머리입니다. 웹에서 자원을 가리키는 주소를 URI(Uniform Resource Identifier, 자원 식별자)라고 합니다. 이 앞머리 아래의 URI 가 웰노운 URI입니다. 경로가 약속돼 있어서 앱은 인증 서버에게 따로 물어보지 않고도 문서를 찾아갑니다.

설정 문서에 적히는 값

설정 문서는 JSON(JavaScript Object Notation, 자바스크립트 객체 표기법) 객체 하나입니다. JSON 은 이름과 값의 짝을 중괄호로 묶어 적는 글자 형식입니다. 대표 값만 남기면 아래처럼 생겼습니다.

JSON
{
  "issuer": "https://id.example",
  "authorization_endpoint": "https://id.example/authorize",
  "token_endpoint": "https://id.example/token",
  "userinfo_endpoint": "https://id.example/userinfo",
  "jwks_uri": "https://id.example/jwks",
  "scopes_supported": ["openid", "profile", "email"],
  "response_types_supported": ["code"],
  "id_token_signing_alg_values_supported": ["RS256"]
}

값은 두 부류로 나뉩니다. _endpoint 와 _uri 로 끝나는 값은 앱이 부를 주소입니다. _supported 로 끝나는 값은 인증 서버가 받아 주는 방식의 목록입니다.

주소 값부터 봅니다. 표의 인가 코드는 로그인이 끝나면 인증 서버가 앱에 건네는 한 번짜리 교환권입니다. 앱은 이 교환권을 토큰으로 바꿔 받습니다.

이름 가리키는 곳 앱이 쓰는 때
issuer 발급자 주소 이 문서와 ID 토큰을 누가 냈는지 맞춰 볼 때
authorization_endpoint 사용자를 보내 로그인시키는 주소 로그인 버튼이 눌렸을 때
token_endpoint 인가 코드를 토큰으로 바꾸는 주소 로그인을 마친 사용자가 돌아왔을 때
userinfo_endpoint 이름·이메일 같은 사용자 정보를 내주는 주소 프로필 값이 더 필요할 때
jwks_uri 공개 키 묶음을 올려 둔 주소 ID 토큰의 서명을 확인할 때

jwks_uri 가 가리키는 문서를 이 편에서는 공개 키 묶음이라고 부릅니다. 인증 서버가 서명에 쓰는 공개 키를 여러 개 모아 둔 JSON 입니다.

묶음 속 키 하나하나는 JWK(JSON Web Key, JSON 웹 키)입니다. JWK 는 공개 키 하나를 JSON 객체 하나로 적은 것입니다.

키마다 kid(key ID, 키 이름)라는 이름이 붙습니다. 묶음에 키가 여럿이라서, 앱은 토큰을 확인할 때 이 이름으로 어느 키를 쓸지 고릅니다.

_supported 목록은 앱이 요청을 만들 때 고를 방식을 알려 줍니다. scopes_supported 는 앱이 청할 수 있는 스코프입니다. 스코프는 사용자 정보 가운데 무엇을 받을지 고르는 이름입니다.

response_types_supported 는 로그인이 끝났을 때 앱이 무엇을 돌려받을지 고르는 방식의 목록입니다. 예의 code 는 앞에서 푼 인가 코드를 돌려받겠다는 뜻입니다.

id_token_signing_alg_values_supported 는 인증 서버가 ID 토큰 서명에 쓰는 방식입니다. 예의 RS256 은 그런 서명 방식 하나의 이름입니다.

발급자 주소, 로그인 주소, 공개 키 묶음 주소와 지원 방식 몇 가지는 문서에 반드시 들어갑니다. 사용자 정보 주소는 넣기를 권하는 값입니다. 그래서 앱은 없는 값을 만나도 멈추지 않게 짭니다.

설정 문서를 읽는 순서

앱이 문서를 받아 로그인을 받을 준비를 마치기까지는 두 번의 요청이 오갑니다. 설정 문서를 한 번 받고, 거기 적힌 주소로 공개 키 묶음을 한 번 더 받습니다.

sequenceDiagram
    participant 앱
    participant 인증서버 as 인증 서버
    앱->>인증서버: 설정 문서를 청한다 · /.well-known/openid-configuration
    인증서버-->>앱: 설정 문서 · JSON
    Note over 앱: issuer 가 발급자 주소와 같은지 본다
    앱->>인증서버: jwks_uri 로 공개 키 묶음을 청한다
    인증서버-->>앱: 공개 키 묶음
    Note over 앱: 두 문서를 받아 두고 로그인을 받기 시작한다

두 요청 모두 평범한 조회 요청입니다. 로그인한 사용자가 없어도 되고 앱이 누구인지 밝힐 필요도 없습니다. 설정 문서는 누구나 읽으라고 내건 문서입니다.

앱은 받은 문서를 곧장 믿지 않고 issuer 부터 봅니다. 이 값이 앱이 문서를 받을 때 쓴 발급자 주소와 글자 하나까지 같아야 합니다. 끝의 빗금 하나가 달라도 다른 주소로 칩니다.

이 확인은 문서가 다른 인증 서버 행세를 하는 것을 막습니다. 누구든 자기 서버에 설정 문서를 올리고 issuer 칸에 남의 발급자 주소를 적을 수 있습니다. 앱이 그 칸을 믿으면 그 서버의 키로 서명한 토큰을 남의 인증 서버가 낸 것으로 받아들입니다.

앱이 발급자 주소를 바깥에서 얻을 때 이런 문서가 끼어들 틈이 생깁니다. 뒤에서 볼 것처럼 사용자가 댄 계정으로 발급자 주소를 찾으면, 그 답이 공격자의 서버를 가리킬 수 있습니다. 두 주소가 같아야 이 문서가 그 주소에 있는 인증 서버의 것입니다. 로그인 뒤에 받는 ID 토큰의 iss 도 같은 값이어야 합니다.

문서를 받는 길을 믿는 까닭

설정 문서는 HTTPS(HyperText Transfer Protocol Secure, 암호화된 웹 통신)로만 받습니다. HTTPS 는 받은 내용이 그 주소의 서버가 보낸 것이고 가는 길에 바뀌지 않았음을 보장합니다.

이 길이 중요한 까닭은 믿음이 줄줄이 이어지기 때문입니다. 앱은 발급자 주소를 믿어서 설정 문서를 믿습니다. 설정 문서를 믿어서 거기 적힌 공개 키 묶음을 믿습니다. 공개 키 묶음을 믿어서 ID 토큰을 믿습니다.

누가 중간에서 설정 문서를 바꿔치면 이 사슬이 처음부터 끊깁니다. 가짜 문서가 가짜 공개 키 묶음을 가리키면 가짜 키로 서명한 토큰이 확인을 통과합니다. 통신 중간에 끼어들어 내용을 바꾸는 이런 공격이 중간자 공격입니다.

발급자 주소를 모를 때

대부분의 앱은 어느 인증 서버를 쓸지 미리 정해 두고 발급자 주소를 설정에 적습니다. 그런데 사용자가 [email protected] 같은 계정만 대고 로그인하려는 경우도 있습니다. 이때 앱은 발급자 주소부터 찾아야 합니다.

이 단계에는 WebFinger 라는 조회 방식을 씁니다. 앱은 계정의 도메인인 example.com 의 /.well-known/webfinger 경로에 이 계정의 발급자 주소를 묻습니다. 돌아온 답에 발급자 주소가 있으면, 거기서부터 앞 소절의 순서를 밟습니다. 발급자 주소를 이미 아는 앱은 이 단계를 건너뜁니다.

받아 둔 문서를 다시 읽는 때

설정 문서의 주소 값은 자주 바뀌지 않습니다. 그래서 앱은 문서를 한 번 읽어 캐시해 두고 한동안 다시 받지 않습니다. 캐시는 받아 온 값을 가까이 보관해 두고 되풀이해 쓰는 일입니다.

공개 키 묶음은 사정이 다릅니다. 인증 서버는 보안을 위해 서명 키를 이따금 바꿉니다. 이렇게 키를 갈아 끼우는 일이 키 교체입니다. 키를 바꾸는 동안에는 옛 키와 새 키가 묶음에 함께 들어 있습니다.

받아 둔 묶음이 낡으면 새 키로 서명한 토큰을 확인하지 못합니다. 앱은 토큰이 어느 키로 서명됐는지를 보고 묶음이 낡았는지 알아챕니다.

ID 토큰은 여러 토막을 점으로 이어 붙인 글자열입니다. 맨 앞 토막을 헤더라고 합니다. 헤더에는 서명에 쓴 키의 이름이 kid 로 들어 있습니다.

앱은 이 kid 를 받아 둔 묶음의 키 이름과 맞춰 봅니다. 그 이름의 키가 없으면 앱은 jwks_uri 를 다시 읽어 묶음을 새로 받습니다.

쓰는 경우와 안 쓰는 경우

OpenID Connect 로 로그인을 붙일 때는 거의 늘 이 방식으로 설정합니다. 로그인 라이브러리 대부분이 발급자 주소 하나를 받아 나머지를 이 방식으로 채웁니다. 인증 서버가 여럿이어도 발급자 주소만 늘리면 됩니다.

인증 서버가 설정 문서를 내걸지 않으면 쓸 수 없습니다. 그때는 엔드포인트와 공개 키 묶음 주소를 손으로 적습니다.

OAuth 만 지원하는 서버가 흔히 이 경우입니다. OAuth 는 앱이 남의 서비스를 부를 권한만 넘겨받는 프로토콜입니다. 그 권한은 액세스 토큰이라는 출입증에 담겨 옵니다. OpenID Connect 는 이 OAuth 위에 로그인을 얹은 프로토콜입니다.

OAuth 서버도 같은 꼴의 설정 문서를 내걸 수 있습니다. 이 문서가 인가 서버 메타데이터입니다. OAuth 서버가 자기 엔드포인트와 지원 방식을 적어 약속된 경로에 올려 둔 JSON 입니다.

로그인을 남에게 맡기지 않는 서비스에는 필요 없습니다. 아이디와 비밀번호를 직접 받아 세션을 여는 서비스는 부를 인증 서버가 없습니다.

대가는 둘입니다. 첫째로 앱은 인증 서버의 문서를 믿고 따릅니다. 문서를 받는 길이 뚫리면 앱 전체가 속습니다. 앞에서 본 HTTPS 와 issuer 확인이 이 대가를 막는 장치입니다.

둘째로 앱이 인증 서버에 기대는 시점이 하나 늘어납니다. 앱이 뜰 때 설정 문서를 읽게 해 두면, 인증 서버가 멈춘 동안에는 앱도 뜨지 못합니다. 받아 둔 문서를 들고 있다가 읽기가 실패하면 그것을 쓰는 식으로 이 의존을 줄입니다.

관련 항목

OpenID Connect 디스커버리가 속하는 상위 프로토콜

OpenID Connect · OAuth 2.0 · HTTP · HTTPS · TLS

설정 문서가 가리키는 엔드포인트

엔드포인트 · 인가 엔드포인트 · 토큰 엔드포인트 · UserInfo 엔드포인트 · JWKS · 로그아웃 엔드포인트

설정 문서를 이루는 형식과 값

JSON · 웰노운 URI · URI · 스코프 · 클레임 · 서명 알고리즘 · 응답 타입

설정 문서로 받은 키가 검증하는 토큰

ID 토큰 · JWT · JWS · JWK · 액세스 토큰 · 인가 코드

설정 문서를 주고받는 역할

인증 서버 · 인가 서버 · 신뢰 당사자 · 클라이언트 · 신원 공급자

발급자 주소와 키를 찾아 쓰는 이웃 방식

WebFinger · 인가 서버 메타데이터 · 동적 클라이언트 등록 · 키 교체 · 캐시 · 서비스 디스커버리

디스커버리가 믿음을 기대는 보안 수단

공개 키 암호 · 개인 키 · 전자 서명 · 인증서 · 중간자 공격 · 발급자 혼동 공격

OpenID Connect 디스커버리가 쓰이는 로그인 방식

싱글 사인온 · 페더레이션 · 인가 코드 흐름 · PKCE · 인증

다른 이름: OpenID Connect Discovery · OIDC Discovery · OIDC 디스커버리