사전 OpenID Connect
프로토콜

OpenID Connect

gabury1고친 사람 github-actions[bot]

OpenID Connect 는 남의 서버에서 로그인한 사람이 누구인지를 내 서버가 확인하게 해 주는 프로토콜입니다. 사용자의 비밀번호는 내 서버로 오지 않습니다. 대신 로그인을 시켜 준 서버가 서명한 증명서 한 장이 건너옵니다. 다른 서비스의 계정으로 로그인하는 버튼 뒤에서 도는 것이 대개 이것입니다.

쉽고 빠른 이해

OpenID Connect 는 로그인을 남의 서버에 맡기고 그 결과만 받아 오는 방법입니다. 사내 위키가 직원 비밀번호를 직접 들고 있지 않아도, 회사 인증 서버에 로그인시킨 뒤 「이 사람은 사번 248 번입니다」라고 서명된 쪽지를 받아 그 사람을 들여보냅니다.

이게 없으면 서비스마다 비밀번호를 따로 받아 보관해야 합니다. 보관하는 데가 하나 늘 때마다 새어 나갈 데도 하나 늘어납니다.

어떻게 도나:

  1. 내 서버가 사용자를 인증 서버로 보내 로그인시킵니다
  2. 인증 서버가 서명한 쪽지를 내 서버로 돌려보냅니다
  3. 내 서버는 서명과 값 몇 개만 맞춰 보고 그 사람을 로그인시킵니다

대가는 손이 늘어난다는 것입니다. 오가는 단계가 늘어납니다. 서명을 검증할 키도 미리 받아 두어야 합니다. 인증 서버가 멈추면 내 서비스도 로그인이 안 됩니다.

계정을 내가 들고 있고 로그인할 곳도 하나뿐이면 안 씁니다. 그때는 아이디·비밀번호를 직접 받는 쪽이 부품이 적습니다.

상세

이 절은 로그인 한 번이 어떻게 이루어지는지를 순서대로 따라갑니다. 먼저 이 프로토콜이 OAuth 에서 무엇을 가져오고 무엇을 더하는지 봅니다. 그다음 오가는 ID 토큰의 내용과, 그 토큰을 믿기 전에 맞춰 볼 것을 봅니다.

부르는 이름부터 정해 둡니다. 로그인을 시켜 주는 쪽은 이 편에서 인증 서버입니다. 로그인을 맡기는 내 서비스는 앱입니다.

OpenID Connect 규격은 이 둘을 OpenID Provider 와 Relying Party(신뢰 당사자)라고 적습니다. OAuth 쪽 문서는 인증 서버를 인가 서버라고 부릅니다. 가리키는 것은 같습니다.

액세스 토큰이 말해 주지 않는 것

OAuth 는 사용자를 대신해 앱이 남의 서비스를 부르게 해 주는 규격입니다. 사용자가 허락하면 앱은 액세스 토큰을 하나 받습니다. 액세스 토큰은 그 서비스를 부를 때 내미는 출입증입니다.

출입증은 문을 열어 줄 뿐 든 사람이 누구인지는 말하지 않습니다. 액세스 토큰도 대개 뜻 없는 문자열이라 받아서 열어 봐도 누구의 것인지 나오지 않습니다.

그래서 액세스 토큰을 로그인의 증거로 쓰면 구멍이 생깁니다. 다른 앱이 같은 인증 서버에서 받은 토큰을 내 앱에 내밀어도, 내 앱은 그것이 자기를 위해 발급된 것인지 가릴 수 없습니다.

OpenID Connect 는 같은 왕복 위에 신원 확인을 얹어 이 구멍을 막습니다. 요청에는 무엇을 받을지 적는 칸(scope)이 있습니다. 여기에 openid 를 적어 보내면 로그인까지 해 달라는 뜻이 됩니다.

그러면 인증 서버가 액세스 토큰과 함께 ID 토큰을 하나 더 줍니다. ID 토큰은 누가, 어디서, 어느 앱을 위해, 언제 로그인했는지를 적고 서명한 문서입니다.

OAuth 가 이미 하던 것 위에 무엇이 얹히는지를 그림으로 보면 이렇습니다.

flowchart TD
    subgraph OIDC["OpenID Connect 가 얹는 것"]
        I["ID 토큰 · 누구인가"]
    end
    subgraph OA["OAuth 가 이미 하던 것"]
        A["액세스 토큰 · 무엇을 해도 되는가"]
    end
    OIDC --- OA

둘을 따로 받는 것이 아닙니다. 한 번의 왕복으로 아래 칸의 액세스 토큰과 위 칸의 ID 토큰이 함께 건너옵니다.

ID 토큰에 무엇이 담기나

ID 토큰은 JWT(JSON Web Token, JSON 웹 토큰) 형식입니다. JWT 는 값 몇 개를 적고 그 내용에 서명을 붙여 한 줄 문자열로 만든 토큰입니다. 점 두 개로 헤더와 내용과 서명 세 토막이 나뉩니다.

block-beta
columns 5
  a["헤더"]:1 d1["."]:1 b["내용"]:1 d2["."]:1 c["서명"]:1

