스프링 시큐리티 OAuth2/OAuth 2.0 Client - oauth2Client()

DefaultOAuth2AuthorizedClientManager - Resource Owner Password 권한 부여 구현하기

webmaster 2023. 2. 20. 18:17
728x90

Resource Owner Password

흐름

  • OAuth2AuthorizeRequest(요청 객체)를 OAuth2AuthorizedClientManager에 전달한다.
  • 만약 OAuth2AuthorizedClient 객체가 null이 아닐 경우 인증된 사용자로 보고 바로 return 을 한다.
  • 만약 OAuth2AuthorizedClient 객체가 null일 경우 OAuth2AuthorizationContext에 저장하여, PasswordOAuth2AuthorizedClientProvider에게 클라이언트 인가 요청을 한다.
    • OAuth2AuthorizedClient가 존재하고, AccessToken이 만료되지 않았다면 권한 부여는 다시 하지 않는다.
    • OAuth2AuthorizedClient가 존재하고, AccessToken이 만료되고, RefreshToken이 존재하면 null을 반환하고 RefreshTokenOAuth2AuthorizedClientProvider에게 처리하도록 한다.
  • DefaultPasswordTokenResponseClient가 OAuth2PasswordGrantRequestEntityConverter가 인증 서버와 RestTemplate으로 통신을 해 OAuth2AccessTokenResponse에 응답을 받아온다.
    • 만약 예외 발생시 OAuth2AuthorizationFailureHandler에서 예외를 처리하면 된다.
    • 성공하게 된다면, OAuth2AccessTokenResponse에 응답으로 받아온 값을 OAuth2AuthorizedClient에 저장 후, OAuth2AuthorizationSuccessHandler에서 성공 처리를 진행하면 된다.

Test

application.yml

server:
  port: 8081
spring:
  security:
    oauth2:
      client:
        registration:
          keycloak:
            client-id: oauth2-client-app
            client-name: oauth2-client-app
            client-secret: xSlqD456gfAeLZO93BLbTwQys0NEc8KL
            authorization-grant-type: password
            scope:
              - profile
              - openid
            client-authentication-method: client_secret_basic
            provider: keycloak
        provider:
          keycloak:
            authorization-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/auth
            issuer-uri: http://localhost:8080/realms/oauth2
            jwk-set-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/certs
            token-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/token
            user-info-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/userinfo
            user-name-attribute: preferred_username

Index.html

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

<head>
  <meta charset="UTF-8">
  <title>Insert title here</title>
  <script>
    function authorizationCode(){
      window.location = new URL('http://localhost:8081/oauth2/authorization/keycloak');
    }

  </script>
</head>
<body>
<div>Welcome</div>
<form sec:authorize="isAnonymous()" action="#">
  <p><input type="button" onclick="authorizationCode()" value="AuthorizationCode Grant" /></p>
  <p><div sec:authorize="isAnonymous()"><a th:href="@{/oauth2Login(username='user',password='1234')}">Password Flow Login</a></div></p>
</form>
</body>
</html>

AppConfig

@Configuration
public class AppConfig {

  @Bean
  public DefaultOAuth2AuthorizedClientManager auth2AuthorizedClientManager(
      ClientRegistrationRepository clientRegistrationRepository,
      OAuth2AuthorizedClientRepository clientRepository) {

    OAuth2AuthorizedClientProvider auth2AuthorizedClientProvider =
        OAuth2AuthorizedClientProviderBuilder.builder()
            .authorizationCode()
            .clientCredentials()
            .password()
            .refreshToken()
            .build();

    DefaultOAuth2AuthorizedClientManager oAuth2AuthorizedClientManager =
        new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, clientRepository);

