스프링 시큐리티 OAuth2/OAuth2LoginConfigurer 초기화 이해

API 커스텀 구현 - OAuth2AuthorizationRequestResolver

webmaster 2023. 1. 24. 15:02
728x90

OAuth2AuthorizationRequestResolver

OAuth2AuthorizationRequestResolver

  • Authorization Code Grant 방식에서 클라이언트가 인가서버로 권한부여 요청할 때 실행되는 클래스
  • OAuth2AuthorizationRequestResolver 는 OAuth 2.0 인가 프레임워크에 정의된 표준 파라미터 외에 다른 파라미터를 추가하는 식으로 인가 요청을 할 때 사용한다
  • DefaultOAuth2AuthorizationRequestResolver 가 디폴트 구현체로 제공 되며Consumer<OAuth2AuthorizationRequest.Builder> 속성에 커스텀할 내용을 구현한다

application.yml 설정

application.yml

TEST

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

application.yml

server:
  port: 8081


spring:
  security:
    oauth2:
      client:
        registration:
          keycloak1:
            client-id: oauth2-client-app
            client-name: oauth2-client-app
            client-secret: xSlqD456gfAeLZO93BLbTwQys0NEc8KL
            authorization-grant-type: authorization_code #authorization_code, authorization_code with pkce, implicit
            scope:
              - profile
              - openid
            client-authentication-method: client_secret_basic
            redirect-uri: http://localhost:8081/login/oauth2/code/keycloak
            provider: keycloak

          keycloakWithPKCE:
            client-id: oauth2-client-app2
            client-name: oauth2-client-app2
            client-secret: g6jIAAN2lmfg3hgovuydzH3BO61ssAAU
            authorization-grant-type: authorization_code
            scope:
              - profile
              - openid
            client-authentication-method: none
            redirect-uri: http://localhost:8081/login/oauth2/code/keycloak
            provider: keycloak

          keycloak2:
            client-id: oauth2-client-app3
            client-name: oauth2-client-app3
            authorization-grant-type: implicit
            scope:
              - profile
              - openid
            client-authentication-method: none
            redirect-uri: http://localhost:8081/home
            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

home.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/keycloak1');
    }

    function authorizationCodeWithPKCE(){
      window.location = new URL('http://localhost:8081/oauth2/authorization/keycloakWithPKCE');
    }

    function implicit(){
      window.location = new URL('http://localhost:8081/oauth2/authorization/keycloak2');
    }

  </script>
</head>
<body>
<div>Welcome</div>
<div sec:authorize="isAuthenticated()"><a th:href="@{/logout}">Logout</a></div>
<form sec:authorize="isAnonymous()" action="#">
  <p><input type="button" onclick="authorizationCode()" value="AuthorizationCode Grant" />
  <p><input type="button" onclick="authorizationCodeWithPKCE()" value="AuthorizationCode Grant with PKCE" />
  <p><input type="button" onclick="implicit()" value="Implicit Grant" />
</form>
</body>
</html>

keycloak 생성

클라이언트 생성

oauth2-client-app2

oauth2-client-app3

keycloak PKCE 수정

해당 알고리즘을 설정해야한다.

Config

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  http.authorizeHttpRequests(authRequest -> authRequest.antMatchers("/home").permitAll()
      .anyRequest().authenticated());
  http.oauth2Login(Customizer.withDefaults());
  http.logout().logoutSuccessUrl("/home");
  return http.build();
}

HomeController

@Controller
public class HomeController {
  @GetMapping("/home")
  public String home() {
    return "home";
  }
}
  • build.gradle에 thymeleaf의 시큐리티 설정을 해야 HTML 클래스를 적용시켜 logout 버튼이 보이지 않는다.
  • 3개의 클라이언트를 만들어, 1번은 이전 상태 그대로, 2번은 PKCE 방식, 3번은 Implicit 방식으로 한다.
    • oauth2-client-app2 같은 경우, PKCE 방식이기 때문에 인증서버에서 PKCE설정을 추가적으로 해주어야 한다.
  • client-authentication-method가 none이 아니면, PKCE가 동작하지 않는다(DefaultOAuth2AuthorizationRequestResolver 에서 None일 경우 PKCE로 전달하기 때문)
    • 단, None으로 설정을 하여도 동작하지 않게 되는데 왜 그러냐면 2번째 filter에서 PKCE가 적용되었지만, secret 키가 포함이 되지 않기 때문에 오류가 발생한다.
    • 따라서 PKCE를 설정해 주는 메서드를 수정을 해줘야 한다 -> DefaultOAuth2AuthorizationRequestResolver.getBuilder() 메서드를 커스텀하게 바꿔주면 된다.

