
OAuth 2.0 Authorization Code Flow 과정 중 아래 과정 살피기
- 사용자가 소셜 로그인을 통한 인증을 시도할 때, 해당 사용자의 정보에 접근하기 위한 권한 즉, Authorization Code 를 얻기 위해 OAuth 2.0 Authorization Server 로 Redirect 되는 과정
- 사용자가 개인 정보 수집에 대한 동의를 한 상태에서 로그인을 성공적으로 마쳤을 때, 등록한 OAuth Redirect URI 에 Authorization Code 를 포함하여 서버로 리다이렉트 되고, 해당 Authorization Code 를 Access Token 으로 교환하는 과정
public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
// ...
private OAuth2AuthorizationRequestResolver authorizationRequestResolver;
// ...
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}
} catch (Exception ex) {
this.unsuccessfulRedirectForAuthorization(request, response, ex);
return;
}
// ...
}
// ...
}
- 사용자 소셜 로그인 시도 시
브라우저 → 백엔드 서버로 다음과 같이 요청을 보낸다.
GET /oauth2/authorization/{registrationId}- Spring Security 의 OAuth2AuthorizationRequestRedirectFilter 클래스가 해당 요청을 감지한 후 처리한다.
✨ OAuth2AuthorizationRequestRedirectFilter Class
- Spring Security 에서 OAuth 2.0 Authorization Request 를 처리하는 필터이다.
- 주로 Authorization Code Grant Flow 및 Implicit Grant Flow와 같은 OAuth 2.0 인증 프로토콜에서 사용된다.
Authorization Code Grant Flow vs Implicit Grant Flow
- Authorization Code Grant Flow : 보안 측면에서 강력하며, 클라이언트는 인가 코드를 받은 후에만 액세스 토큰을 요청할 수 있다.
- Implicit Grant Flow : 액세스 토큰이 바로 리다이렉션 URL의 프래그먼트에 포함되어 반환되며, 인가 코드를 교환하는 추가 단계가 없다. 보안 측면에서는 Authorization Code Flow보다 취약할 수 있다.
- 주요 목적은 사용자에게 로그인 및 동의 페이지로 리다이렉트하고, OAuth 2.0 Authorization Server 로부터 Authorization Code 를 부여받는 프로세스를 시작하는 것이다.
💡 doFilterInternal Method
- authorizationRequestResolver 의 resolve 메서드를 호출하여 Authorization Request 를 위한 OAuth2AuthorizationRequest 객체를 생성한다.
- Authorization Request 객체를 이용하여 sendRedirectForAuthorization 메서드를 호출하고 OAuth 2.0 Authorization Server 로 Redirect 를 수행한다.
public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";
private final ClientRegistrationRepository clientRegistrationRepository;
private final AntPathRequestMatcher authorizationRequestMatcher;
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
String registrationId = this.resolveRegistrationId(request);
if (registrationId == null) {
return null;
}
String redirectUriAction = getAction(request, "login");
return resolve(request, registrationId, redirectUriAction);
}
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId,
String redirectUriAction) {
if (registrationId == null) {
return null;
}
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
}
Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration, attributes);
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
builder
.clientId(clientRegistration.getClientId())
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
.redirectUri(redirectUriStr)
.scope(clientRegistration.getScopes())
.state(this.stateGenerator.generateKey())
.attributes(attributes);
this.authorizationRequestCustomizer.accept(builder);
return builder.build();
}
private String resolveRegistrationId(HttpServletRequest request) {
if (this.authorizationRequestMatcher.matches(request)) {
return this.authorizationRequestMatcher.matcher(request).getVariables()
.get(REGISTRATION_ID_URI_VARIABLE_NAME);
}
return null;
}
}
✨ DefaultOAuth2AuthorizationRequestResolver Class
- OAuth2AuthorizationRequestResolver 인터페이스의 구현체이다. 즉, Spring Security 에서 OAuth 2.0 Authorization Request 를 해석하고 생성하는 클래스이다.
- 사용자로부터 소셜 로그인 요청이 들어왔을 때 OAuth 2.0 Authorization Server 로 Redirect 하기 위한 OAuth 2.0 Authorization Request 객체를 구성하는 역할을 담당한다. ( 이는 URL 및 Parameter를 구성하는 데 사용된다. )
💡 resolveRegistrationId Method
- 주어진 요청이 특정 패턴과 일치하는 지 확인한 후 일치할 경우 해당 패턴에서 추출한 registrationId 를 반환한다.
authorizationRequestMatcher = /oauth2/authorization/{registrationId}💡 resolve Method
- ClientRegistrationRepository 에는 application.yml 파일에서 설정한 OAuth 2.0 Client Registration 정보가 들어있다.
- registrationId 에 해당하는 OAuth 2.0 Client Registration 객체를 가져온다.
- 이를 활용하여 OAuth 2.0 Authorization Request 객체를 구성하고 반환한다.
public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
// ...
private RedirectStrategy authorizationRedirectStrategy = new DefaultRedirectStrategy();
private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository();
// ...
private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
OAuth2AuthorizationRequest authorizationRequest) throws IOException {
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
}
this.authorizationRedirectStrategy.sendRedirect(request, response,
authorizationRequest.getAuthorizationRequestUri());
}
// ...
}
💡 sendRedirectForAuthorization Method
- 생성된 OAuth 2.0 Authorization Request 객체를 통해 Redirection 를 위한 작업을 수행한다.
- authorizationRequestRepository 의 saveAuthorizationRequest 메서드를 호출하여 OAuth 2.0 Authorization Request 객체를 HttpSession 에 저장한다.
- 이후 authorizationRedirectStrategy 의 sendRedirect 메서드를 통해
백엔드 서버 → 브라우저 → OAuth 2.0 인가 서버의 흐름으로 Redirection 을 수행한다.GET authorization_uri? response_type=code &client_id=my_client_id &redirect_uri=my_redirect_uri &scope=scope1 scope2 &state=random_string✋ state parameter
- CSRF (Cross-Site Request Forgery) 공격을 방지하기 위한 보안 목적의 매개변수로 사용된다.
- 이 매개변수는 랜덤하게 생성된 문자열로 구성되어 있으며, 클라이언트가 인증 요청을 시작할 때 생성되어 응답 받을 때까지 변하지 않는다.
- 인증 요청을 보낸 클라이언트는 응답을 받을 때 해당 매개변수를 확인하여 요청과 응답이 서로 일치하는지 확인한다.
- 이를 통해 중간자 공격 등으로 인한 보안 문제를 방지할 수 있다.
public final class HttpSessionOAuth2AuthorizationRequestRepository
implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
private static final String DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME = HttpSessionOAuth2AuthorizationRequestRepository.class
.getName() + ".AUTHORIZATION_REQUEST";
private final String sessionAttributeName = DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME;
// ...
@Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest,
HttpServletRequest request, HttpServletResponse response) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(response, "response cannot be null");
if (authorizationRequest == null) {
removeAuthorizationRequest(request, response);
return;
}
String state = authorizationRequest.getState();
Assert.hasText(state, "authorizationRequest.state cannot be empty");
request.getSession().setAttribute(this.sessionAttributeName, authorizationRequest);
}
// ...
}
✨ HttpSessionOAuth2AuthorizationRequestRepository Class
- OAuth 2.0 Authorization Request 정보를 관리하기 위한 기본적인 구현을 제공하는 클래스이다.
- OAuth2AuthorizationRequestRepository 인터페이스를 구현하고 있어, OAuth 2.0 Authorization Request 정보를 세션에 저장하고 검색하는 역할을 수행한다.
💡 saveAuthorizationRequest Method
- OAuth 2.0 Authorization Request 객체에 저장된 정보를 세션에 저장한다.
HttpSession과 JSESSIONID
- 클라이언트가 웹 애플리케이션에 최초로 접속했을 때 톰캣과 같은 서블릿 컨테이너가 새로운 세션을 생성하고 그 세션에 대한 고유한 식별자인 JSESSIONID를 생성한다.
- 서버는 JSESSIONID를 클라이언트에게 쿠키로 전송한다. 이 쿠키는 클라이언트의 브라우저에 저장되어 나중에 클라이언트가 서버에 요청을 보낼 때마다 함께 전송된다.
- 서버는 JSESSIONID를 사용하여 클라이언트의 세션을 식별하고, 클라이언트에게 연관된 세션 데이터를 유지한다.
- 세션에서 OAuth 2.0 Authorization Request 에 관한 데이터를 저장하고 검색할 때 사용되는 키 값으로 sessionAttributeName 을 지정한다.
- OAuth 2.0 Authorization Server 로부터 부여받은 Authorization Code 를 Access Token 으로 교환하기 위한 단계에서 사용될 수 있다.
- 이후 사용자에게 로그인 페이지를 표시하게 된다. 사용자는 OAuth 2.0 인가 서버에서 자신의 계정 정보로 로그인하고, 해당 어플리케이션이 사용자의 정보에 접근하기 위한 권한 부여를 승인 또는 거부할 수 있다.
- 사용자 인증 완료 시
OAuth 2.0 인가 서버 → 브라우저 → 백엔드 서버의 흐름으로 리다이렉트를 수행한다.
GET redirect_uri?code=authorization_code&state=state_value- 이후 OAuth2LoginAuthenticationProvider 의 authenticate 메서드가 실행되는데, 이는 Authorization Code 를 통해 Access Token 을 교환하고, 교환된 Access Token 을 사용하여 사용자 정보를 가져오는 역할을 수행한다.
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
// ...
private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;
private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
private GrantedAuthoritiesMapper authoritiesMapper = ((authorities) -> authorities);
// ...
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken) authentication;
if (loginAuthenticationToken.getAuthorizationExchange().getAuthorizationRequest().getScopes()
.contains("openid")) {
return null;
}
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
try {
authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
loginAuthenticationToken.getClientRegistration(),
loginAuthenticationToken.getAuthorizationExchange()));
}
catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
}
OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
.mapAuthorities(oauth2User.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(),
oauth2User, mappedAuthorities, accessToken, authorizationCodeAuthenticationToken.getRefreshToken());
authenticationResult.setDetails(loginAuthenticationToken.getDetails());
return authenticationResult;
}
}
✨ OAuth2LoginAuthenticationProvider Class
- AuthenticationProvider 인터페이스를 구현한 클래스 중 하나이다.
- OAuth 2.0 Loign 을 이용한 Authentication 정보를 제공하는 클래스로, Authorization Code → Access Token Exchange 작업을 수행하고, Access Token → Resoure Exchange 작업을 수행한다.
- 최종적으로 사용자 인증이 완료되었을 때 완전한 Authentication 정보가 담긴 OAuth2.0 Login Authentication Token 객체를 생성하여 반환한다.
✨ 흐름 최종 정리 ✨
- 사용자가 애플리케이션에서 소셜 로그인 시도 시 Client Server 는 OAuth 2.0 Authorization Code 를 얻기 위해 OAuth 2.0 Authorization Server 로 Redirection 시킨다.
- 사용자는 OAuth 2.0 Authorization Server 에서 로그인하고 권한 부여를 허용한다.
- 로그인을 통한 인증 성공 시 OAuth 2.0 Authorization Server 는 OAuth 2.0 Authorization Code 정보를 포함하여 다시 Client Server 로 Redirection 시킨다.
- Client Server 는 OAuth 2.0 Authorization Code 를 사용하여 다시 OAuth 2.0 Authorization Server 로부터 Access Token 을 교환하는 작업을 수행한다.
- Client Server 는 Access Token 을 사용하여 Resoure Server 로부터 OAuth 2.0 User 정보를 가져오고, 최종적으로 OAuth 2.0 Login Authentication Token 을 생성하여 Authentication 정보를 SecurityContextHolder 에 저장한다.
- 위의 과정 중 Access Token 취득 후 직접 커스텀한 CustomOAuth2UserService 클래스의 loadUser 메서드가 호출되고, 데이터베이스 조회를 통해 현재 사이트에 가입된 사용자인 지 판단 후 회원가입 또는 회원 정보 갱신 로직을 수행시키며, 최종적으로 Spring Security 가 인증 여부를 확인하여 JWT 를 발급을 할 수 있도록 OAuth2User 객체를 반환한다.