OAuth 2.0 (1)

강서진·2024년 4월 8일

미니 프로젝트에 소셜 로그인 기능을 넣고 싶었는데, 자꾸 오류가 발생해서, 그리고 및 프론트와의 협업이 익숙하지 않아 JWT를 사용한 회원가입 및 로그인 구현에서 끝났다. 아쉬워서 더 공부해보려 한다.

OAuth2

The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowing the third-party application to obtain access on its own behalf.

OAuth 2.0은 리소스 소유자를 대신하여 리소스 소유자와 HTTP 서비스 간의 상호작용을 승인하거나, 제3자 애플리케이션이 이를 수행하도록 허용하여 자체적으로 HTTP 서비스에 제한적인 액세스를 얻도록 하는 프레임워크이다.
다시 말해, 구글, 페이스북과 같은 다양한 플랫폼의 특정한 사용자 데이터에 접근하기 위해 제3자 어플리케이션(여기서는 우리가 만든 서비스가 되겠다.)이 사용자의 접근 권한을 위임받을 수 있도록 해준다.
이 때 리소스 소유자(Resource Owner)는 사용자, 즉 인증 정보를 가지고 있는 유저를 뜻한다. 클라이언트(Client)는 OAuth 인증을 필요로 하는 어플리케이션이고, 인증 서버(Authorization Server)는 OAuth를 이용해 인증을 처리하는 서버, 리소스 서버(Resource Server)는 인증 정보를 보호하고 있는 서버를 말한다.

즉 개인 프로젝트에 OAuth 2.0을 사용해 구글 로그인을 구현하였다면,

  • 리소스 소유자: 개인 프로젝트를 사용하는 유저
  • 클라이언트: 개인 프로젝트
  • 인증 서버: OAuth 인증 서버(구글이 가진 인증 서버)
  • 리소스 서버: 구글 서버

인 것으로 이해했다.

Authorization Code Grant

권한을 위임하는 방법에는 여러 가지가 있는데, 그 중에서도 가장 많이 쓰게 되는 방법이 Authorization Code Grant이다. 간편 로그인 등에서 쓰이며, 이름대로 Authorization Code를 이용해 인증을 처리한다.
Redirect가 들어가고 복잡해지다보니, 사실 꽤나 전부터 꼭 이 기능을 구현해야지 해놓고 여전히 제대로 이해가 되지 않은 것 같다. 그래서 직접 플로우를 그려보았다...

Implicit Grant

Authorization Code Grant를 단순화한 버전으로, redirect_uri에 직접 access token을 발급한다. 위 그림에서 Authorization code가 발급되어 redirect 되는 부분이 없고 바로 access token이 발급된다고 보면 되겠다.
토큰이 노출되므로 보안에 취약한 편이다.

파라미터

요청 시에 함께 보내야 하는 파라미터들은 다음과 같다.

  • client_id, client_secret
    클라이언트용 자격 증명으로, 인증 서버에 등록할 때 함께 발급된다.
  • redirect_uri
    인증 서버가 응답을 보낼 url이다.
  • response_type
    code: Authorization Code Grant
    token: Implicit Grant
  • grant_type
    access token 요청 시 사용되는 값으로, authorization_code, password, client_credentials, refresh token를 받는다.
  • code
    Authorization Code Grant에서 access token 발급 시 받는 값으로, authorization code를 넣어주면 된다.
  • token_type
    발행된 토큰 scheme으로, Bearer, MAC 등을 받는다.
  • expires_in
    토큰의 TTL(만료기간)

응답 확인

Spring Boot 3.x에서 스프링 시큐리티와 스프링 OAuth2 클라이언트 라이브러리를 사용해 소셜 로그인을 구현해보려면, 먼저 OAuth 제공자인 리소스 서버에서 애플리케이션을 생성 및 등록해야 한다.

여기서 구글은 이미 Provider로 라이브러리에 등록되어 있지만, 네이버나 카카오는 따로 applicaiton.yml에 provider 정보를 등록해주어야 한다.

