Ordinary
About

Spring Security OAuth2 Client Refresh Token

profileordilov / 2022. 2. 9

Google OAuth2 Refresh Token

구글 OAuth2 기준 다른 설정 없이 Authorization Code 로 Token을 받으면 refresh Token을 받을 수 없습니다. 구글 OAuth2에서 refresh token을 요청하려면 필요한 조건들이 있습니다.

  • access_type : offline 설정
  • prompt : consent (처믕 설정 시)

access_type 옵션은 브라우저가 아닌 서버에서 재요청하기 위해 refresh token을 요청하는 옵션입니다. prompt는 OAuth2 인증을 위해 요청하는 화면이 나오는 기준을 설정합니다. 설정하지 않으면 기본적으로 처음 요청시에만 확인을 받고 그 이후에는 받지 않습니다. 처음에 refresh token을 받았으면 상관 없지만 테스트를 위해 refresh token을 확인하고 싶은 경우 consent를 설정해야 합니다. 그 이유는 구글에서 refresh token은 사용자에게 직접 인증을 받는 순간에만 제공됩니다. 그 이후에는 access_type이 offline 이더라도 이미 인증 받은 경우 refresh token이 제공되지 않습니다.

Spring Security OAuth2 Client에서 설정하기

spring security oauth2 client를 사용하면 oauth2 연결을 쉽게 사용할 수 있습니다. application.yml에서 client-id와 client-secret 그리고 인증을 받을 범위등 옵션등을 설정할 수 있습니다.

security: oauth2: client: registration: google: client-id: {client-id} client-secret: {client-secret} redirectUri: "{baseUrl}/oauth2/callback/{registrationId}" scope: - email - profile

하지만 yml 설정에서 할 수 없는 부분은 refresh token을 받기 위한 설정을 추가할 수 없습니다. access_type이나 prompt 같은 옵션 파라미터를 추가하려면 추가적인 설정이 필요합니다. 설정할 수 있는 방법은 두 가지가 방법이 있습니다.

ClientRegistrationRepository 수정하기

ClientRegistrationRepository는 위에서 등록한 client registration들을 불러와 정보들을 보관합니다. 여기에서 인증 요청을 위해 사용되는 주소는 authorizationUri입니다. 이 주소 뒤에 필요한 파라미터 쿼리를 추가해주면 됩니다. access_type을 추가한다면 다음과 같은 주소가 됩니다.

"https://accounts.google.com/o/oauth2/v2/auth/?access_type=offline"

@Configuration public class OAuth2LoginConfig { @Bean public ClientRegistrationRepository clientRegistrationRepository() { return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); } private ClientRegistration googleClientRegistration() { return ClientRegistration.withRegistrationId("google") .clientId("google-client-id") .clientSecret("google-client-secret") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") .scope("openid", "profile", "email", "address", "phone") .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") .tokenUri("https://www.googleapis.com/oauth2/v4/token") .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") .userNameAttributeName(IdTokenClaimNames.SUB) .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") .clientName("Google") .build(); } }

스프링 공식 문서에서 제공하는 ClientRegistration 구성 코드입니다. 하지만 설명에는 모든 설정이 하드코딩된 형태로 보통 이렇게 client-id 나 client-secret을 공개된 형태로 사용하지 않습니다. 위에서 사용하듯이 yml처럼 환경 변수로 코드 밖에서 따로 관리하길 원합니다. 따로 환경 변수를 불러올 수도 있지만, oauth2 client에서 기본적으로 설정한 구성을 그대로 불러오고 싶을 수 있습니다. 이 경우 Spring에서는 OAuth2ClientProperties 클래스를 통해 불러올 수 있습니다. 따라서 위의 코드를 수정하면 다음과 같은 형태가 됩니다.

@Configuration @RequiredArgsConstructor public class OAuth2LoginConfig { private final OAuth2ClientProperties properties; @Bean public ClientRegistrationRepository clientRegistrationRepository() { return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); } private ClientRegistration googleClientRegistration() { Registration googleRegistration = this.properties.getRegistration().get("google"); return ClientRegistration.withRegistrationId("google") .clientId(googleRegistration.getClientId()) .clientSecret(googleRegistration.getClientSecret()) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") .scope("openid", "profile", "email", "address", "phone") .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth/?access_type=offline") .tokenUri("https://www.googleapis.com/oauth2/v4/token") .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") .userNameAttributeName(IdTokenClaimNames.SUB) .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") .clientName("Google") .build(); } }

