본문 바로가기

Develop/Springboot

Spring Security OAuth2 Login Flow

www.callicoder.com/spring-boot-security-oauth2-social-login-part-2/

 

Spring Boot OAuth2 Social Login with Google, Facebook, and Github - Part 2

Integrate social login with Facebook, Google, and Github in your spring boot application using Spring Security's OAuth2 functionalities. You'll also add email and password based login along with social login.

www.callicoder.com

에 있는 글을 번역하여, 공부하는 용도 입니다.

 

OAuth2 Login Flow 

SecurityConfig.java 파일과 관련한 OAuth2 LoginFlow. 글로 설명된 흐름을 그림으로 그려보았다.

1. OAuth2 login 플로우는 맨처음 frontend client 에서 엔드포인트에 서 요청을 보내면서 시작된다.

http://localhost:8080/oauth2/authorize/{provider}?redirect_uri=<redirect_uri_after_login>

  • provider : google, facebook, github
  • redirect_uri : OAuth2 provider가 성공적으로 인증을 완료했을 때 redirect 할 URI를 지정한다. (OAuth2의 redirectUri 와는 다르다)

2. endpoint로 인증 요청을 받으면, Spring Security의 OAuth2 클라이언트는 user를 provider가 제공하는 AuthorizationUrl로 redirect 한다.
Authorization request와 관련된 state는 authorizationRequestRepository 에 저장된다 (Security Config에 정의함)
provider에서 제공한 AutorizationUrl에서 허용/거부가 정해진다.

  • 이때 만약 유저가 앱에 대한 권한을 모두 허용하면 provider는 사용자를 callback url로 redirect한다. (http://localhost:8080/oauth2/callback/{provider}) 그리고 이때 사용자 인증코드 (authroization code) 도 함께 갖고있다.
  • 만약 거부하면 callbackUrl로 똑같이 redirect 하지만 error가 발생한다.

3. Oauth2 에서의 콜백 결과가 에러이면 Spring Security는 oAuth2AuthenticationFailureHanlder 를 호출한다. (Security Config에 정의함)

4. Oauth2 에서의 콜백 결과가 성공이고 사용자 인증코드 (authorization code)도 포함하고 있다면 Spring Security는 access_token 에 대한 authroization code를 교환하고, customOAuth2UserService 를 호출한다 (Security Config에 정의함)

5. customOAuth2UserService 는 인증된 사용자의 세부사항을 검색한 후에 데이터베이스에 Create를 하거나 동일 Email로 Update 하는 로직을 작성한다.

6. 마지막으로 oAuth2AuthenticationSuccessHandler 이 불리고 그것이 JWT authentication token을 만들고 queryString에서의 redirect_uri로 간다 (1번에서 client가 정의한 ) 이때 JWT token과 함께!

 

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    private CustomOAuth2UserService customOAuth2UserService;

    @Autowired
    private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;

    @Autowired
    private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
    
    @Autowired
    private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;

    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter();
    }

    /*
      By default, Spring OAuth2 uses HttpSessionOAuth2AuthorizationRequestRepository to save
      the authorization request. But, since our service is stateless, we can't save it in
      the session. We'll save the request in a Base64 encoded cookie instead.
    */
    @Bean
    public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() {
        return new HttpCookieOAuth2AuthorizationRequestRepository();
    }
    
    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors()
                    .and()
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                .csrf()
                    .disable()
                .formLogin()
                    .disable()
                .httpBasic()
                    .disable()
                .exceptionHandling()
                    .authenticationEntryPoint(new RestAuthenticationEntryPoint())
                    .and()
                .authorizeRequests()
                    .antMatchers("/",
                        "/error",
                        "/favicon.ico",
                        "/**/*.png",
                        "/**/*.gif",
                        "/**/*.svg",
                        "/**/*.jpg",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js")
                        .permitAll()
                    .antMatchers("/auth/**", "/oauth2/**")
                        .permitAll()
                    .anyRequest()
                        .authenticated()
                    .and()
                .oauth2Login()
                    .authorizationEndpoint()
                        .baseUri("/oauth2/authorize")
                        .authorizationRequestRepository(cookieAuthorizationRequestRepository())
                        .and()
                    .redirectionEndpoint()
                        .baseUri("/oauth2/callback/*")
                        .and()
                    .userInfoEndpoint()
                        .userService(customOAuth2UserService)
                        .and()
                    .successHandler(oAuth2AuthenticationSuccessHandler)
                    .failureHandler(oAuth2AuthenticationFailureHandler);

        // Add our custom Token based authentication filter
        http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}
  • 영펩 2021.01.05 22:58 댓글주소 수정/삭제 댓글쓰기

    안녕하세요. Spring Security을 공부하다가 여기까지 오게 됐네요! 그림 정말 설명이 잘 되어 있는거 같습니다. 헷갈리는 부분이 많았는데 수월하게 이해할 수 있었어요.
    궁금한게 있는데, CustomOauth2UserService를 따로 정의한 이유가 있을까요? OAuth2UserService 인터페이스의 주석을 인용하면, Implementations of this interface are responsible for obtaining the user attributes of the End-User (Resource Owner) from the UserInfo Endpoint 다음과 같더라구요. OAuth2UserService 인터페이스의 책임은 "인증 서버에 있는 Resource Owner의 정보를 가져오는 역할" 로 이해했는데, 책임이 명확하지 않은 것 같아 질문 드립니다!

    • 아 실제로 말씀하신대로 해당 유저 서비스에서는 UserInfo interface에 각기 githubUserInfo GoogleUserInfo로 책임을 분리했었습니다. 해당 도메인에 대한 도식보단 프로세스 위주로 큰 그림을 보다보니 그랬던것 같네요.
      또 로직적으로 해당 커스텀 서비스를 만든 이유는 위와 같은 UserInfo 객체의 내용을 가공하여 추가로 DB에 저장하는 로직을 담고싶었기 때문입니다. 기본적으로 로그인시 나오는 유저 아이디만을 사용해서 서버를 구성할수도있겠지만, 저는 제 서버에서 해당 유저 아이디를 기반으로한 커스텀 유저디비에 저장하고 싶어서 당시 저런 설계가 나왔던걸로 기억합니다.

  • 피곤해 2021.03.28 16:40 댓글주소 수정/삭제 댓글쓰기

    안녕하세요 이글을 시작으로 oauth2 구현을 할 수 있었습니다 감사합니다!
    그리고 질문이 있습니다!
    제가 지금 React 프론트엔드랑 연동을 하면서 Rest API을 만들고 있는 중인데요,
    oAuth2AuthenticationSuccessHandler에서 수행 후 localhost:3000/home으로 response sendRedirect하신 건가요?

    그러면 클라이언트에서 받는 응답값이 302로 나올 텐데, 개발자가 직접 200이나, 다른 값으로 지정해서 응답을 보내는 방법이 없을까요??

    • 안녕하세요 :) 저도 다른글을 참고해서 만들어보다가 제가 그냥 끄적인건데..ㅎㅎ 다행이네요.
      제 기억으로는 서버에서 바로 redirect를 localhost:3000/home으로 하는 로직을 구현하지 않았습니다. Oauth2 기본기능을 이용해서 위에 적힌 url처럼 loclahost:8080/oauth2/authorize/google?redirect_uri=localhost:3000/home
      이렇게 지정을 해주는 방식을 이용했습니다. parameter에 설정을 해주고, redirection에 대한 자세한 구현은 Oauth2에서 제공해주는 대로 그대로 사용한 셈이죠.

  • 전반적 흐름을 알게됐습니다!! 좋은포스팅 감사합니다.