WEB/Security

OAuth2.0 Client - oauth2Login() + API 커스텀 구현

Tony Lim 2022. 11. 23. 12:38
728x90

Authorization BaseUrl  &  Redirection BaseUrl

http oauth2Login 메소드에 인자 값으로 들어가는것은 OAuth2Configurer 이고 내부적으로 authorization , access token , userinfo , redirecturl 에 관한 endpoint를 설정할 수 있는 Configurer들도 포함되어 있다.

각각 endpoint에 알맞은 Filter의 RequestMatcher의 url 을 변경하게 되어있다. 
예를들면 authorization endpoint를 변경했을시에는 client단에서 요청도 해당 endpoint로 날려야한다. application .yaml에서는 그대로 auth-sever 의authorization endpoint를 유지해줘야한다.
예외적으로 redirect uri endpoint 를 위에 사진 처럼 설정하면 application yaml , auth-sever에서의 redirecturl을 변경해줘야한다.
client에서 auth server로 부터 요청을 날릴때 redirect uri 는 application.yml 에서 읽어오고 auth server도 redirecturi 값이 등록이 되어야 오류를 내지않는다.

loginProcessingUrl 보다 redirectionEndpoint 메소드가 더 우선순위를 가진다.
글면 왜 존재하는가? 답변기다리자


OAuth2AuthorizationRequestResolver

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

spring:
    security:
        oauth2:
            client:
                registration:
                    keycloak1:
                        clientId: oauth2-client-app
                        clientSecret: dXf021lMWuZ9kZafqxZn230MvVEdROIo
                        clientName: oauth2-client-app
                        authorizationGrantType: authorization_code
                        scope: openid,profile
                        clientAuthenticationMethod: client_secret_basic
                        redirectUri: http://localhost:8081/login/oauth2/code/keycloak
                        provider: keycloak

                    keycloakWithPKCE:
                        clientId: oauth2-client-app2
                        clientSecret: 2cejARMoAhl95E4drdGm5ROzTof0S8Zz
                        clientName: oauth2-client-app2
                        authorizationGrantType: authorization_code
                        scope: openid,profile
                        clientAuthenticationMethod: client_secret_basic
#                        clientAuthenticationMethod: none
                        redirectUri: http://localhost:8081/login/oauth2/code/keycloak
                        provider: keycloak

                    keycloak2:
                        clientId: oauth2-client-app3
                        clientSecret: tynI8eYUw4H1fJYxwLQ36XhFC1Ge1w1x
                        clientName: oauth2-client-app3
                        authorizationGrantType: implicit
                        scope: openid,profile
                        clientAuthenticationMethod: none
                        redirectUri: http://localhost:8081/home
                        provider: keycloak

                provider:
                    keycloak:
                        issuerUri: http://localhost:8080/realms/oauth2
                        authorizationUri: http://localhost:8080/realms/oauth2/protocol/openid-connect/auth
                        jwkSetUri: http://localhost:8080/realms/oauth2/protocol/openid-connect/certs
                        tokenUri: http://localhost:8080/realms/oauth2/protocol/openid-connect/token
                        userInfoUri: http://localhost:8080/realms/oauth2/protocol/openid-connect/userinfo
                        userNameAttribute: preferred_username
private OAuth2AuthorizationRequest.Builder getBuilder(ClientRegistration clientRegistration) {
   if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
      // @formatter:off
      OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest.authorizationCode()
            .attributes((attrs) ->
                  attrs.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()));
      // @formatter:on
      if (!CollectionUtils.isEmpty(clientRegistration.getScopes())
            && clientRegistration.getScopes().contains(OidcScopes.OPENID)) {
         // Section 3.1.2.1 Authentication Request -
         // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest scope
         // REQUIRED. OpenID Connect requests MUST contain the "openid" scope
         // value.
         applyNonce(builder);
      }
      if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
         DEFAULT_PKCE_APPLIER.accept(builder);
      }
      return builder;
   }
   if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
      return OAuth2AuthorizationRequest.implicit();
   }
   throw new IllegalArgumentException(
         "Invalid Authorization Grant Type (" + clientRegistration.getAuthorizationGrantType().getValue()
               + ") for Client Registration with Id: " + clientRegistration.getRegistrationId());
}

 

