예전에 스프링 시큐리티 없이 OAuth 2.0을 구현할 당시에는 구현 자체가 목적이었기 때문에 로그인을 개발자의 서비스 대신 제3자 서비스에서 수행하는 방식이라고 간단하게 소개했었다. 하지만 OAuth 2.0은 이런 말로 단정짓기에는 부족하다.

따라서 오늘은 OAuth 2.0과 이를 개선한 OpenId Connect, 그리고 웹 프로그래밍에서 OAuth 2.0을 구현하는 패턴까지 알아보도록 하겠다.

OAuth 2.0

OAuth 2.0은 개방형 인가(Open Authorization)의 약자로, 어떤 서비스가 사용자를 대신하여 서드 파티 서비스에 존재하는 사용자 리소스에 접근할 수 있게 해주도록 고안된 사실상의 업계 표준 프로토콜이다.

image.png

주요 역할 및 구성요소

리소스 오너 (Resource Owner)

리소스에 대한 소유권과 접근 권한을 부여해줄 수 있는 사용자 또는 시스템

클라이언트 (Client)

리소스에 접근하고자 하는 주체로, 액세스 토큰을 가지고 있어야 한다.

인가 서버 (Authorization Server)

인가 서버는 주로 두 개의 엔드포인트를 제공하며, 방식에 따라 사용 여부가 다르다. 하나는 인가 엔드포인트로, 리소스 오너에게 클라이언트의 요청 권한을 안내하고 리소스 접근을 동의받는 것이다. 동의가 완료되면 클라이언트의 리다이렉트 URI로 리다이렉트하며 인가 코드를 발급한다.

이후 리다이렉트 받은 클라이언트가 인가 코드를 가지고 토큰 엔드포인트에 요청하면, 인가 서버는 리소스 접근 권한이 있는 액세스 토큰을 발급한다.

리소스 서버 (Resource Server)

클라이언트로부터 액세스 토큰과 함께 리소스 접근 요청을 받으면 이에 대한 리소스를 반환한다.

범위 (Scope)

클라이언트가 리소스 중 어디까지 접근해도 되는지 명시하는 권한 단위다.

앞으로의 설명은 모두 위 용어를 쓰도록 하겠다.

등장 배경

해당 프로토콜이 나오기 전에는 리소스에 접근하기 위해서 리소스 오너가 서드 파티 서비스의 비밀번호 같은 민감한 정보를 클라이언트에서 알아야 했다. 이 말을 좀 더 이해하기 쉽도록 클라이언트에서 구글의 어떤 리소스에 접근하는 기능을 구현하는 것을 생각해보도록 하자.

OAuth 2.0이 없다면 클라이언트에서 리소스 오너로부터 구글 계정 인증 정보를 받아서 직접 구글 리소스 접근을 구현하던가, 구글 계정에 존재하는 어떤 정보를 직접 달라고 해야 한다.

첫 번째 방법은 클라이언트와는 관계가 없는 리소스 오너의 서드 파티 서비스에 관한 민감 정보를 알아야 하기 때문에 클라이언트를 신뢰할 수 없으며, 두 번째 방법은 번거로운 과정이기도 하고 전달받은 정보를 클라이언트에서 신뢰할 수 없다.

반면에 OAuth 2.0을 사용해서 구현한다면 다음과 같은 과정을 거치게 된다.

  1. 클라이언트가 리소스 오너를 구글(인가 서버)의 인가 엔드포인트로 리다이렉트하여 동의를 요청
  2. 동의가 이루어지면 요청된 리다이렉트 URI로 리다이렉트하며 인가 코드를 발급
  3. 클라이언트는 발급받은 인가 코드를 토큰 엔드포인트에 보내 액세스 토큰을 발급
  4. 액세스 토큰을 가지고 리소스 서버에 리소스 요청

이 과정을 통해서 클라이언트는 서드 파티 서비스의 사용자 자격 증명에 대한 민감 정보를 알지 못한다. 단지 리소스 오너의 동의 절차가 성공적으로 이루어지면 리소스에 접근할 토큰을 얻게 되고, 이를 통해서 클라이언트가 원하는 리소스에 접근할 수 있게 된다.

