Spring Security를 활용한 OIDC 소셜 로그인 (1) - 도입 전 고려사항

유우비트·2024년 1월 30일
4
post-thumbnail

극악무도한 소셜 로그인

보통은 소셜 로그인 이렇게 하죠?

IT연합동아리 디프만(depromeet)에서 '10분만(10MM)' 이라는 서비스를 개발하고 있습니다.
저희 팀에서는 소셜 로그인 기능 구현을 위해 Open ID Connect (OIDC) 프로토콜을 사용하고 있습니다.

국내에서는 소셜 로그인 하면 카카오, 애플, 구글을 뽑는 편이죠?
보통은 카카오 로그인을 기본적으로 넣고, iOS 런칭까지 노리는 경우 애플 로그인을 넣습니다.

스프링 시큐리티로 소셜 로그인을 구현해보신 분들이라면 알겠지만, 구글은 크게 문제가 되지 않습니다. 구글 로그인만 사용한다면 별도의 커스터마이징 없이 기본적인 시크릿 값만 넣어주고 시큐리티의 디폴트 설정을 사용하면 됩니다. 구글, 깃허브 같은 경우 프로바이더 정보나 응답 값에 대한 포맷이 스프링 시큐리티 내부에 들어있기 때문이죠.

서버 개발자들은 애플 로그인이 싫다

딸깍 몇 번으로 구현이 가능한 구글 로그인과 다르게, 카카오와 애플은 쉽지 않습니다.
먼저 카카오의 경우 userinfo 응답을 매핑해줄 객체를 커스터마이징 해줘야 합니다.

하지만 애플의 경우 카카오가 선녀같이 보일 정도로 아주 극악무도한 구현 난이도를 보여주는데, 바로 카카오에서는 그냥 내줬던 client_secret 을 직접 만들어야 한다는 것입니다. 스프링 시큐리티에서 애플 로그인 구현한 레퍼런스를 보면 대부분 "국내에 레퍼런스가 없어서..." 혹은 "구현이 굉장히 까다로워서..." 같은 코멘트를 남겨놓고 있죠.

좀 과장하긴 했는데 도저히 손도 못댈 정도로 어려운 것은 아닙니다. 특히 보안 관련 컨텍스트에 대한 지식이 있다면 PEMParser 니 서명이니 뭐니 하면서 시크릿 값을 만드는 플로우를 이해해볼 수는 있겠죠. 그렇지 않은 경우라면, 게다가 OAuth 2.0를 처음 접하는 분이라면 따라가기가 어려울 거라고 생각합니다.

OIDC의 특징

OAuth 2.0과 Open ID Connect에 대한 간단한 이해가 있다고 가정합니다.
만약 관련 지식이 없으시다면, 아래 링크를 참고해주세요.

(프론트가 희생하면) 인증 과정이 간단해진다

OIDC 프로토콜과 함께라면 앞에서 말했던 이 모든 것들을 걱정할 필요가 없습니다.

authorization_code 를 받고 토큰을 받아오고 userinfo 를 받아오고 매핑해주고... 이런 번잡하고 귀찮은 과정은 프론트에게 토스하고, 백엔드에서는 프론트가 넘겨준 ID Token만을 검증하고 유저 정보로 파싱하여 사용하면 끝이거든요!

뭐라고요? 프론트한테 책임을 떠넘기는 거 아니냐고요? 사실 맞아요 JDD를 충실하게 따르는 중

아닙니다. 프론트가 ID Token을 처리할 때 얻을 수 있는 장점도 있습니다.

  1. 프론트엔드는 SDK(웹)나 라이브러리(앱)를 사용하여 ID Token을 받아오는 부분을 백엔드에 비해 간단하게 처리할 수 있습니다.
    • Spring Security에서 .p8 파일로 애플 로그인에 사용할 client_secret 을 구현하는 공수를 없앨 수 있죠. 라이브러리가 이 부분을 해결해주니까요!
  2. 훨씬 더 네이티브한 로그인 작업이 가능합니다.
    • 애플 로그인의 경우 프론트에서 처리하면 Face ID, Touch ID 등의 기능을 활용할 수 있습니다. 카카오 로그인의 경우 카카오 앱을 띄워서 로그인 처리하는 것을 예시로 들 수 있겠네요. 이를 백엔드가 처리한다면 그저 로그인 웹페이지를 프론트로 내려주는 것 외에는 불가능하겠죠.

