Google OAuth2 로그인

뾰족머리삼돌이·2024년 8월 26일
0

Spring Security

목록 보기
10/16

지난 JWT 로그인 에 이어서 이번에는 Google OAuth2 로그인을 적용시켜보려고 한다.

프로젝트는 그대로 사용할 예정이고,
Spring Security에서의 OAuth2에 대한 이해는 공식문서 혹은 OAuth2 Login, OAuth2 Client 포스팅을 참고하자

Spring Security OAuth2 동작흐름

OAuth2 로그인은 권한 부여방식에 따라 동작흐름이 변화한다.
가장 기본적인 방식이라고 볼 수 있는 Authorization Code의 동작흐름은 아래와 같다.

The OAuth 2.0 Authorization Framework, RFC 6749에서 가져온 Authorization Code 방식의 동작흐름도다.
먼저 OAuth 2.0에는 네 가지의 동작 주체가 존재한다.

역할설명
resource owner보호된 리소스에 대한 접근 권한을 부여할 수 있는 엔티티, 즉 사용자
resource server보호된 리소스를 호스팅하는 서버, access token을 이용하여 리소스 요청을 수락하고 응답가능
clientresource owner를 대신하여 해당 권한을 이용하여 보호된 리소스 요청을 하는 애플리케이션
authorization serverresource owner 인증을 통해 권한을 획득한 후, client에게 access token을 발행하는 서버

쉽게 말하자면 Client가 현재 OAuth 2.0을 적용하고자 하는 Spring Boot 애플리케이션이고,
authorization serverresource server는 Google, resource owner는 인증을 요구하는 사용자다.

  1. 사용자가 Client를 통해 OAuth 2.0 정보를 얻어오려고 요청
  2. 사용자 자격증명을 입력할 수 있는 페이지로 redirect
  3. 자격증명을 입력하고, 권한접근을 허락하면 Client에서 authorization serverAuthorization Code와 함께 Access Token 요청
  4. Access Token, Refresh Token( 선택적 ), Id Token으로 이뤄진 응답 수신
  5. Id Token으로 사용자 정보 획득
    a. 획득과정에서 Access Token으로 접근 허용된 정보인지 확인

대략적인 작업의 흐름은 위 구성과 같다.

OAuth2AuthorizationRequestRedirectFilter

처음 사용자 자격증명을 획득하기 위해 redirect 를 일으키는 클래스다.
OAuth2AuthorizationRequestResolver를 이용하여 현재 사용자가 입력한 경로가 redirect가 필요한지 파악하고,
필요하다면 /oauth2/authorization/{registrationId} 경로로 redirect를 진행한다.

redirect가 필요하다고 판단하면 OAuth2 인가서버로의 요청을 생성하고, 이를 Session에 저장한다.

OAuth2LoginAuthenticationFilter

OAuth2AuthorizationRequestRedirectFilter에서 redirect가 진행되지 않았을 경우, 실질적인 OAuth2 요청을 처리하는 필터다.
앞서 세션에 저장했던 OAuth2 인가서버로의 요청정보를 읽어온 뒤, 애플리케이션에 등록된 Provider( 여기선 Google )로의 요청인지 확인한다.

이후, 인가서버로 Authorization Code을 포함한 요청을 토큰 엔드포인트 보내 토큰정보를 획득한다.

OidcAuthorizationCodeAuthenticationProvider

OAuth2LoginAuthenticationFilter에서 토큰 엔드포인트로 요청을 보내는 작업을 실제로 수행하는 클래스다.

scope가 OpenID가 아니라면 OAuth2LoginAuthenticationProvider에서 작업을 처리한다.

Open Id Spec에 대해서는 관련 문서를 읽어보자..

응답받은 토큰 정보는 OAuth2AccessTokenResponse 객체로 저장된다.
해당 객체는 Access Token과 Refresh Token, Id Token으로 이루어져 있다.

  • Access Token : 사용자가 클라이언트에게 부여한 권한을 증명
  • Refresh Token : Access Token 만료 시, 재발급을 위한 자격증명
  • Id Token : 사용자의 실제 정보를 가지고있는 JWT토큰

이후 동작은 애플리케이션에 등록된 Provider( 여기선 Google )를 이용하여 JWT Decoder를 생성,
Id Token을 디코딩 하여 사용자 정보를 포함하는 OidcIdToken 인스턴스를 생성한다.

다음으론 Access Token에 부여된 권한과 OidcIdToken 인스턴스의 권한을 비교하고,
OAuth2LoginAuthenticationFilter에서 Authentication 타입의 OAuth2AuthenticationToken 인스턴스를 생성한다.

최종적으로는 OAuth2LoginAuthenticationFilter에서 OAuth2AuthorizedClient 인스턴스로 인증된 사용자정보를 관리한다.

SecurityContextHolder에도 저장한다.

프로젝트에 적용

OAuth 2.0 인증을 사용한다는 것은 사용자에 대한 인증을 외부 Provider에게 위임한다는 의미가 된다.
따라서, 프로젝트에서 처리할 작업은 인증이 성공한 뒤 동작하는 SuccessHandler에 있다.