인증이 아닌 인가 프로토콜이다

OAuth 2.0은 개방형 인가 프로토콜이라는 이름 그대로 엄연히 인가 프로토콜이다. 물론 대부분의 소셜 로그인에서 로그인 절차를 거치기 때문에 인증처럼 보일 수 있지만, 엄밀히 말하면 OAuth 2.0은 서드 파티 서비스의 리소스 접근을 어떻게 처리할 것인지를 정의한 것이다.

단지 대부분의 OAuth 2.0 제공자가 클라이언트에 리소스 접근 권한(scope 등)을 위임하는 것에 대한 동의를 받기 위해 인증 절차를 거칠 뿐이다. 즉, 서드 파티 서비스에서 수행되는 인증은 OAuth 2.0 스펙이 직접 정의하는 범위 밖의 일이라고 볼 수 있다.

이를 이해하기 위해 인증의 개념을 생각해보도록 하자. 인증은 주어진 식별 정보를 바탕으로 유일한 사용자를 식별해야 하며, 같은 식별 정보는 항상 같은 사용자로 취급되어야 한다. 이때 클라이언트에서 OAuth 2.0을 로그인 용도로 사용할 경우 다음 조건에서 문제가 발생할 수 있다.

  • 서비스에서 이메일을 식별 정보로 활용
  • OAuth 2.0 제공자는 이메일이 식별 정보가 아니며, 사용자 정보를 통해 언제든지 이메일을 변경할 수 있음

예를 들어, 클라이언트가 OAuth 2.0 제공자에서 조회한 sehako@example.com을 사용자 식별자로 데이터베이스에 저장했다고 가정하자. 이후 사용자가 서드 파티 서비스의 이메일을 hako@example.com으로 변경한 상태로 다시 OAuth 로그인 흐름을 수행하면, 클라이언트는 이를 새로운 사용자로 간주할 수 있다. 즉, OAuth 2.0을 로그인 용도로 이용한다면 클라이언트가 어떤 값을 사용자 식별자로 삼는지에 따라 이러한 문제가 발생할 수 있다.

권한 부여 시나리오

Authorization Code Grant

인가 서버가 리다이렉트 URI로 발급한 인가 코드를 받고, 클라이언트는 인가 서버의 토큰 엔드포인트에 요청하여 액세스 토큰을 발급받는 방식이다.

image.png

안전하게 액세스 토큰 교환이 가능한 전통적인 웹 앱에 적합한 방식이다. 이 방식을 SPA/모바일에서도 쓸 수 있지만, client secret을 안전하게 저장하기 어려워 교환 단계 인증이 제한되기에 이 경우에는 PKCE가 권장된다.

Authorization Code Grant + PKCE

Authorization Code 흐름을 보완한 방식으로, 추가 검증 단계(PKCE) 를 넣어, 인가 코드 탈취 위험을 줄인 방식이다. 앞서 언급한 것처럼 SPA/모바일처럼 client secret을 안전하게 보관하기 어려운 환경에서 권장된다.

Implicit Grant

인가 서버가 액세스 토큰을 클라이언트에 직접 반환하는 방식이다.

image.png

이 방식은 리다이렉트 경로에서 토큰 유출 위험이 커서 현재는 권장되지 않는다.

Resource Owner Password Credentials Grant (ROPC)

클라이언트가 사용자의 인증 정보를 직접 받아서 인가 서버에 전달하는 방식이다.

image.png

리다이렉트가 필요 없어서 리다이렉트가 불가능한 환경에서 쓸 수 있다. 다만 클라이언트가 사용자 비밀번호를 다루므로 완전히 신뢰되는 클라이언트에만 제한되고, 사실상 대부분의 상황에서 지양된다.

Client Credentials Grant

클라이언트 자체가 인증되어 토큰을 발급하는 머신-투-머신 방식이다.

image.png