위에서 말한 "프론트가 ID Token을 받아오는 플로우"를 SDK 방식이라 하고, 반대로 "백엔드가 ID Token을 받아오는 플로우"를 REST API 방식이라고 하겠습니다. 두 방식의 차이점에 대해서는 아래에서 자세히 다룰 거에요.

프로바이더마다 응답을 따로따로 처리해 줄 필요가 없다

또 다른 장점이 있습니다.

OIDC 프로토콜에서는 기존 OAuth 2.0 스펙에서 access token, refresh token을 내려줄 때 추가적으로 ID Token을 발급해줍니다. 이 ID Token에는 iss(발급자), aud(발급 대상) 등 기본적인 정보가 포함되어 있고, OpenID 스펙에서 정의하는 Standard Claims를 통해 유저의 정보를 통일된 포맷으로 받아볼 수 있습니다.

반면 OAuth 2.0 방식에서, a 프로바이더에서는 유저 식별자를 id 로, b 프로바이더에서는 유저 식별자를 user_id 와 같이 응답을 다르게 내려줄 수 있기 때문에 기존 OAuth 2.0 스펙에서는 프로바이더의 문서 스펙에 맞춰서, 각각의 응답 내용을 매핑해주는 객체를 만들어야 했습니다.

하지만 ID Token을 사용하면 각각의 프로바이더들은 이러한 userinfo 를 openid 스펙에 정의된 클레임 이름에 맞춰서 내려주기 위해 노력합니다. 앞에서 말한 유저 식별자는 JWT의 sub 클레임으로 통일됩니다. 그 외 이메일, 닉네임, 이름과 같은 유저 정보들은 다음 Open ID Connect Core 1.0에 정의된 5.1. Standard Claims 장에서 어떻게 정의해야 하는지 볼 수 있습니다.

백엔드에서는 ID Token을 매핑해줄 하나의 객체만 만들면 됩니다. 심지어 이마저도 이미 스프링 시큐리티에서 제공하고 있습니다. KakaoOauth2UserInfo , AppleOauth2UserInfo 와 같은 것들은 그저 OIDC가 없던 시절에 태어났을 뿐인 범부에 불과할 뿐이죠.

앞으로 많이 쓸 거 같은데?

이러한 장점 덕분인지 OIDC에 대한 관심도는 (제가 느끼기에) 나날이 늘어나고 있습니다.

특히 카카오가 22년 3월에 Open ID Connect를 도입한 이후 OIDC 로그인을 채택한 신규 프로젝트가 많아졌다는 것이 체감됩니다. 앞에서 말했듯이 국내에서 소셜 로그인 하면 카카오, 애플, 구글을 뽑잖아요? 애플과 구글은 이미 OIDC를 지원하고 있는 상황에서, 카카오가 OIDC를 지원하기 시작하면서 신규 프로젝트에서도 OIDC 도입 논의가 활발하게 이루어지는 것 같습니다.

하지만 국내에서 OIDC 관련 레퍼런스, 그중에서도 Spring Security 관련 레퍼런스는 사실상 없는 거나 다름없습니다. 저도 이번에 OIDC를 도입하면서 많은 어려움을 겪었지만... 아무튼, 이 글이 도움이 되기를 바라면서 다음 섹션으로 넘어가도록 하겠습니다.

REST API vs SDK

사진으로 이해해봅시다

OIDC를 도입하려고 한다면, REST API 방식과 SDK 방식의 차이에 대해서 알고 있어야 합니다.
아래 카카오 문서 사진을 보면서 알아봅시다.

REST API 방식(수정된) SDK 방식

(참고) SDK 방식을 보시면 1-5, 2-1, 2-2번 로직을 Service Server에서 수행하고 있습니다.
react native에서는 관련 라이브러리를 사용하므로, 해당 로직을 Service Client가 수행한다고 이해해야 합니다. 파란색 선을 확인해주세요.

핵심은 "누가 ID Token을 받아올 것인가?" 입니다.

REST API 방식에서는 Service Server, 즉 백엔드가 ID Token 발급을 위한 프로토콜을 처리합니다.
SDK 방식에서는 Service Client, 즉 프론트엔드가 ID Token 발급을 위한 프로토콜을 처리합니다.

