필요한 API 서비스를 나누고 서비스의 규모가 커지면, 인증과 인가에 대해 구조적으로 돌아보게 됩니다. 그러면서 자연스럽게 SSO(IdP, SP)나 OAuth 2.0, OpenID Connect 같은 큰 개념을 돌아보게 되고, 어떻게 이를 위임하거나 전파할지를 구상하게 됩니다.
단일 로그인입니다. 자기 서비스에 한번만 로그인 하면, 연관있는 다른 “서비스”를 로그인 없이 이용할 수 있게 하는 것이지요. 사용자에게는 여러 서비스를 한번에 쓸 수 있게끔 세팅해주는 것이고, 뒷단에서는 각 서비스에 대해 별도 인증시스템을 따로 꾸리지 않으며 사용자 접근관리를 하도록 합니다.
SSO를 구현하는 구현모델 및 기법을 살펴봅시다. 여기서는 제가 이해한 모델에 대해서 기술하겠습니다.
SSO 에이전트가 중앙에서 인증을 대행합니다. 대상 애플리케이션의 인증방식을 변경하기 어려울 때 서비스에 일일이 붙여주는 방식으로 사용합니다. 사용자의 인증 정보를 SSO 에이전트가 관리합니다(SPOF가 될 수 있겠지요).
통합인증 서비스에서 인증을 수행하고 인증 토큰을 받아옵니다. 서비스 접근 시, 인증토큰을 서비스에 전달하고, 이를 가지고 사용자를 확인할 수 있는 구조입니다.
유저에게 자신, 타인의 리소스에 접근할 수 있는 권한을 토큰으로 부여 하는 프로토콜(RFC 6749)입니다.
OAuth 2.0은 인증 성공시 제한된 권한을 가진 Access Token과, 이를 갱신하는 Refresh Token을 동시에 발급받습니다. 이를 통해 유저별로 다양한 서비스에 대해 필요한 권한을 부여할 수 있습니다.
이처럼 권한을 부여하고 토큰을 관리하는 구조를 이해하는 것이 OAuth 2.0의 핵심 입니다.
OAuth 2.0을 이해하기에 앞서, 용어를 정리해봅시다.
OAuth 2.0을 구성하고 있는 주요 4가지 객체(Roles)는 다음과 같습니다. 이 글을 기술하며, 아래의 상황을 가정합시다.
“나”는, “파이어폭스”로 “가나다” 서비스에서 “곰곰”이라는 서비스에 접근하는 권한을 주려합니다.
Resource owner
Resource server
Client
Resource owner
가 리소스에 접근하는 애플리케이션을 의미합니다. “가나다” 서비스에 접근하는 “파이어폭스” 이자, “가나다” 서비스 내부의 비밀정보를 저장하는 프로그램을 의미합니다.Authorization server
Resource owner
를 인증한 후, Client에 대해 적절한 권한을 부여해줍니다.Access token
, Refresh token
를 발급합니다.
Access token
에는 각종 권한과 유효기간 등 주요정보가 들어가있습니다.OAuth 2.0에서 “토큰”이란 아래와 같은 두 가지가 있습니다. 토큰을 어떤 스펙으로 정해야 하는지는 정해져있진 않습니다만, 주로 JWT
를 사용합니다.
Access token
Access token의 주요 특징은 아래와 같습니다:
Access Token
만료시 Refresh Token
으로 갱신합니다.Refresh token
Refresh token의 주요 특징은 아래와 같습니다:
Access token
을 갱신하기 위한 토큰입니다.Refresh token에 담긴 주요 리소스들을 살펴보도록 합시다.
이 토큰은 인증 서버로부터 받아옵니다. 관련 엔드포인트는 authorize
, token
이라는 두 개의 엔드포인트를 일컫습니다.
token
엔드포인트
authorize
엔드포인트에 성공적으로 응답을 받고, Access token
과 Refresh token
을 받습니다.authorize
엔드포인트
introspect
엔드포인트
token
엔드포인트로부터 받은 토큰을 물어보는 과정입니다.revoke
엔드포인트
Refresh token
을 “만료시키는” 엔드포인트 입니다. 유저가 특정 앱에 대해 권한을 주지 않음을 명시할 때 호출합니다.획득한 Access token으로 API에 접근할 때 사용하며, 만료되면 내부 클라이언트가 Refresh token을 가지고 Access token을 갱신합니다.
아래 내용을 시행하기에 앞서, 클라이언트는 자신의 서비스에 먼저 “로그인” 한 것을 가정한 것으로 이해하시면 됩니다. 저는 RFC 문서 상의 2.3. Client Authentication 을 보고 이를 판단했습니다:
If the client type is confidential, the client and authorization server establish a client authentication method suitable for the security requirements of the authorization server. The authorization server MAY accept any form of client authentication meeting its security requirements. (후략)
그 후에 추가적인 “인가” 를 받고(OAuth 2.0) Resource Server에 통신하거나, 추가적인 “인증”, “인가” 를 모두 받고(OpenID Connect) Resource Server와 통신하는 것으로 이해하시면 됩니다.
상세한 내용은 후술하겠습니다.
OAuth 2.0의 전체 프레임워크의 가장 기본적인 Flow는 다음과 같습니다.
+--------+ +---------------+
| |--(A)- Authorization Request ->| Resource |
| | | Owner |
| |<-(B)-- Authorization Grant ---| |
| | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(D)----- Access Token -------| |
| | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource |
| | | Server |
| |<-(F)--- Protected Resource ---| |
+--------+ +---------------+
Figure 1: Abstract Protocol Flow
또한, 이 문서에서는 Front channel이란 용어와, Back channel이란 용어를 각각 사용하겠습니다. Front channel은 고객(클라이언트 사이드)과 연관있는 파트를, Back channel은 Authorization Server, Resource server 와 같이 프로그래머 및 서비스와 밀접한 관계가 있는 파트를 의미합니다.
“나”가 “파이어폭스”를 통해, “가나다” 서비스에서 “곰곰” 서비스로 요청하는 과정을 도식화한 것입니다. 여기서는 간략히 설명하겠습니다.
(A): 클라이언트는 인가를 resource owner에 요청합니다. 인증요청은 resource owner에 직접적으로 요청되거나, authorization server를 중간에 거쳐서 수행합니다.
(B): Resource owner는 Client에게 인증허가를 수행합니다. 인증허가 방법은 총 4가지가 있습니다. 인증허가 방식은 하단에서 자세히 후술하겠습니다. 여기서는 특징만을 기술합니다.
Authorization Code Grant
Implicit
Resource Owner Password Credentials
Client Credentials
참고) 인증 허가 후의 과정은 아래의 Back channel과도 밀접한 연관을 가집니다. Back channel에서의 플로우 및 상세 구현방법이 정해지기 때문입니다. 여기서는 개념만 짚고 넘어가도록 하겠습니다.
유저에게 동의(consent)를 구하고, 클라이언트는 토큰을 통해 작업하는 큰 흐름에 대해 설명합니다. 상세한 구현방법은 상기 (B) 과정에 설명한 내용입니다. 아래 내용은 OAuth 2.0의 대략적인 플로우를 의미합니다.
토큰 부여까지는 살펴보았고, 그렇다면 이제 그 토큰을 통해 어떤식으로 권한 처리 에 대한 플로우를 사용하는지 알아봅시다. 이 글에서는 Authorization Code Grant만을 살펴보도록 하겠습니다. 보다 더 상세한 스펙과 내용에 대해서는 RFC 문서를 참고해주시기 바랍니다.
인가 이 후 어떤 식으로 동작하는지에 대해 살펴봅시다. Back channel에서 일어나는 일에 대해 보다 소상히 살펴본다고 생각하시면 됩니다.
Authorization code grant type은 Access token과 Refresh token을 모두 얻는 데 사용되며 기밀 클라이언트에 최적화되어 있습니다. 이것은 리디렉션 기반 흐름이므로 클라이언트는 Resource Owner의 사용자 에이전트(일반적으로 웹 브라우저)와 상호 작용할 수 있어야 하며 Authorization Server에서 들어오는 요청(리디렉션을 통해)을 수신할 수 있어야 합니다.
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server |
| | | |
| -+----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)
Note: The lines illustrating steps (A), (B), and (C) are broken into
two parts as they pass through the user-agent.
Figure 3: Authorization Code Flow
Authorization Code Grant 플로우 중, 권한요청 시에는 아래 request 쿼리값이 들어갈 수 있습니다. application/x-www-form-urlencoded
포맷으로 전송합니다.
필드명 | 필수여부 | 상세설명 |
---|---|---|
response_type |
REQUIRED | 반드시 code 라는 문자열이야 합니다. |
client_id |
REQUIRED | Client identifier 값입니다. |
redirect_uri |
OPTIONAL | 토큰이 발급받을 위치를 지정합니다. 이 값을 엉뚱한 값으로 바꾸어 리소스에 접근할 수 없도록 하는 약속입니다. |
scope |
OPTIONAL | 클라이언트가 수행할 수 있는 “범위”를 지정해주는 값입니다. |
state |
RECOMMENDED | 요청-콜백 간 상태를 유지하기 위해 클라이언트 측에서 사용하는 opaque한 값입니다. CSRF를 방지하기 위해 사용됩니다. |
요청 예시는 아래와 같습니다:
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
필드명 | 필수여부 | 상세설명 |
---|---|---|
code |
REQUIRED | Authrization 서버로부터 생성된 권한 부여 코드값입니다. 이 값은 악의적인 사용(유출되거나 하는 등)을 방지하기 위해 만료시간이 짧아야 합니다(10분 이 권장됩니다). 해당 값은 반드시 한번 이상 사용할 수 없도록 해야합니다. 한번 이상 사용한다면 요청을 반드시 거부해야합니다. |
state |
REQUIRED | 앞선 요청에 state 필드값이 있었다면, 요청과 동일한 값을 응답해줍니다. |
응답 예시는 아래와 같습니다:
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
&state=xyz
클라이언트 측에서도 식별되지 않은 응답 파라미터는 반드시 무시해야합니다.
Authorization server는 아래 상황에 대해 에러를 리턴해야 하고, 잘못된 리디렉션 URI로 반드시 가게해선 안 됩니다.
필드명 | 필수여부 | 상세설명 |
---|---|---|
error |
REQUIRED | 에러를 표기하기 위한 ASCII 값입니다(알파벳 대소문자, 숫자 이외의 값은 포함하면 안됩니다). 값들은 아래와 같습니다: invalid_request : 필수 파라미터를 포함하지 않았거나 잘못 사용한 경우 unauthorized_client : 클라이언트가 해당 리소스를 사용하기에 적절한 권한이 없는 경우 access_denied : Resource owner 혹은 Authorization server가 요청을 거부한 경우 unsupported_response_type : Authorization server가 이 방법을 사용한 액세스 토큰 획득을 지원하지 않는 경우 invalid_scope : 요청의 scope 값이 유효하지 않거나, 알려지지 않은 값이거나, 손상된 경우 server_error : Authorization server 측의 예기치 않은 에러가 발생된 경우. 500 Internal Server Error Response를 줄 수 없는 경우 사용합니다. temporarily_unavailable : Authorization server 측의 과부화 혹은 점검의 이유로 인해 해당 요청을 처리할 수 없는 경우 사용합니다. 503 Service Unavailable Response를 줄 수 없는 경우 사용합니다. |
error_description |
OPTIONAL | human-readable ASCII 텍스트. 클라이언트 개발자에게 보다 유용한 에러 메시지를 전달하기 위한 필드입니다. 알파벳 대소문자, 숫자 이외의 값을 포함하면 안됩니다. |
error_uri |
OPTIONAL | 클라이언트 개발자에게 보다 유용한 에러 메시지를 전달하기 위한 human-readable 웹페이지 URI 값입니다. |
state |
REQUIRED | 앞선 요청에 state 필드값이 있었다면, 요청과 동일한 값을 응답해줍니다. |
Authorization server가 요청을 거부한 경우에 대한 응답 예시는 아래와 같습니다.
HTTP/1.1 302 Found
Location: https://client.example.com/cb#error=access_denied&state=xyz
HTTP request entity-body에 UTF-8
인코딩을 사용하고, application/x-www-form-urlencoded
포맷으로 보냅니다.
필드명 | 필수여부 | 상세설명 |
---|---|---|
grant_type |
REQUIRED | 이 값은 반드시 문자열 authorization_code 값이어야합니다. |
code |
REQUIRED | Authorization server로부터 받은 authorization code 값입니다. |
redirect_url |
REQUIRED | Authorization request 과정에서 해당 값이 포함되어 있었다면, 반드시 그 값과 동일해야합니다. |
client_id |
REQUIRED | 클라이언트 자격증명에 사용됩니다. 클라이언트가 권한 서버에 등록하면 발급받을 수 있으며 권한 서버 연동 시 클라이언트의 검증에 사용됩니다. |
요청 예시는 아래와 같습니다:
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
해당 값 리턴 시, 헤더에 Cache-Control: no-store
값과 Pragma: no-cache
값이 반드시 존재해야 합니다. Content-Type
은 application/json
입니다.
필드명 | 필수여부 | 상세설명 |
---|---|---|
access_token |
REQUIRED | Authorization server로부터 발급받은 Access token 값입니다. |
token_type |
REQUIRED | 발급한 토큰의 타입을 기재합니다. 대소문자를 구별합니다. |
expires_in |
RECOMMENDED | Access token의 유효기간입니다. 3600 이라면, 1시간 후 만료됩니다. 만약 이 값이 없다면 Authorization server에서 만료 시간을 기본값으로 정해주어야 합니다. |
refresh_token |
OPTIONAL | Refresh token값입니다. 이 값은 동일한 authorization grant 과정에 사용되는 새로운 Access token을 발급받는데 사용됩니다. |
scope |
OPTIONAL | 어떤 권한을 요청할 것인지에 대한 범위입니다. 유저별로 구별되어있다면 OPTIONAL이지만, 그렇지 않다면 REQUIRED 입니다. |
응답 예시는 아래와 같습니다:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
상단에서 살펴본 Access token과 Refresh token이 보입니다. 이를 통해 리소스 요청 및 Access token 갱신을 수행합니다. 또한 클라이언트 측에서도 식별되지 않은 응답 파라미터는 반드시 무시해야합니다.
필드명 | 필수여부 | 상세설명 |
---|---|---|
error |
REQUIRED | 에러를 표기하기 위한 ASCII 값입니다(알파벳 대소문자, 숫자 이외의 값은 포함하면 안됩니다). 값들은 아래와 같습니다: invalid_request : 파라미터가 잘못되거나, 클라이언트 인증에 문제가 있거나, 값이 변조된 경우 invalid_client : 클라이언트 인증에 실패하는 경우 invalid_grant : authorization grant 과정 혹은 refresh token이 잘못된 경우 unauthorized_client : 클라이언트가 적절한 권한을 가지지 않은 경우unsupported_grant_type : authorization server에서 지원하지 않는 authorization grant type인 경우 invalid_scope : resource owner가 제공하지 않은 권한 밖의 scope 를 요청한 경우 |
error_description |
OPTIONAL | human-readable ASCII 텍스트. 클라이언트 개발자에게 보다 유용한 에러 메시지를 전달하기 위한 필드입니다. 알파벳 대소문자, 숫자 이외의 값을 포함하면 안됩니다. |
error_uri |
OPTIONAL | 클라이언트 개발자에게 보다 유용한 에러 메시지를 전달하기 위한 human-readable 웹페이지 URI 값입니다. |
응답 예시는 아래와 같습니다:
HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"error":"invalid_request"
}
또한 클라이언트 측에서도 식별되지 않은 응답 파라미터는 반드시 무시해야합니다.
권한서버는
필드명 | 필수여부 | 상세설명 |
---|---|---|
grant_type |
REQUIRED | 이 값은 반드시 refresh_token 이라는 문자열이어야 합니다. |
refresh_token |
REQUIRED | 클라이언트에게 발급된 refresh token 값입니다. |
scope |
OPTIONAL | 어떤 권한을 요청할 것인지에 대한 범위입니다. 유저별로 구별되어있다면 OPTIONAL이지만, 그렇지 않다면 REQUIRED 입니다. |
요청 예시는 아래와 같습니다
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
이번 글을 통해, 아래 내용들을 살펴볼 수 있었습니다:
잘못된 사항은 언제든 댓글로 되짚어주십시오.
읽어주셔서 감사합니다.