내용에 들어가는 값 하나하나를 클레임이라고 부릅니다. 아래 코드블록은 가운데 「내용」 토막을 푼 것입니다. 로그인에 쓰이는 대표 클레임들이 들어 있습니다.

{
  "iss": "https://id.example.com", // 발급자
  "sub": "248289761001",           // 사용자 번호
  "aud": "wiki-app",               // 받을 앱
  "exp": 1781000600,               // 만료 시각
  "nonce": "n-0S6WzA2Mj"           // 요청과 짝
}

sub 가 사용자를 가리키는 번호입니다. 한 인증 서버 안에서 사람마다 하나씩 붙고 바뀌지 않습니다. 그래서 내 데이터베이스에서 사용자를 잇는 기준은 이메일이 아니라 iss 와 sub 의 짝입니다.

이메일을 기준으로 삼으면 두 군데서 어긋납니다. 사람이 주소를 바꾸면 다른 사람으로 보입니다. 조직에서는 나간 사람의 주소가 새 사람에게 다시 가기도 합니다.

로그인 한 번의 흐름

브라우저를 쓰는 서비스는 인가 코드를 한 번 거치는 흐름으로 로그인시킵니다. 인가 코드는 토큰으로 바꾸는 한 번짜리 교환권입니다. 브라우저와 앱과 인증 서버 셋이 이렇게 주고받습니다.

sequenceDiagram
    participant 브라우저
    participant 앱
    participant 인증서버
    브라우저->>앱: 로그인 버튼을 누른다
    앱-->>브라우저: 인증 서버로 보낸다 · scope 와 nonce 를 실어서
    브라우저->>인증서버: 아이디와 비밀번호를 낸다
    인증서버-->>브라우저: 앱으로 돌려보낸다 · 인가 코드를 붙여서
    브라우저->>앱: 인가 코드를 전한다
    앱->>인증서버: 인가 코드와 앱의 비밀값을 보낸다
    인증서버-->>앱: ID 토큰과 액세스 토큰
    Note over 앱: 서명과 값을 맞춰 본 뒤 로그인시킨다
    앱->>인증서버: 나중에 · 액세스 토큰을 내밀고 프로필을 청한다
    인증서버-->>앱: 이름 · 사진

마지막 두 줄은 로그인이 끝난 뒤의 왕복입니다. 프로필 값이 필요할 때만 부릅니다. 이 대목은 뒤의 「이름과 사진은 따로 가져온다」에서 다시 봅니다.

앱은 먼저 사용자를 인증 서버로 보냅니다. 이때 요청에 네 가지를 싣습니다.

값 무엇인가
client_id 앱을 가리킨다
redirect_uri 돌아올 주소
scope 무엇을 받을지 정한다
nonce 이 요청을 나중에 알아보려고 앱이 만들어 넣는 한 번짜리 값

nonce 는 앱이 매번 새로 지어 보내는 값입니다. 돌아온 토큰에 같은 값이 적혀 있어야 내가 방금 낸 요청의 답이라는 것을 압니다.

사용자는 인증 서버의 화면에서 로그인합니다. 비밀번호는 그 화면에만 들어가고 앱은 보지 못합니다.

인증 서버는 사용자를 다시 앱으로 돌려보냅니다. 이때 짧은 문자열 하나를 붙입니다. 이것이 인가 코드입니다. 한 번 쓰면 다시 못 씁니다.

앱은 브라우저를 거치지 않고 인증 서버를 직접 부릅니다. 인가 코드와 앱만 아는 비밀값을 함께 보내면 ID 토큰과 액세스 토큰이 돌아옵니다.

이렇게 한 단계를 더 거치는 까닭은 토큰이 브라우저를 지나가지 않게 하려는 것입니다. 주소창을 지난 값은 기록에 남고 옆 사람 눈에도 들어옵니다. 교환권은 새더라도 앱의 비밀값이 없으면 토큰으로 바뀌지 않습니다.

토큰을 믿기 전에 맞춰 보는 것

받은 ID 토큰은 문자열일 뿐입니다. 누구든 비슷한 것을 만들어 보낼 수 있습니다. 그래서 앱은 받자마자 다섯 가지를 맞춰 봅니다. 하나라도 어긋나면 로그인을 거절합니다.

항목 무엇을 맞춰 보나 안 보면
서명 인증 서버의 공개 키로 검증한다 누가 만든 토큰이든 통한다
iss 내가 아는 발급자가 발급했나 모르는 서버가 만든 토큰도 통한다
aud 받을 앱으로 내 앱이 적혔나 옆 앱에 발급된 토큰이 통한다
exp 만료 시각이 지나지 않았나 오래전 토큰이 계속 통한다
nonce 내가 보낸 값과 같은가 가로챈 토큰을 다시 밀어 넣을 수 있다

서명 검증에 쓰는 공개 키는 인증 서버가 주소 하나에 모아 공개합니다. 키는 이따금 바뀌므로 앱은 그 주소를 주기적으로 다시 읽어 받아 둔 키를 갱신합니다.