그러기에 앞서 우선 의존성을 추가해줘야하고,
ClientRegistration 객체 생성을 위한 정보를 Spring Boot 애플리케이션에 등록해줘야 한다.

OAuth 2.0 Client 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

현재 나의 애플리케이션이 OAuth 2.0 Spec에서의 Client 역할을 할 것이기 때문에 관련 의존성을 추가해준다.

ClientRegistration 객체 생성을 위한 정보 입력

spring:
  application:
    name: JWTAuthentication
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: client-id
            client-secret: client-secret
            client-authentication-method: client_secret_basic
            authorization-grant-type: authorization_code
        provider:
          google:
            authorizationUri: https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent

프로퍼티 파일을 통해서 ClientRegistration 객체 생성을 위한 정보를 입력할 수 있다. 관련 정보는 이전 포스팅이전 포스팅에 소개되어있다.

중요한 부분은 client-idclient-secret 이다.
현재 애플리케이션에 OAuth 인증을 적용하면 사용자로 부터 권한을 수락받아 Provider( 여기선 Google )로 요청을 진행한다.
client-idclient-secret은 이때, 요청하는 클라이언트 애플리케이션이 Provider에게 등록된, 안전한 애플리케이션임을 증명하기 위해 사용된다.

client-authentication-methodauthorization-grant-type

이 프로퍼티는 Google의 기본적으로 적용되는 정보를 담고있다.
이는 CommonOAuth2Provider 클래스에서 Provider별 기본 설정을 확인할 수 있다.

authorizationUriGoogle Provider는 ?access_type=offline&prompt=consent를 추가하지 않으면 Refresh Token을 발급해주지 않기 때문에 작성했다.

client-id와 client-secret 획득

Google 기준으로 Google API Console에 애플리케이션을 등록하면 위 이미지와 같이 획득할 수 있다.

SecurityConfiguration 수정

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
            .sessionManagement(sessions -> sessions
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션 사용안함.
            )
            .csrf(AbstractHttpConfigurer::disable) // 사용자 정보 관리에 쿠키 사용 안하므로, csrf 보안 비활성화
            // AuthenticationFilter 등록
            .addFilterAfter(loginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            .addFilterAfter(jwtAuthenticationFilter, LoginAuthenticationFilter.class)
            .authorizeHttpRequests(requests -> requests
                    .anyRequest().authenticated()
            )
            .oauth2Login(Customizer.withDefaults())
            .build();
}

세션을 사용하지 않고 OAuth2 인증을 활성화 시켰을 때, 동작하는 SuccessHandler는 SavedRequestAwareAuthenticationSuccessHandler 다.

해당 Handler에서는 요청 캐시에 저장했던 이전의 요청으로 돌려보내는 동작을 하는데, OAuth2 인증에서는 null 이다.
최종적으로 redirect 되는 주소는 / 이다.

세션을 사용하지 않으므로 이전에 인증받은 정보는 세션에 존재하지 않아서 다시 인증을 요청한다. 따라서, redirect를 하지않고 사용자 정보 인증에 사용하는 JWT를 발급해주는 SuccessHandler를 등록해야한다

OidcAuthenticationSuccessHandler

@Component
@RequiredArgsConstructor
public class OidcAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private final JwtProvider jwtProvider;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        OidcUser oidcUser = (OidcUser) authentication.getPrincipal();

        String name = oidcUser.getClaim("email");
        JwtToken token = jwtProvider.generateToken(name, oidcUser.getAuthorities());

        Cookie refresh_cookie = new Cookie("refresh", token.getRefresh());
        refresh_cookie.setPath("/");
        refresh_cookie.setHttpOnly(true);

        response.addHeader(HttpHeaders.AUTHORIZATION, token.getAccess());
        response.addCookie(refresh_cookie);
    }
}

// SecurityConfig.class
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
            .sessionManagement(sessions -> sessions
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션 사용안함.
            )
            .csrf(AbstractHttpConfigurer::disable) // csrf 보안 비활성화
            // AuthenticationFilter 등록
            .addFilterAfter(loginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            .addFilterAfter(jwtAuthenticationFilter, LoginAuthenticationFilter.class)
            .authorizeHttpRequests(requests -> requests
                    .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                    .successHandler(oidcAuthenticationSuccessHandler))
            .build();
}

현재 프로젝트의 사용자 인증수단인 JWT를 발급해주고, 이를 등록해준다.

OAuth2 인증도 인증에 성공하면 SecurityContextHolder에 저장한다.
즉, (OidcUser) authentication.getPrincipal() 형식으로 꺼내올 수 있다

고려해볼 부분은 인증된 사용자를 OAuth2LoginAuthenticationToken 타입으로 받아오는 것이다.
해당 클래스에는 OidcUserOAuth2AccessToken, OAuth2RefreshToken 등이 모두 저장되어있다.

만약, 이와 관련된 작업처리를 원한다면 해당 타입을 이용하는게 좋다고 생각된다.

0개의 댓글