배치/자동화 프로세스, 백엔드 서비스/마이크로서비스 간 호출 등에 사용된다.

Device Authorization Flow

브라우저 환경이 아니거나 키보드 입력이 불편한 입력 제약 장치에서 사용되는 방식이다. 일반적으로 다른 기기를 통해 동의를 얻는 방식으로 진행된다.

Refresh Token Grant

리프레시 토큰을 새로운 액세스 토큰으로 교환해 세션을 연장한다. 이 방식을 사용하면 액세스 토큰 만료 후 재로그인 없이 재발급이 가능하다. 즉, 기존 권한을 유지하기 위한 토큰 갱신 흐름이다.

OpenID Connect (OIDC)

OIDC는 OAuth 2.0을 기반으로 인증 단계를 추가한 프로토콜로, 리소스 오너가 인가 서버에서 수행한 인증 결과를 기반으로 클라이언트가 사용자의 신원을 검증하고 표준화된 방식으로 사용자 식별자와 표준 클레임을 획득 및 검증하는 절차를 정의한다.

image.png

이를 통해 OAuth 2.0을 인증 용도로 사용할 때 발생할 수 있는 잘못된 식별자 선택 문제를 줄이고, 클라이언트에서 여러 OAuth 2.0 제공자를 연동해도 일관된 방식으로 사용자 식별 정보를 처리할 수 있게 한다.

또한 OIDC는 ID 토큰에 기본 신원 정보를 포함하므로, 단순 로그인/식별을 위해 매번 UserInfo 같은 리소스 서버 API를 호출하는 오버헤드를 줄일 수 있다. 필요한 경우에만 액세스 토큰으로 사용자 정보를 조회하면 된다.

주요 역할 및 구성요소

OIDC는 기본적으로 OAuth 2.0의 주요 역할과 구성요소를 공유하고, 추가로 다음 역할들을 정의한다.

End-User

리소스 오너와 같은 개념으로 봐도 무방하다. 단지 인증의 대상을 강조하기 위해서 OIDC에서는 해당 용어를 주로 사용한다.

Relying Party (RP)

OIDC에서 클라이언트를 지칭할 때 사용하는 용어로, OIDC를 이용해 사용자 신원을 확인하고 서비스를 제공하는 주체라고 볼 수 있다. 추가적으로 ID 토큰의 서명(JWKS)과 iss, aud, exp 등의 클레임을 검증해 토큰의 신뢰성을 확인한다.

OpenID Provider (OP)

OAuth 2.0으로 치면 인가 서버라고 볼 수 있다. OIDC에서는 인증을 수행하고 ID Token을 발급하며, 필요에 따라 Access Token도 함께 발급한다. 또한 OP는 UserInfo 엔드포인트를 제공할 수 있는데, 이는 Access Token으로 보호되는 표준 API로서 토큰의 주체(End-User)에 대한 프로필 등 클레임을 조회하는 엔드포인트이다.

ID Token

OIDC 인증 요청에서 OP는 ID Token을 반환하며, 필요에 따라 액세스 토큰도 함께 발급한다. 이 ID 토큰은 사용자의 신원 정보를 포함한다. JWT에는 대표적으로 다음 클레임이 포함된다.

  • iss (발급자): ID 토큰을 발급한 OpenID 공급자의 고유 식별자
  • sub (주체): 사용자의 고유 식별자
  • aud (청중): ID 토큰을 수신하는 클라이언트 식별자
  • exp (만료시간): ID 토큰이 만료되는 시간
  • iat (발급 시각): ID 토큰이 발급된 시각

OIDC 스코프

클라이언트가 접근할 수 있는 OIDC의 스코프는 다음과 같다.

  • openid: ID 토큰 발급을 위한 OIDC 인증 요청을 나타낸다.
  • profile: 기본 사용자 정보
  • email: 사용자의 이메일 정보
  • phone: 사용자의 전화번호
  • address: 사용자의 주소 정보

OAuth 2.0 패턴

프론트엔드 주도