TEST(PKCE  이슈 해결)

CustomOAuth2AuthorizationRequestResolver

public class CustomOAuth2AuthorizationRequestResolver implements
    OAuth2AuthorizationRequestResolver {
  private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";

  private ClientRegistrationRepository clientRegistrationRepository;
  private String baseUri;

  private DefaultOAuth2AuthorizationRequestResolver defaultOAuth2AuthorizationRequestResolver;

  private final AntPathRequestMatcher authorizationRequestMatcher;

  private static final Consumer<Builder> DEFAULT_PKCE_APPLIER = OAuth2AuthorizationRequestCustomizers
      .withPkce();

  public CustomOAuth2AuthorizationRequestResolver(
      ClientRegistrationRepository clientRegistrationRepository, String BaseUri) {
    this.clientRegistrationRepository = clientRegistrationRepository;
    this.authorizationRequestMatcher = new AntPathRequestMatcher(
        BaseUri + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}");
    this.baseUri = BaseUri;
    defaultOAuth2AuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(
        clientRegistrationRepository, BaseUri);
  }

  //PKCE를 제외한 값은 기존방식을 이용할 것이다.
  @Override
  public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
    String registrationId = resolveRegistrationId(request);
    if (registrationId == null) {
      return null;
    }
    if( registrationId.equals("keycloakWithPKCE")){
      OAuth2AuthorizationRequest oAuth2AuthorizationRequest = defaultOAuth2AuthorizationRequestResolver.resolve(
          request);
      return customResolver(oAuth2AuthorizationRequest, registrationId);
    }
    return defaultOAuth2AuthorizationRequestResolver.resolve(request);
  }

  @Override
  public OAuth2AuthorizationRequest resolve(HttpServletRequest request,
      String clientRegistrationId) {
    String registrationId = resolveRegistrationId(request);
    if (registrationId == null) {
      return null;
    }
    if( registrationId.equals("keycloakWithPKCE")){
      OAuth2AuthorizationRequest oAuth2AuthorizationRequest = defaultOAuth2AuthorizationRequestResolver.resolve(
          request);
      return customResolver(oAuth2AuthorizationRequest, clientRegistrationId);
    }
    return defaultOAuth2AuthorizationRequestResolver.resolve(request);
  }

  private OAuth2AuthorizationRequest customResolver(OAuth2AuthorizationRequest oAuth2AuthorizationRequest, String clientRegistrationId) {
    Map<String, Object> extraParam = new HashMap<>();
    extraParam.put("customName1", "customValue1");
    extraParam.put("customName2", "customValue2");
    extraParam.put("customName3", "customValue3");

    OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest
        .from(oAuth2AuthorizationRequest)
        .additionalParameters(extraParam);
    DEFAULT_PKCE_APPLIER.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;
  }
}

client authentication off

  • 해당 코드는 DefaultOAuth2AuthorizationRequestResolver를 참고해서 작성했으며, DefaultOAuth2AuthorizationRequestResolver을 builder() 한 이후의 값을 한번 더 builder 하여 파라미터를 추가할 수 있다.
  • client authentication off 하여 해결할 수도 있지만, 이는 보안에 취약하기 때문에 위와 같이 Resolver를 만들어 PKCE에서 파라미터를 추가로 전달하여 인증처리를 해야 한다.
  • 지금의 SpringSecurity는 none일 때, PKCE로 파악하여, 인증서버로 요청하게 되는데 이때, 인증서버의 secret 값을 가지고 가지 않기 때문에 오류가 발생하는 것으로, 이 부분을 파라미터를 추가하는 방식으로 해결할 수 있을 것이다.
  • 현재는 파라미터를 추가할 수 있다는 것만 설명하였다.
728x90