exp 를 볼 때는 두 서버의 시계가 조금 어긋난다는 것을 감안합니다. 대개 몇 분의 여유를 두고 판정합니다.

주소를 알려 주는 설정 문서

공개 키를 모아 둔 주소를 비롯해 앱이 불러야 할 주소는 인증 서버마다 다릅니다. 그래서 인증 서버는 자기 주소들과 지원 기능을 적은 설정 문서를 정해진 경로에 올려 둡니다. 앱은 그것을 읽어 필요한 주소를 얻습니다. 이 방식이 OpenID Connect 디스커버리입니다.

이름과 사진은 따로 가져온다

ID 토큰에는 로그인 사실만 담는 것이 보통입니다. 이름이나 사진 같은 프로필 값이 필요하면 앱이 사용자 정보 엔드포인트를 따로 부릅니다. 이때 내미는 것이 앞에서 함께 받은 액세스 토큰입니다.

무엇을 받을지는 scope 로 고릅니다. openid 는 로그인 자체를 뜻합니다. 여기에 profile 이나 email 을 더하면 그만큼의 값을 더 받습니다. 쓰지 않을 값을 안 받아 두면 사고가 났을 때 새는 양이 줄어듭니다.

언제 쓰고 언제 안 쓰나

쓸지 말지는 물음 둘로 갈립니다.

flowchart TD
    Q1["사람이 브라우저 앞에 있나"]
    Q1 -->|아니다| S["앱 자신의 자격으로 토큰을 받는 OAuth 방식"]
    Q1 -->|그렇다| Q2["그 계정을 내가 들고 있나"]
    Q2 -->|들고 있다| K["아이디 · 비밀번호 + 세션과 쿠키"]
    Q2 -->|남이 들고 있다| O["OpenID Connect"]

로그인시킬 사람이 브라우저 앞에 있고 그 계정을 내가 들고 있지 않을 때 씁니다. 회사 계정으로 사내 도구에 들어가는 경우, 그리고 한 번 로그인해 여러 서비스를 함께 쓰는 싱글 사인온이 여기 듭니다.

사람이 없는 통신에는 쓰지 않습니다. 서버끼리 부르는 일에는 로그인시킬 사용자가 없어서, 앱 자신의 자격으로 토큰을 받는 OAuth 의 다른 방식을 씁니다.

계정을 내가 직접 들고 있고 로그인할 곳도 하나뿐이라면, 아이디와 비밀번호를 받아 세션을 쿠키에 실어 주는 쪽이 부품이 적습니다. 다른 서버로 보냈다 받는 일도, 키를 갱신하는 일도 없습니다.

대가는 셋입니다. 첫째로 로그인 경로가 길어져 주고받는 단계와 검증할 값이 늘어납니다. 둘째로 인증 서버가 멈추면 앱의 로그인도 함께 멈춥니다.

셋째로 로그아웃이 둘로 나뉩니다. 앱의 세션을 지워도 인증 서버의 세션은 남아서, 사용자가 버튼을 다시 누르면 비밀번호를 묻지 않고 곧장 들어옵니다. 둘을 함께 끊으려면 인증 서버가 정한 로그아웃 절차를 따로 불러야 합니다.

flowchart TD
    L["로그아웃"] --> A
    subgraph 앱["앱의 세션"]
        A["지워짐"]
    end
    subgraph 인증서버["인증 서버의 세션"]
        B["그대로 남음"]
    end
    A --> B
    B --> R["버튼을 다시 누르면 비밀번호를 안 묻는다"]

관련 항목

OpenID Connect 가 얹히는 바탕 프로토콜

OAuth · HTTP · HTTPS · TLS · URI · REST

OpenID Connect 가 주고받는 토큰

ID 토큰 · 액세스 토큰 · 리프레시 토큰 · 인가 코드 · JWT · 토큰 · Bearer 토큰

OpenID Connect 에 참여하는 역할

인증 서버 · 신뢰 당사자 · 자원 서버 · 사용자 에이전트 · 브라우저

OpenID Connect 가 다루는 인증·인가 개념

인증 · 인가 · 인증과 인가 · 클레임 · 스코프 · 권한 · 싱글 사인온 · 페더레이션

ID 토큰을 검증할 때 쓰는 수단

전자 서명 · 공개 키 암호 · JWS · JWK · 해시 함수 · Base64url · 인증서

로그인 한 번이 거치는 단계

인가 코드 흐름 · PKCE · 리다이렉트 · nonce · state 파라미터 · 토큰 엔드포인트 · OpenID Connect 디스커버리

OpenID Connect 를 대신할 수 있는 로그인 방식

SAML · LDAP · Kerberos · 세션 · 쿠키 · 비밀번호 인증 · 다요소 인증

로그인 흐름을 노리는 공격

재전송 공격 · 크로스 사이트 요청 위조 · 오픈 리다이렉트 · 세션 고정 · 피싱 · 토큰 탈취

OpenID Connect 를 구현한 제품

Keycloak · Auth0 · Okta · Spring Security · Passport.js · Microsoft

다른 이름: OIDC