프론트엔드와 OAuth 2.0 제공자 간에서만 통신을 하는 구조다.

image.png

구조가 단순하여 네이티브 앱이나 SPA 초기에는 많이 사용되었으나, 액세스 토큰이 브라우저에 그대로 노출되는 치명적인 단점 때문에 보안이 중요한 서비스에서는 권장되지 않는다. 따라서 일반적으로 PKCE를 통한 보안 처리가 권장된다.

백엔드 일부 위임

프론트에서는 OAuth 2.0 제공자로부터 인가 코드를 발급받아 백엔드에 전달하면, 백엔드에서 인가 서버에 엑세스 토큰을 요청하고 해당 토큰으로 리소스 서버에 요청해 사용자 정보를 발급받는 방식이다.

image.png

토큰을 백엔드에서 관리하고 프론트에서는 백엔드로부터 발급받은 자체 서비스의 JWT만 관리하면 되기 때문에 비교적 안전하지만, 인가 코드가 탈취당할 위험이 존재하고 PKCE 보안 처리가 되어 있지 않으면 취약하다고 볼 수 있다.

백엔드 주도 (서버 사이드 랜더링)

백엔드에서 인가 코드 발급부터 사용자 정보를 발급받는 모든 과정을 처리한다.

image.png

프론트엔드에서 아무런 관여를 하지 않으므로 가장 안전한 방식이지만 모바일이나 SPA 구조와는 맞지 않아서 이 부분에 대한 처리를 고려해야 한다.

BFF(Backend For Frontend) 방식

SPA 구조에서는 BFF 패턴을 사용한다. BFF란 프론트엔드를 위한 백엔드 서버를 말한다. 이를 추가하면 다음과 같은 구조가 된다.

image.png

이를 통해 다음과 같은 문제를 해결할 수 있다고 한다.

  • 브라우저 to 서버 다중 API 호출 해결
  • CORS 관리 포인트 감소
  • 불필요한 데이터 제거 및 클라이언트 별 맞춤 데이터 응답 처리
  • API 백엔드 서버에서 클라이언트의 스펙을 알 필요가 없어짐
  • 민감한 보안 정보를 브라우저로부터 숨길 수 있음
  • 복잡한 비지니스 로직이나 작업을 BFF에서 대신 처리

그렇다면 SPA 애플리케이션이라고 가정한다면, 어떻게 할 수 있을까? 사실 전체적인 과정은 앞서 백엔드 주도 방식과 별 다를 게 없다. 단지 마지막에 백엔드에서 OAuth 2.0 제공자로부터 리소스를 얻은 다음에 SPA로 리다이렉트할 때 엑세스 토큰 대신 자체 발급한 쿠키를 전달하기만 하면 BFF 패턴을 이용한 SPA에서의 백엔드 주도 OAuth 2.0 구현이 되는 것이다.

추가적으로 모바일 앱 환경에서는 어떻게 리다이렉트가 가능한지 궁금해서 찾아보니 앱에서도 자체적인 주소를 가지는 딥 링크 방식이 있다고 한다. 이 부분은 나중에 안드로이드 개발을 할 기회가 있으면 한 번 공부해봐도 좋을 것 같다는 생각이 들었다.


스프링 시큐리티를 이용해서 OAuth 2.0을 바로 구현하고 싶었지만, 이에 대해서 자세히 알아보고자 찾다보니 생각보다 정리할 것이 많았다. 마지막으로 웹이나 앱 환경에서 권장되는 권한 획득 시나리오는 대부분 Authorization Code Grant + PKCE 방식인 것 같다.

다음 포스팅은 스프링 시큐리티를 활용하여 SPA 환경에서 BFF 패턴을 이용해 OAuth 2.0을 구현하는 방법을 알아보도록 하자.

참고 자료

What is OAuth 2.0?

2. OAuth 2.0

OIDC란 무엇인가: 왜 필요한지 그리고 어떻게 작동하는지

OIDC란?

Spring Boot OAuth2 구현 방법

OAuth2 인증 서버 BFF 적용기

댓글남기기