둘 다 구현해본 후기

저희 팀의 경우 슬프게도... 백엔드가 REST API 방식으로 구현이 끝난 이후에 SDK 방식으로 전환이 논의되었고...
저는 의도치 않게 두 방식을 모두 경험해본 사람이 되어버렸습니다.

하지만 이미 일어난 일은 어쩔 수 없는 법...! 두 방식 모두 시도해본 후기를 남겨보겠습니다.

REST API 방식

  1. 스프링 시큐리티에서 제공하는 OIDC 지원 기능(DefaultOidcService)을 그대로 활용할 수 있습니다. (레퍼런스)
  2. 카카오 로그인의 경우 OIDC의 표준 스코프 이름(email <-> profile_email)을 사용하지 않기 때문에 발생하는 문제가 있습니다. 이 부분은 2편에서 다룰 예정입니다.
  3. 애플 로그인의 경우 client_secret 을 직접 만들어야 하는 번거로움이 있습니다.

SDK 방식

  1. 스프링 시큐리티에서 제공하는 OIDC 지원 기능을 전부 활용할 수 없습니다. (중요)

    Q. 해당 내용은 확인하였고, spring으로 rest api를 사용할 때와 javascript sdk로 사용할 때를 비교해보니 아래 내용에 차이가 있어서 이걸 어떻게 해야할지 잘 모르겠습니다. ... (중략) ... javascript sdk 로 구현했을 경우, 사전에 호출하는 값이 없어 registrationId의 값을 못받아 오고, 바로 code 값을 파라미터로 던져주던데, 이에 대한 해결방안이 있을까요?

    A. 아쉽지만, 직접 개발 하셔야 합니다.
    redirect_uri를 처리할 수 있는 Controller 또는 AuthenticationFilter를 직접 구현하여 서비스측의 인가 처리를 직접 하셔야 합니다. Spring Security의 OAuth2를 사용하면 registration, provider 설정으로 간단히 웹 기반의 OAuth2 스팩의 로그인을 사용할 수 있지만 카카오톡으로 인증이 이루어지는 경우 이를 그대로 사용할 수는 없습니다.

    (카카오 DevTalk QnA 발췌)

  2. ID Token 검증 로직을 직접 구현해야 합니다. (REST API에서는 시큐리티가 다 해줍니다)
  3. 프론트에서 카카오 서버에 요청하고 > ID Token을 받아오는 과정 (Step 1, 2) 을 담당하기 때문에, client_secret 을 만드는 공수가 없어집니다.

그래서 결론은?

두 방식 모두 일장일단이 있습니다.

따라서 각자의 방식에서 취할 수 있는 (장점의 효용가치) - (단점의 효용가치) 값의 trade-off 를 따져봐야 합니다.
아래 비교 기준을 통해서 각자의 서비스 방향성에 맞는 방식을 골라보시길 바랍니다.

REST API 방식

  • 스프링 시큐리티가 제공하는 OIDC 지원 기능을 그대로 쓰고 싶은 경우
  • 현재 개발하려는 서비스가 웹 환경인 경우

SDK 방식

  • 현재 개발하려는 서비스가 앱 환경인 경우
  • 네이티브 로그인 기능을 통한 UX 개선이 중요한 경우

저희 팀은 웹/앱 모두 지원하지만, 앱을 주력으로 하고 있습니다.
따라서 로그인 시 네이티브 기능을 사용할 수 있는 SDK 방식을 채택했습니다.

다음 글에서는...

2편에서는 REST API 방식으로 구현한 카카오 소셜 로그인 예제를 기반으로,
Spring Security가 어떻게 OIDC 인증을 구현하고 있는지 딥다이브 해보는 시간을 가져봅시다.

3편에서는 SDK 방식으로 구현한 전체 예제를 다루도록 하겠습니다.
SDK 방식에서 ID Token을 검증하는 부분의 경우 모든 로직을 직접 구현하지 않고,
스프링 시큐리티가 제공하는 유틸리티를 사용합니다.

이때 2편에서의 딥다이브 파트에 대한 이해가 필요하니, SDK 방식을 채택할 분들도 2편을 꼼꼼히 봐주세요.
읽어주셔서 감사합니다.

profile
글 쓰는 코딩노예

0개의 댓글