OAuth2AuthorizationRequest를 만들때  pkce 같은 경우 ClientAuthenticationMethod를 none으로 해줘야 cdoeVerifier 와 codeChallenge를 만들어줘서 pkce를 제대로 실행할 수 있도록하게 한다. 

pkce는 keycloak clients setting 에서 advanced에서 Proof Key for Code Exchange  Code Challenge Method를 설정해줘야한다.
하지만 none으로 해주면 client secret을 auth server에게 보내지 않으므로 401 오류가 생긴다.

그렇다면 cilentAuthenticationMethod 설정을 client_secret_basic 으로 하고도 pkce를 적용하는 방법은 OAuth2AuthorizationRequestResolver를 우리가 만들면 되는것이다.

public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {

    private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";
    private static final Consumer<OAuth2AuthorizationRequest.Builder> DEFAULT_PKCE_APPLIER = OAuth2AuthorizationRequestCustomizers
            .withPkce();
    private ClientRegistrationRepository clientRegistrationRepository;
    DefaultOAuth2AuthorizationRequestResolver defaultResolver;

    private final AntPathRequestMatcher authorizationRequestMatcher;


    public CustomOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository, String authorizationRequestBaseUri) {

        this.clientRegistrationRepository = clientRegistrationRepository;
        this.authorizationRequestMatcher = new AntPathRequestMatcher(
                authorizationRequestBaseUri + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}");

        defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, authorizationRequestBaseUri);
    }
@EnableWebSecurity
public class OAuth2ClientConfig {

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @Bean
    SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests((requests) -> requests.antMatchers("/home").permitAll()
                .anyRequest().authenticated());
        http.oauth2Login(authLogin ->
                authLogin.authorizationEndpoint(authEndpoint ->
                        authEndpoint.authorizationRequestResolver(customOAuth2AuthenticationRequestResolver())));
        http.logout().logoutSuccessUrl("/home");
        return http.build();
   }

    private OAuth2AuthorizationRequestResolver customOAuth2AuthenticationRequestResolver() {
        return new CustomOAuth2AuthorizationRequestResolver(clientRegistrationRepository, "/oauth2/authorization");
    }
}

CustomAuthorizationRequestResolver를 만든후에 HttpSecurity 설정에서 등록을 해줘야한다.

다음으로는 만든 구현체의 메소드를 살펴보자

public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
    String registrationId = resolveRegistrationId(request);
    if (registrationId == null) {
        return null;
    }
    ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(registrationId);
    if(registrationId.equals("keycloakWithPKCE")){
        OAuth2AuthorizationRequest oAuth2AuthorizationRequest = defaultResolver.resolve(request);
        return customResolve(oAuth2AuthorizationRequest, clientRegistration);

    }
    return defaultResolver.resolve(request);
}

@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
    ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(clientRegistrationId);
    if(clientRegistrationId.equals("keycloakWithPKCE")){
        OAuth2AuthorizationRequest oAuth2AuthorizationRequest = defaultResolver.resolve(request);
        return customResolve(oAuth2AuthorizationRequest, clientRegistration);
    }
    return defaultResolver.resolve(request,clientRegistrationId);
}

registration id 가 keycloakWithPKCE인 경우에만 custom 하게 만든 resolver가 처리가하게 한다. 나머진 defaultResolver.resolve를 통해 처리하게 된다.

keycloakWithPKCE인 경우 우선 기본적으로 defaultResolver.resolve를 통해 기본적인 설정들을 저장하고 추가적인 작업인 code verifer등을 설정하게 된다.

private OAuth2AuthorizationRequest customResolve(OAuth2AuthorizationRequest authorizationRequest, ClientRegistration clientRegistration) {

    Map<String,Object> extraParam = new HashMap<>();
    extraParam.put("customName1","customValue1");
    extraParam.put("customName2","customValue2");
    extraParam.put("customName3","customValue3");

    OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest
            .from(authorizationRequest)
            .additionalParameters(extraParam)
            ;
    DEFAULT_PKCE_APPLIER.accept(builder);

    return builder.build();
}

DEFAULT_PKCE_APPLIER.accept(builder) 에서 code verifier 설정을 추가하게 된다.
추가로 extraParam 을 추가하면 request가 날라갈때 query param에 추가되어 날아가게 된다.

 

auth server 설정에서 client authentication을 꺼버리면 client secret 을 통해 client 를검증하지 않는것이니 clientAuthorizationMethod: none 으로 해여도 PKCE만 동작하게 할 수 있다.

728x90