    oAuth2AuthorizedClientManager.setAuthorizedClientProvider(auth2AuthorizedClientProvider);
    oAuth2AuthorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
    return oAuth2AuthorizedClientManager;
  }

  private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
    return oAuth2AuthorizeRequest -> {
      Map<String, Object> contextAttributes = new HashMap<>();
      HttpServletRequest request = oAuth2AuthorizeRequest.getAttribute(HttpServletRequest.class.getName());
      String username = request.getParameter(OAuth2ParameterNames.USERNAME);
      String password = request.getParameter(OAuth2ParameterNames.PASSWORD);
      if(StringUtils.hasText(username) && StringUtils.hasText(password)){
        contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
        contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
      }
      return contextAttributes;
    };
  }
}

LoginController

@Controller
public class LoginController {

  @Autowired
  private DefaultOAuth2AuthorizedClientManager oAuth2AuthorizedClientManager;

  @Autowired
  private OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository;

  @GetMapping("/oauth2Login")
  public String oauth2Login(Model model, HttpServletRequest request, HttpServletResponse response) {

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    OAuth2AuthorizeRequest auth2AuthorizeRequest =
        OAuth2AuthorizeRequest.withClientRegistrationId("keycloak")
            .principal(authentication)
            .attribute(HttpServletRequest.class.getName(), request)
            .attribute(HttpServletResponse.class.getName(), response)
            .build();

    OAuth2AuthorizationSuccessHandler successHandler = (authorizedClient, principal, attributes) -> {
      oAuth2AuthorizedClientRepository
          .saveAuthorizedClient(authorizedClient, principal,
              (HttpServletRequest) attributes.get(HttpServletRequest.class.getName()),
              (HttpServletResponse) attributes.get(HttpServletResponse.class.getName()));
      System.out.println("authorizedClient = " + authorizedClient);
      System.out.println("principal = " + principal);
      System.out.println("attributes = " + attributes);
    };
    oAuth2AuthorizedClientManager.setAuthorizationSuccessHandler(successHandler);


    OAuth2AuthorizedClient authorizedClient = oAuth2AuthorizedClientManager.authorize(
        auth2AuthorizeRequest);

    if (authorizedClient != null) {
      //인증이 성공했을때
      OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
      ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
      OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
      OAuth2UserRequest oAuth2UserRequest = new OAuth2UserRequest(clientRegistration, accessToken);
      OAuth2User oAuth2User = oAuth2UserService.loadUser(oAuth2UserRequest);

      SimpleAuthorityMapper authorityMapper = new SimpleAuthorityMapper(); //Scope를 통해 커스텀하게 권한 조율 가능
      authorityMapper.setPrefix("SYSTEM_"); //SYSTEM_SCOPE_XXX
      Set<GrantedAuthority> grantedAuthorities = authorityMapper.mapAuthorities(
          oAuth2User.getAuthorities());
      OAuth2AuthenticationToken oAuth2AuthenticationToken = new OAuth2AuthenticationToken(
          oAuth2User, grantedAuthorities, clientRegistration.getRegistrationId());

      SecurityContextHolder.getContext().setAuthentication(oAuth2AuthenticationToken);

      model.addAttribute("oAuth2AuthenticationToken", oAuth2AuthenticationToken);
    }
    return "home";
  }

  @GetMapping("/logout")
  public String logout(Authentication authentication, HttpServletRequest request,
      HttpServletResponse response) {
    SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
    logoutHandler.logout(request, response, authentication);
    return "redirect:/";
  }
}

UserInfo 요청

  • OAuth2AuthorizedClient에서 OAuth2AccessToken 값을 가지고, OAuth2UserService를 통해 "/userinfo" 로 요청을 해 인증 서버로 부터 User 정보를 가지고 온다.
  • 얻어온 정보를 통해 OAuth2AuthenticationToken을 만들어 SecurityContext에 담고(스프링 시큐리티가 인증이 되었다고 판단), OAuth2AuthorizationSuccessHandler에서 후속처리를 하면된다.
  • OAuth2AuthorizedClientRepository, OAuth2AuthorizedClientService에서 OAuth2AuthorizedClient 정보를 저장하여 어디서나 해당 값을 꺼낼수 있도록 한다.
728x90