일단 구글부터 성공한 다음에 다른 OAuth를 시도해보고자 했다.
프로젝트에는 자체적으로 JWT로 구현한 회원가입, 로그인이 존재하기 때문에 소셜 로그인도 여기에 맞추고 싶어서 기존의 Member에 OAuth 관련 칼럼을 추가하였다. 인터넷을 뒤져가며 비슷하게 구현한 사람들의 코드도 참고하며 작성을 해보았으나, 문제는 시큐리티가 합쳐지니까 시큐리티에서 설정하는 oauth uri와 플로우 상에서의 uri들이 헷갈리는 것이었다. 결국 구글에서 제대로 로그인 창을 받아오는 것부터 실패했다...
특히 authorization endpoint는 어떤 uri를 등록하는 것인지, redirection endpoint는 구글에서 OAuth ID를 등록할 때 넣었던 redirection uri와 일치하는 것인지 등에서 많은 혼란을 겪었다.
하여 다시 처음부터 작성해볼 예정이다.

먼저 기본적으로 Google, Github, Facebook, Okta는 이러한 설정값들이 CommonOAuth2Provider에 내장되어 있다.

GOOGLE {
        public ClientRegistration.Builder getBuilder(String registrationId) {
            ClientRegistration.Builder builder = this.getBuilder(registrationId, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, "{baseUrl}/{action}/oauth2/code/{registrationId}");
            builder.scope(new String[]{"openid", "profile", "email"});
            builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
            builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
            builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");
            builder.issuerUri("https://accounts.google.com");
            builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
            builder.userNameAttributeName("sub");
            builder.clientName("Google");
            return builder;
        }
    },
  • Authorization Uri:
    프론트단에서 소셜 로그인 버튼을 눌렀을 때 이동하는 uri. 구글에서 이미 정해둔 상태이다.
  • Token Uri:
    애플리케이션 서버에서 redirect uri로 전달받은 인가 코드를 가지고 토큰 요청을 날리는 uri이다.
  • User Info Uri:
    리소스 서버에서 발급받은 액세스 토큰으로 애플리케이션에서 사용자 정보를 가져올 때 요청을 날리는 uri이다.

직접 해보기

redirect_uri가 오류가 났다길래 오류 세부사항을 눌렀을 때 뜨는 redirect_uri를 구글 클라우드 콘솔에서 등록해주었더니 오류 없이 제대로 연결되었다.
email과 name을 받으려고 했더니 name은 올바른 scope가 아니라고 하여 profile로 교체하였다.

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: google-client-id
            client-secret: google-client-secret
            scope:
              - email
              - profile

다른 시큐리티 설정은 전부 빼고, 엔드포인트를 하나만 만들고 인증을 거치게 작성하였다.

	@Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity

                .httpBasic(AbstractHttpConfigurer::disable)
                .csrf(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(
                        it -> it
                                .anyRequest().authenticated())
                .oauth2Login()
                .defaultSuccessUrl("/api/member/me", true)
                ;

이러면 구글 로그인창이 뜨고, 쭉 진행하면 결과가 그대로 출력된다.

{
  "sub": "숫자",
  "name": "홍길동",
  "given_name": "길동",
  "family_name": "홍",
  "picture": "프로필이미지링크",
  "email": "구글아이디@gmail.com",
  "email_verified": true,
  "locale": "ko"
}

다음으로는 PostMan을 사용해 액세스 토큰을 받아와본다.
PostMan에서 OAuth2.0을 선택하고, 일단은 내용을 확인하기 위함이라 리프레시 토큰은 생각하지 않고 진행하였다.


사진과 같이 필요한 값을 집어넣고 create token을 누르면 구글 로그인창이 뜬다.

로그인을 하면 이렇게 토큰을 받아온다. 혹시 몰라 내용은 가렸다.

id_token을 싹 긁어서 JWT.io에 붙여넣기하면,
아까 로그인했을 때 받았던 내용 전체가 담겨있음을 알 수 있었다.

0개의 댓글