회사에서 신규 플랫폼 런칭과 함께 사용자 경험의 증대, 계정의 통합 관리, 브랜드 이미지 구축 등을 위해 자사 서비스들에 SSO(Signle Sign-On)를 적용하기로 했다.
나는 인증 서버를 구축하고 SSO 기획자, 서비스 개발자들과 함께 소통하며 전반적인 SSO 구축을 리딩하는 역할을 맡았다.
SSO 구축 과정을 기록하고 회고하기 위해 이 시리즈를 작성한다.
SSO를 구축하기 위해서는, 우선 SSO가 어떻게 동작하는지 알아야 한다.
SSO Wiki 에 따르면, SSO는 관련된 독립적인 여러 소프트웨어 시스템에 사용자가 단일 ID로 로그인 할 수 있도록 하는 인증 체계이다.
SSO를 구현하는 방식은 여러 가지가 있는데, SSO 구현 모델 Wiki 에 따르면 Delegation Model, Propagation Model, Browser 기반 Cookie Domain 등이 존재한다.
SSO 구현 모델을 결정하기 전에, 자사 서비스들의 기존 인증 방식과 사내 인프라를 고려하여야 한다.
위와 같은 사항들을 고려하여, IdP에 인증을 위임하고 서비스 에이전트의 Root Domain에 SSO Token Cookie를 생성하여 해당 Root Domain을 사용하는 모든 서브 도메인에서 Token을 공유하여 사용할 수 있도록 설계하였다.(SSO 구현 모델 Wiki 에서는 Web 기반 Multi Cookie Domain SSO에 해당)
설계된 SSO Model의 로그인 Flow는 다음과 같다.
1. 사용자가 a.com에서 로그인을 요청한다.
2. Agent는 로그인 요청을 Id Provider에 위임하기 위해 별도의 IdP 로그인 페이지로 Redirect 시킨다.
3. 사용자가 제공된 로그인 페이지에서 로그인한다.
4. IdP가 User Credential을 검증하고 로그인이 성공하는 경우, IdP에서 SSO Session이 생성된다.
5. IdP가 Agent로 SSO Token과 함께 Callback 시킨다.
6. Agent가 a.com Domain의 SSO Token Cookie를 굽는다.
Root Domain으로 Cookie를 굽는 이유는, a.com과 같은 Root Domain으로 Cookie를 구워야만 '1.a.com', '2.a.com'과 같은 a.com을 Root Domain으로 사용하는 여러 Sub Domain 들에서 Cookie를 공유할 수 있기 때문이다.
a.com에서 로그인에 성공했으니, 이제 b.com에서 어떻게 Single-Sign-On이 이루어지는지 알아보자.
1. 사용자가 b.com에서 로그인을 요청한다.
2. Agent는 로그인 요청을 Id Provider에 위임하기 위해 별도의 IdP 로그인 페이지로 Redirect 시킨다.
3. IdP에서 해당 사용자의 SSO Session이 존재함을 확인하고, 로그인 프로세스를 생략한다.
4. IdP가 Agent로 SSO Token과 함께 Callback시킨다.
5. Agent가 b.com Domain의 SSO Token Cookie를 굽는다.
이 시점에서 a.com Agent, b.com Agent와 같이 Root Domain 별로 왜 Agent가 별도 구성되어 있는지 의문이 생길 수 있는데, RFC6265 사양에 따르면 'Domain 속성이 원본 서버를 포함하는 쿠키의 범위를 지정하지 않는 한 사용자 에이전트는 쿠키를 거부한다' 라고 명시되어 있다.
즉 Origin Server(a.com)가 b.com을 포함하지 않으므로 Cross Root Domain 간에 쿠키를 조작할 수 없기 때문에, Root Domain 별로 Cookie를 생성하기 위해 각기 다른 Domain을 가진 Agent를 구성한 것이다.
SSO 모델을 설계하였으니, 이제 어떤 Protocol을 사용하여 SSO를 구현할 것인지 고민해야 한다.
SSO에 사용되는 인증 프로토콜은 일반적으로 SAML, OpenID Connect 두 가지가 존재한다.
SAML은 IdP와 SP를 연결하는 XML 기반 표준이고, SAML Token을 통해 IdP와 SP 간에 Resource User(최종 사용자)에 대한 정보를 전달하는 XML 기반 Assertion이다.
SAML은 verbose XML을 사용하여 ID 데이터를 교환하는데, 이 과정에서 많은 데이터 처리 부하를 생성하고 OpenID Connect보다 복잡하여 구현이 어렵고, 자사 서비스에 주로 사용되는 모바일 앱과 SPA에 적합한 OpenID Connect를 선택하였다.
SSO에 SAML, OIDC(OpenID Connect)가 일반적으로 사용된다면서 소제목에는 왜 OAuth 2.0이 들어가 있는지 궁금하신 분들이 있을 것이다.
OIDC는 그 자체로 존재하는 프로토콜이 아닌, OAuth 2.0이라는 인가를 위해 만들어진 프로토콜 위에 인증을 수행하기 위해 올라간 프로토콜이기 때문이다.
즉 OIDC를 사용한다는 것은, OAuth 2.0 + OIDC를 사용한다는 것과 동일하기에, OAuth 2.0에 대한 이해가 선행되어야 한다.
OAuth 2.0을 설명하기에 앞서 OAuth 2.0에서 사용되는 용어에 대해 알아두어야 한다.
그래서 OAuth 2.0이 무엇이냐 하면, Client에 특정 권한만을 위임하기 위해 사용되는 프로토콜 이다.
OAuth Protocol이 존재하지 않던 과거에는, 특정 권한만을 위임하는 방식이 존재하지 않아 아래 사진과 같이 권한이 필요한 서비스의 계정 정보를 요구했다.
'우리는 당신의 이메일과 비밀번호, 친구들의 주소를 가지고 있지 않겠다' 라는 멘트와 함께..
해당 방법은 계정 정보를 통째로 넘기기 때문에, '주소록 조회' 등 특정 권한만을 위임할 수 없다.
이러한 문제를 해결하기 위해 Authorization(인가)에 초점을 맞춘 OAuth 라는 프로토콜이 만들어지게 된 것이다.
OAuth 2.0 Protocol의 인가 Flow는 다음과 같다.(OAuth 2.0의 Grant Type 중 보안이 가장 뛰어나 권장되고 일반적으로 사용되는 방식인 Authorization Code Flow를 설명하는 그림이다. 그 외 Grant Type의 설명은 oauth.net의 링크로 대체하겠다. 출처: Okta의 OAuth 2.0, OpenID Connect 세미나 영상)
1. Resource Owner(최종 사용자)는 Client(yelp.com)에서 Google Login을 요청한다.
2. Client가 Authorization Server(accounts.google.com)에게 profile, contacts scope(얻고자 하는 권한)를 포함하여 요청하고, Authorization Server는 로그인 페이지로 Redirect 시킨다.
3. Resource Owner가 로그인에 필요한 정보(Credential)를 입력하여 로그인한다.
4. Authorization Server는 Credential을 검증하고, 로그인에 성공하면 Resource Owner에게 profile, contacts 권한을 Client에게 위임할 것인지 동의를 구한다.
5. Resource Owner가 동의하는 경우, Authorization Code와 함께 Client로 Callback 시킨다.
6. Client는 Authorization Code를 가지고 Authorization Server에 Access Token 발급을 요청한다.
7. Client는 Token을 가지고 Resource Server(contacts.google.com)에게 Resource를 요청한다.
이로써 Resource Owner는 Client에게 계정 정보를 넘겨주지 않고도 profile, contacts라는 특정 권한만을 위임할 수 있게 된다.
이 과정에서 왜 Client에게 곧바로 Access Token을 발급해주지 않고, Authorization Code라는 것을 통해 이중 교환을 하게 하는지 의문이 생길 수 있다.
그 이유를 알기 전에, OAuth 2.0에서 말하는 Front Channel과 Back Channel에 대해 알아야 한다.
Back Channel은 Authorization Server와 Client가 서로 통신하는 구간으로, 사용자 혹은 제 3자의 개입이 있을 가능성이 매우 적어 위변조의 가능성이 낮다.
Front Channel은 사용자의 브라우저를 거치는 통신 구간을 의미하고, Back Channel에 비해 상대적으로 위변조의 가능성이 존재하여 덜 안전하다.
Authorization Code은 Front Channel을 통해 전달되기 때문에, Code가 Leak 될 가능성이 있어 Back Channel을 통해 Token Exchange 과정을 거치는 것이다.
Access Token을 Front Channel로 곧바로 발급받는 Implicit Grant Type도 있는데, 사내 SSO 구현에는 Authorization Code만을 사용할 것이기 때문에 자세한 설명은 oauth.net으로 대체하겠다.
이제 OAuth 2.0을 알게 되었으니, OpenID Connect에 대해 설명하겠다.
OAuth 2.0은 인가를 위해 만들어졌지만, 다양한 서비스들이 인가 외의 아래와 같은 목적을 위해 사용하는 상황이 생겼다.
OAuth 2.0은 사용자가 누군지에는 관심이 없고, Scope를 통해 제한된 정보에 접근할 수 있도록 지원한다.
하지만 그 과정에서 인증을 수행하고 있다는 것이 OAuth 2.0을 오용되게 한다.
얼핏 생각하면 OAuth 2.0 Flow에서 로그인을 하기 때문에 인증을 포함하는 것이 아닌가 생각할 수 있지만, Access Token은 로그인한 사용자가 아닌 누구나 사용할 수 있고, Refresh Token을 통해 재발급 할 수 있다는 것이 인증에 사용할 수 없는 이유이다.
OAuth 2.0이 인증을 위해 사용되면 발생하는 문제들은 다음과 같다.
위 문제들을 해결하기 위해 OAuth 2.0 스펙을 유지하는 선에서 인증을 허용하는 방법을 구상한 결과, OAuth 2.0 위에 OpenID Connect라는 인증 계층을 만들어 인증을 수행할 수 있게 하였다.
OpenID Connect Protocol의 인증 Flow는 다음과 같다.(출처: Okta의 OAuth 2.0, OpenID Connect 세미나 영상)
1. Resource Owner(최종 사용자)는 Client(yelp.com)에서 Google Login을 요청한다.
2. Client가 Authorization Server(accounts.google.com)에게 profile, openid scope(OIDC를 사용한다는 의미)를 포함하여 요청하고, Authorization Server는 로그인 페이지로 Redirect 시킨다.
3. Resource Owner가 로그인에 필요한 정보(Credential)를 입력하여 로그인한다.
4. Authorization Server는 Credential을 검증하고, 로그인에 성공하면 Resource Owner에게 profile 정보를 Client에게 제공할 것인지 동의를 구한다.
5. Resource Owner가 동의하는 경우, Authorization Code와 함께 Client로 Callback 시킨다.
6. Client는 Authorization Code를 가지고 Authorization Server에 Access Token, ID Token 발급을 요청한다. 발급받은 ID Token로 사용자가 증명되었음을 인증한다.
아직 OAuth 2.0, OpenID Connect Protocol이 무엇인지 이해가 잘 되지 않는 분들은 IAM 솔루션 회사인 Okta에서 진행한 세미나 영상을 시청하면 도움이 될 것이다. 인가 프로토콜의 History부터 시작해서 OAuth 2.0, OIDC를 쉽고 명확하게 설명해준다.
다음 포스팅에서는 Authorization Server 구축에 대해 이야기 해보겠다.
Authorization Code를 발급하는 이유가 탈취 가능성때문에 그렇다고 하셨는데요,
결국 Authorization Code 또한 프론트 채널로 전달되므로 탈취되면 Access Token을 발급할 수 있는건 똑같지 않나요?
제가 잘 몰라서 질문드립니다