google이 아니더라도 registration에 등록한 키 값을 기준으로 불러와 속성들을 가져와 사용할 수 있습니다.

CustomAuthorizationRequestResolver 추가하기

ClientRegistrationRepository를 수정하는 것도 방법이지만 옵션만 추가하는 건데 등록 정보 전부를 불러오는 건 번거로울 수 있습니다. 다른 방법으로는 AuthorizationRequestResolver를 구현하는 방법으로 request에 resolver를 추가할 수 있습니다. AuthorizationRequestResolver를 구현하고 나면 추가할 위치는 다음 코드와 같습니다.

@EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig extends WebSecurityConfigurerAdapter { private final ClientRegistrationRepository clientRegistrationRepository; @Override protected void configure(HttpSecurity http) throws Exception { http. .oauth2Login() .authorizationEndpoint() .authorizationRequestResolver(customAuthorizationRequestResolver))

AuthorizationRequestResolver는 ClientRegistrationRepository를 생성 파라미터로 받습니다. 이 코드는 Spring docs에서 제공하는 파리미터를 수정하는 코드에서 access_type을 추가한 코드입니다. 코드를 살펴보면 생성자로 ClientRegistrationRepository를 주입 받습니다. 위에서 ClientRegistationRepository를 수정했다면 중복해서 처리되므로 하나만 선택해서 설정해야 합니다. 다음으로 OAuth2AuthorizationRequestResolver로 DefaultOAuth2AuthorizationRequestResolver를 만들어서 사용합니다. 구현하는 클래스를 필드값으로 갖고 있는 이유는 DefaultOAuth2AuthorizationRequestResolver는 final 클래스로 상속받을 수 없습니다. 상속받을 수 없지만 Spring에서 기본적으로 사용하는 위 클래스가 구현하는 기능이 많기 때문에 가져와서 쓰는 것이 편합니다. 모든 기능을 그대로 복사해서 사용해도 되지만 그것도 마찬가지로 중복이 많아지게 됩니다. 따라서 필드값으로 사용해서 기본 기능을 실행하고 추가적인 옵션을 넣는 식으로 구현됩니다.

@Component public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { private final OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver; public CustomAuthorizationRequestResolver( ClientRegistrationRepository clientRegistrationRepository) { this.defaultAuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( clientRegistrationRepository, "/oauth2/authorize"); } @Override public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { OAuth2AuthorizationRequest authorizationRequest = this.defaultAuthorizationRequestResolver.resolve(request); return authorizationRequest != null ? customAuthorizationRequest(authorizationRequest) : null; } @Override public OAuth2AuthorizationRequest resolve( HttpServletRequest request, String clientRegistrationId) { OAuth2AuthorizationRequest authorizationRequest = this.defaultAuthorizationRequestResolver.resolve( request, clientRegistrationId); return authorizationRequest != null ? customAuthorizationRequest(authorizationRequest) : null; } private OAuth2AuthorizationRequest customAuthorizationRequest( OAuth2AuthorizationRequest authorizationRequest) { Map<String, Object> additionalParameters = new LinkedHashMap<>(authorizationRequest.getAdditionalParameters()); additionalParameters.put("access_type", "offline"); additionalParameters.put("prompt", "consent"); return OAuth2AuthorizationRequest.from(authorizationRequest) .additionalParameters(additionalParameters) .build(); } }

Refresh Token 사용하기

위에서 받아온 Refresh Token은 OAuth2AuthorizedClientService에서 관리됩니다. 따라서 사용할 클래스에서 Bean으로 주입받고 사용하면 됩니다. OAuth2AuthorizedClientService에서 loadAuthorizedClient로 저장된 정보를 불러와 사용할 수 있습니다.

private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; OAuth2AuthorizedClient user = oAuth2AuthorizedClientService.loadAuthorizedClient("google", authentication.getName()); OAuth2RefreshToken refreshToken = user.getRefreshToken();

Controller에서 사용하는 경우 다른 방법으로 받을 수 있습니다.

@GetMapping("/foo") void foo(@RegisteredOAuth2AuthorizedClient("google") OAuth2AuthorizedClient user) { OAuth2RefreshToken refreshToken = user.getRefreshToken(); }

이렇게 정리한 이유는 OAuth2 Access Token만으로 구현한 글들은 많이 접할 수 있었지만 Refresh Token은 많이 없었습니다. 로그인이 아니라 API를 호출하는 경우 이후에 토큰을 갱신하기 위해서 필요한데 생길 수 있는 문제를 정리해봤습니다.