diff --git a/README.MD b/README.MD index 73f9514ea..25cd7d94f 100644 --- a/README.MD +++ b/README.MD @@ -482,7 +482,7 @@ These starters are designed to push auto-configuration one step further. In most I could forget to update README before releasing, so please refer to [maven central](https://repo1.maven.org/maven2/com/c4-soft/springaddons/spring-addons/) to pick latest available release ```xml - 7.3.3 + 7.3.4 @@ -518,9 +518,12 @@ I could forget to update README before releasing, so please refer to [maven cent ### 5.1. `7.x` Branch +### `7.3.4` +- [gh-178](https://github.com/ch4mpy/spring-addons/issues/178) `authorization-request-params` ignored + ### `7.3.3` -- [gh-176](https://github.com/ch4mpy/spring-addons/issues/176) Exception thrown when post-logout-redirect-path is null -- [gh-177](https://github.com/ch4mpy/spring-addons/issues/177) post-login-success-uri params and headers on authentication request are ignored +- [gh-176](https://github.com/ch4mpy/spring-addons/issues/176) Exception thrown when `post-logout-redirect-path` is null +- [gh-177](https://github.com/ch4mpy/spring-addons/issues/177) Post-login success & failure URI params and headers on authentication request are ignored in reactive applications ### `7.3.2` - [gh-174](https://github.com/ch4mpy/spring-addons/issues/174) Fix a regression on request to exchange authorization-code for tokens in servlet applications diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcClientProperties.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcClientProperties.java index 785303d0f..a3059813a 100644 --- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcClientProperties.java +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/properties/SpringAddonsOidcClientProperties.java @@ -12,224 +12,226 @@ import lombok.Data; /** - * Auto-configuration for an OAuth2 client (secured with session, not access token) Security(Web)FilterChain with @Order(Ordered.LOWEST_PRECEDENCE - 1). - * Typical use-cases are spring-cloud-gateway used as BFF and applications with Thymeleaf or another server-side rendering framework. Default configuration - * includes: enabled sessions, CSRF protection, "oauth2Login", "logout". securityMatchers must be set for this filter-chain @Bean and its dependencies to be - * defined. Properties defined here are a complement for spring.security.oauth2.client.* (which are required when enabling spring-addons client - * filter-chain). + * Auto-configuration for an OAuth2 client (secured with session, not access token) Security(Web)FilterChain with + * @Order(Ordered.LOWEST_PRECEDENCE - 1). Typical use-cases are spring-cloud-gateway used as BFF and applications with Thymeleaf or + * another server-side rendering framework. Default configuration includes: enabled sessions, CSRF protection, "oauth2Login", "logout". + * securityMatchers must be set for this filter-chain @Bean and its dependencies to be defined. Properties defined here are a + * complement for spring.security.oauth2.client.* (which are required when enabling spring-addons client filter-chain). * * @author Jerome Wacongne ch4mp@c4-soft.com */ @Data @ConfigurationProperties("com.c4-soft.springaddons.oidc.client") public class SpringAddonsOidcClientProperties { - public static final String POST_AUTHENTICATION_SUCCESS_URI_HEADER = "X-POST-LOGIN-SUCCESS-URI"; - public static final String POST_AUTHENTICATION_SUCCESS_URI_PARAM = "post_login_success_uri"; - public static final String POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE = POST_AUTHENTICATION_SUCCESS_URI_PARAM; - - public static final String POST_AUTHENTICATION_FAILURE_URI_HEADER = "X-POST-LOGIN-FAILURE-URI"; - public static final String POST_AUTHENTICATION_FAILURE_URI_PARAM = "post_login_failure_uri"; - public static final String POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE = POST_AUTHENTICATION_FAILURE_URI_PARAM; - - public static final String POST_LOGOUT_SUCCESS_URI_HEADER = "X-POST-LOGOUT-SUCCESS-URI"; - public static final String POST_LOGOUT_SUCCESS_URI_PARAM = "post_logout_success_uri"; - - /** - * Path matchers for the routes secured with the auto-configured client filter-chain. If left empty, OAuth2 client auto-configuration is disabled. It should - * include "/login/**" and "/oauth2/**" for login process. Can be set to "/**" to intercept all requests (OAuth2 client only application, no REST API - * secured with access tokens). - */ - private String[] securityMatchers = {}; - - /** - * Fully qualified URI of the configured OAuth2 client. - */ - private URI clientUri = URI.create("/"); - - /** - * Path to the login page. Provide one only in the following cases: - * - * If left empty, the default Spring Boot configuration for OAuth2 login is applied - */ - private Optional loginPath = Optional.empty(); - - /** - * URI containing scheme, host and port where the user should be redirected after a successful login (defaults to the client URI) - */ - private Optional postLoginRedirectHost = Optional.empty(); - - /** - * Where to redirect the user after successful login - */ - private Optional postLoginRedirectPath = Optional.empty(); - - /** - * Where to redirect the user after login failure - */ - private Optional loginErrorRedirectPath = Optional.empty(); - - /** - * HTTP status for redirections in OAuth2 login and logout. You might set this to something in 2xx range (like OK, ACCEPTED, NO_CONTENT, ...) for single - * page and mobile applications to handle this redirection as it wishes (change the user-agent, clear some headers, ...). - */ - private OAuth2RedirectionProperties oauth2Redirections = new OAuth2RedirectionProperties(); - - public URI getPostLoginRedirectHost() { - return postLoginRedirectHost.orElse(clientUri); - } - - public Optional getPostLoginRedirectUri() { - if (postLoginRedirectHost.isEmpty() && postLoginRedirectPath.isEmpty()) { - return Optional.empty(); - } - final var uri = UriComponentsBuilder.fromUri(getPostLoginRedirectHost()); - postLoginRedirectPath.ifPresent(uri::path); - - return Optional.of(uri.build(Map.of())); - } - - /** - * URI containing scheme, host and port where the user should be redirected after a successful logout (defaults to the client URI) - */ - private Optional postLogoutRedirectHost = Optional.empty(); - - /** - * Path (relative to clientUri) where the user should be redirected after being logged out from authorization server(s) - */ - private Optional postLogoutRedirectPath = Optional.empty(); - - public URI getPostLogoutRedirectHost() { - return postLogoutRedirectHost.orElse(clientUri); - } - - public URI getPostLogoutRedirectUri() { - final var uri = UriComponentsBuilder.fromUri(getPostLogoutRedirectHost()); - postLogoutRedirectPath.ifPresent(uri::path); - - return uri.build(Map.of()); - } - - /** - * Map of logout properties indexed by client registration ID (must match a registration in Spring Boot OAuth2 client configuration). - * {@link OAuth2LogoutProperties} are configuration for authorization server not strictly following the - * RP-Initiated Logout standard, but exposing a logout end-point expecting an - * authorized GET request with following request params: - * - */ - private Map oauth2Logout = new HashMap<>(); - - /** - *

- * If true, AOP is used to instrument authorized client repository and keep the principalName current user has for each issuer he authenticates on. - *

- *

- * This is useful only if you allow a user to authenticate on more than one OpenID Provider at a time. For instance, user logs in on Google and on an - * authorization server of your own and your client sends direct queries to Google APIs (with an access token issued by Google) and resource servers of your - * own (with an access token from your authorization server). - *

- */ - private boolean multiTenancyEnabled = false; - - /** - * Whether to enable a security filter-chain and a controller (intercepting POST requests to "/backchannel_logout") to implement the client side of a - * Back-Channel Logout - */ - // private boolean backChannelLogoutEnabled = false; - - /** - * Path matchers for the routes accessible to anonymous requests - */ - private String[] permitAll = { "/login/**", "/oauth2/**" }; - - /** - * CSRF protection configuration for the auto-configured client filter-chain - */ - private Csrf csrf = Csrf.DEFAULT; - - /** - * Fine grained CORS configuration - */ - private CorsProperties[] cors = {}; - - /** - * Additional parameters to send with authorization-code request, mapped by client registration IDs - */ - private Map authorizationRequestParams = new HashMap<>(); - - /** - * Logout properties for OpenID Providers which do not implement the RP-Initiated Logout spec - * - * @author Jerome Wacongne ch4mp@c4-soft.com - */ - @Data - public static class OAuth2LogoutProperties { - - /** - * URI on the authorization server where to redirect the user for logout - */ - private URI uri; - - /** - * request param name for client-id - */ - private Optional clientIdRequestParam = Optional.empty(); - - /** - * request param name for post-logout redirect URI (where the user should be redirected after his session is closed on the authorization server) - */ - private Optional postLogoutUriRequestParam = Optional.empty(); - - /** - * request param name for setting an ID-Token hint - */ - private Optional idTokenHintRequestParam = Optional.empty(); - } - - /** - * Request parameter - * - * @author Jerome Wacongne ch4mp@c4-soft.com - */ - @Data - public static class RequestParam { - /** - * request parameter name - */ - private String name; - - /** - * request parameter value - */ - private String value; - } - - @Data - @ConfigurationProperties("com.c4-soft.springaddons.oidc.client.oauth2-redirections") - public static class OAuth2RedirectionProperties { - - /** - * Status for the 1st response in authorization code flow, with location to get authorization code from authorization server - */ - private HttpStatus preAuthorizationCode = HttpStatus.FOUND; - - /** - * Status for the response after authorization code, with location to the UI - */ - private HttpStatus postAuthorizationCode = HttpStatus.FOUND; - - /** - * Status for the response after BFF logout, with location to authorization server logout endpoint - */ - private HttpStatus rpInitiatedLogout = HttpStatus.FOUND; - } - - public Optional getLogoutProperties(String clientRegistrationId) { - return Optional.ofNullable(oauth2Logout.get(clientRegistrationId)); - } + public static final String POST_AUTHENTICATION_SUCCESS_URI_HEADER = "X-POST-LOGIN-SUCCESS-URI"; + public static final String POST_AUTHENTICATION_SUCCESS_URI_PARAM = "post_login_success_uri"; + public static final String POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE = POST_AUTHENTICATION_SUCCESS_URI_PARAM; + + public static final String POST_AUTHENTICATION_FAILURE_URI_HEADER = "X-POST-LOGIN-FAILURE-URI"; + public static final String POST_AUTHENTICATION_FAILURE_URI_PARAM = "post_login_failure_uri"; + public static final String POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE = POST_AUTHENTICATION_FAILURE_URI_PARAM; + + public static final String POST_LOGOUT_SUCCESS_URI_HEADER = "X-POST-LOGOUT-SUCCESS-URI"; + public static final String POST_LOGOUT_SUCCESS_URI_PARAM = "post_logout_success_uri"; + + /** + * Path matchers for the routes secured with the auto-configured client filter-chain. If left empty, OAuth2 client auto-configuration is + * disabled. It should include "/login/**" and "/oauth2/**" for login process. Can be set to "/**" to intercept all requests (OAuth2 client + * only application, no REST API secured with access tokens). + */ + private String[] securityMatchers = {}; + + /** + * Fully qualified URI of the configured OAuth2 client. + */ + private URI clientUri = URI.create("/"); + + /** + * Path to the login page. Provide one only in the following cases: + *
    + *
  • you want to provide your own login @Controller
  • + *
  • you want to use port 80 or 8080 with SSL enabled (this will require you to provide with the login @Controller above)
  • + *
+ * If left empty, the default Spring Boot configuration for OAuth2 login is applied + */ + private Optional loginPath = Optional.empty(); + + /** + * URI containing scheme, host and port where the user should be redirected after a successful login (defaults to the client URI) + */ + private Optional postLoginRedirectHost = Optional.empty(); + + /** + * Where to redirect the user after successful login + */ + private Optional postLoginRedirectPath = Optional.empty(); + + /** + * Where to redirect the user after login failure + */ + private Optional loginErrorRedirectPath = Optional.empty(); + + /** + * HTTP status for redirections in OAuth2 login and logout. You might set this to something in 2xx range (like OK, ACCEPTED, NO_CONTENT, + * ...) for single page and mobile applications to handle this redirection as it wishes (change the user-agent, clear some headers, ...). + */ + private OAuth2RedirectionProperties oauth2Redirections = new OAuth2RedirectionProperties(); + + public URI getPostLoginRedirectHost() { + return postLoginRedirectHost.orElse(clientUri); + } + + public Optional getPostLoginRedirectUri() { + if (postLoginRedirectHost.isEmpty() && postLoginRedirectPath.isEmpty()) { + return Optional.empty(); + } + final var uri = UriComponentsBuilder.fromUri(getPostLoginRedirectHost()); + postLoginRedirectPath.ifPresent(uri::path); + + return Optional.of(uri.build(Map.of())); + } + + /** + * URI containing scheme, host and port where the user should be redirected after a successful logout (defaults to the client URI) + */ + private Optional postLogoutRedirectHost = Optional.empty(); + + /** + * Path (relative to clientUri) where the user should be redirected after being logged out from authorization server(s) + */ + private Optional postLogoutRedirectPath = Optional.empty(); + + public URI getPostLogoutRedirectHost() { + return postLogoutRedirectHost.orElse(clientUri); + } + + public URI getPostLogoutRedirectUri() { + final var uri = UriComponentsBuilder.fromUri(getPostLogoutRedirectHost()); + postLogoutRedirectPath.ifPresent(uri::path); + + return uri.build(Map.of()); + } + + /** + * Map of logout properties indexed by client registration ID (must match a registration in Spring Boot OAuth2 client configuration). + * {@link OAuth2LogoutProperties} are configuration for authorization server not strictly following the + * RP-Initiated Logout standard, but exposing a logout end-point + * expecting an authorized GET request with following request params: + *
    + *
  • "client-id" (required)
  • + *
  • post-logout redirect URI (optional)
  • + *
+ */ + private Map oauth2Logout = new HashMap<>(); + + /** + *

+ * If true, AOP is used to instrument authorized client repository and keep the principalName current user has for each issuer he + * authenticates on. + *

+ *

+ * This is useful only if you allow a user to authenticate on more than one OpenID Provider at a time. For instance, user logs in on Google + * and on an authorization server of your own and your client sends direct queries to Google APIs (with an access token issued by Google) + * and resource servers of your own (with an access token from your authorization server). + *

+ */ + private boolean multiTenancyEnabled = false; + + /** + * Whether to enable a security filter-chain and a controller (intercepting POST requests to "/backchannel_logout") to implement the client + * side of a Back-Channel Logout + */ + // private boolean backChannelLogoutEnabled = false; + + /** + * Path matchers for the routes accessible to anonymous requests + */ + private String[] permitAll = { "/login/**", "/oauth2/**" }; + + /** + * CSRF protection configuration for the auto-configured client filter-chain + */ + private Csrf csrf = Csrf.DEFAULT; + + /** + * Fine grained CORS configuration + */ + private CorsProperties[] cors = {}; + + /** + * Additional parameters to send with authorization request, mapped by client registration IDs + */ + private Map authorizationRequestParams = new HashMap<>(); + + /** + * Logout properties for OpenID Providers which do not implement the RP-Initiated Logout spec + * + * @author Jerome Wacongne ch4mp@c4-soft.com + */ + @Data + public static class OAuth2LogoutProperties { + + /** + * URI on the authorization server where to redirect the user for logout + */ + private URI uri; + + /** + * request param name for client-id + */ + private Optional clientIdRequestParam = Optional.empty(); + + /** + * request param name for post-logout redirect URI (where the user should be redirected after his session is closed on the authorization + * server) + */ + private Optional postLogoutUriRequestParam = Optional.empty(); + + /** + * request param name for setting an ID-Token hint + */ + private Optional idTokenHintRequestParam = Optional.empty(); + } + + /** + * Request parameter + * + * @author Jerome Wacongne ch4mp@c4-soft.com + */ + @Data + public static class RequestParam { + /** + * request parameter name + */ + private String name; + + /** + * request parameter value + */ + private String value; + } + + @Data + @ConfigurationProperties("com.c4-soft.springaddons.oidc.client.oauth2-redirections") + public static class OAuth2RedirectionProperties { + + /** + * Status for the 1st response in authorization code flow, with location to get authorization code from authorization server + */ + private HttpStatus preAuthorizationCode = HttpStatus.FOUND; + + /** + * Status for the response after authorization code, with location to the UI + */ + private HttpStatus postAuthorizationCode = HttpStatus.FOUND; + + /** + * Status for the response after BFF logout, with location to authorization server logout endpoint + */ + private HttpStatus rpInitiatedLogout = HttpStatus.FOUND; + } + + public Optional getLogoutProperties(String clientRegistrationId) { + return Optional.ofNullable(oauth2Logout.get(clientRegistrationId)); + } } diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsServerOAuth2AuthorizationRequestResolver.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsServerOAuth2AuthorizationRequestResolver.java index 644f5b6f0..95342019a 100644 --- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsServerOAuth2AuthorizationRequestResolver.java +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/reactive/client/SpringAddonsServerOAuth2AuthorizationRequestResolver.java @@ -133,11 +133,10 @@ static String resolveRegistrationId(String requestPath) { } private static Consumer requestParamAuthorizationRequestCustomizer(RequestParam[] additionalParams) { - return customizer -> customizer.authorizationRequestUri(authorizationRequestUri -> { + return customizer -> customizer.additionalParameters(params -> { for (var reqParam : additionalParams) { - authorizationRequestUri.queryParam(reqParam.getName(), reqParam.getValue()); + params.put(reqParam.getName(), reqParam.getValue()); } - return authorizationRequestUri.build(); }); } diff --git a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOAuth2AuthorizationRequestResolver.java b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOAuth2AuthorizationRequestResolver.java index dd4451df2..bbecf4771 100644 --- a/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOAuth2AuthorizationRequestResolver.java +++ b/spring-addons-starter-oidc/src/main/java/com/c4_soft/springaddons/security/oidc/starter/synchronised/client/SpringAddonsOAuth2AuthorizationRequestResolver.java @@ -25,133 +25,119 @@ /** * Support three features: *
    - *
  • Use the {@link SpringAddonsOidcClientProperties#clientUri SpringAddonsOidcClientProperties#client-uri} to set the base URI of authorization-code callback - * (of interest for instance when using an ingress or another gateway in front of the OAuth2 client with oauth2Login)
  • + *
  • Use the {@link SpringAddonsOidcClientProperties#clientUri SpringAddonsOidcClientProperties#client-uri} to set the base URI of + * authorization-code callback (of interest for instance when using an ingress or another gateway in front of the OAuth2 client with + * oauth2Login)
  • *
  • Defining authorization request additional parameters from properties (like audience for Auth0)
  • - *
  • Save in session post-login URIs provided as header ({@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_HEADER} and - * {@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_FAILURE_URI_HEADER}) or request param + *
  • Save in session post-login URIs provided as header ({@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_HEADER} + * and {@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_FAILURE_URI_HEADER}) or request param * ({@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_PARAM} and - * {@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_FAILURE_URI_PARAM}). If both are provided, header wins. The key used in session are - * {@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE} and + * {@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_FAILURE_URI_PARAM}). If both are provided, header wins. The key used in + * session are {@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE} and * {@link SpringAddonsOidcClientProperties#POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE}
  • *
* The post-login URIs are used by the default {@link AuthenticationSuccessHandler} and {@link AuthenticationFailureHandler} * * @author Jerome Wacongne ch4mp@c4-soft.com - * @see SpringAddonsOidcClientProperties for header and request parameter constants definitions - * @see SpringAddonsOauth2AuthenticationSuccessHandler - * @see SpringAddonsOauth2AuthenticationFailureHandler + * @see SpringAddonsOidcClientProperties for header and request parameter constants definitions + * @see SpringAddonsOauth2AuthenticationSuccessHandler + * @see SpringAddonsOauth2AuthenticationFailureHandler */ public class SpringAddonsOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { - private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId"; - - private final URI clientUri; - private final DefaultOAuth2AuthorizationRequestResolver delegate; - private final Map> authRequestCustomizers; - private final AntPathRequestMatcher authorizationRequestMatcher = new AntPathRequestMatcher( - OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}"); - - public SpringAddonsOAuth2AuthorizationRequestResolver( - ClientRegistrationRepository clientRegistrationRepository, - SpringAddonsOidcClientProperties addonsClientProperties) { - - this.clientUri = addonsClientProperties.getClientUri(); - - authRequestCustomizers = addonsClientProperties.getAuthorizationRequestParams().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> { - final var params = addonsClientProperties.getAuthorizationRequestParams().get(e.getKey()); - return e.getValue() == null ? null : requestParamAuthorizationRequestCustomizer(params); - })); - - delegate = new DefaultOAuth2AuthorizationRequestResolver( - clientRegistrationRepository, - OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); - } - - private Optional getFirstParam(HttpServletRequest request, String paramName) { - final var values = request.getParameterValues(paramName); - if (values == null || values.length < 1) { - return Optional.empty(); - } - return Optional.of(values[0]); - } - - private void savePostLoginUrisInSession(HttpServletRequest request) { - final var session = request.getSession(); - Optional - .ofNullable( - Optional - .ofNullable(request.getHeader(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_HEADER)) - .orElse(getFirstParam(request, SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_PARAM).orElse(null))) - .filter(StringUtils::hasText) - .map(URI::create) - .ifPresent(postLoginSuccessUri -> { - session.setAttribute(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE, postLoginSuccessUri); - }); - - Optional - .ofNullable( - Optional - .ofNullable(request.getHeader(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_HEADER)) - .orElse(getFirstParam(request, SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_PARAM).orElse(null))) - .filter(StringUtils::hasText) - .map(URI::create) - .ifPresent(postLoginFailureUri -> { - session.setAttribute(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE, postLoginFailureUri); - }); - } - - @Override - public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { - savePostLoginUrisInSession(request); - final var registrationId = resolveRegistrationId(request); - delegate.setAuthorizationRequestCustomizer(authRequestCustomizers.getOrDefault(registrationId, b -> {})); - final var resolved = delegate.resolve(request); - final var absolute = toAbsolute(resolved, request); - return absolute; - } - - @Override - public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) { - savePostLoginUrisInSession(request); - delegate.setAuthorizationRequestCustomizer(authRequestCustomizers.getOrDefault(clientRegistrationId, b -> {})); - final var resolved = delegate.resolve(request, clientRegistrationId); - final var absolute = toAbsolute(resolved, request); - return absolute; - } - - private OAuth2AuthorizationRequest toAbsolute(OAuth2AuthorizationRequest defaultAuthorizationRequest, HttpServletRequest request) { - if (defaultAuthorizationRequest == null || clientUri == null) { - return defaultAuthorizationRequest; - } - - final var original = URI.create(defaultAuthorizationRequest.getRedirectUri()); - final var redirectUri = UriComponentsBuilder - .fromUri(clientUri) - .path(original.getPath()) - .query(original.getQuery()) - .fragment(original.getFragment()) - .build() - .toString(); - return OAuth2AuthorizationRequest - .from(defaultAuthorizationRequest) - .redirectUri(redirectUri) - .authorizationRequestUri(defaultAuthorizationRequest.getAuthorizationRequestUri()) - .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; - } - - private static Consumer requestParamAuthorizationRequestCustomizer(RequestParam[] additionalParams) { - return customizer -> customizer.authorizationRequestUri(authorizationRequestUri -> { - for (var reqParam : additionalParams) { - authorizationRequestUri.queryParam(reqParam.getName(), reqParam.getValue()); - } - return authorizationRequestUri.build(); - }); - } + private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId"; + + private final URI clientUri; + private final DefaultOAuth2AuthorizationRequestResolver delegate; + private final Map> authRequestCustomizers; + private final AntPathRequestMatcher authorizationRequestMatcher = new AntPathRequestMatcher( + OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}"); + + public SpringAddonsOAuth2AuthorizationRequestResolver( + ClientRegistrationRepository clientRegistrationRepository, + SpringAddonsOidcClientProperties addonsClientProperties) { + + this.clientUri = addonsClientProperties.getClientUri(); + + authRequestCustomizers = addonsClientProperties.getAuthorizationRequestParams().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> { + final var params = addonsClientProperties.getAuthorizationRequestParams().get(e.getKey()); + return e.getValue() == null ? null : requestParamAuthorizationRequestCustomizer(params); + })); + + delegate = new DefaultOAuth2AuthorizationRequestResolver( + clientRegistrationRepository, + OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); + } + + private Optional getFirstParam(HttpServletRequest request, String paramName) { + final var values = request.getParameterValues(paramName); + if (values == null || values.length < 1) { + return Optional.empty(); + } + return Optional.of(values[0]); + } + + private void savePostLoginUrisInSession(HttpServletRequest request) { + final var session = request.getSession(); + Optional.ofNullable( + Optional.ofNullable(request.getHeader(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_HEADER)) + .orElse(getFirstParam(request, SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_PARAM).orElse(null))) + .filter(StringUtils::hasText).map(URI::create).ifPresent(postLoginSuccessUri -> { + session.setAttribute(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_SUCCESS_URI_SESSION_ATTRIBUTE, postLoginSuccessUri); + }); + + Optional.ofNullable( + Optional.ofNullable(request.getHeader(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_HEADER)) + .orElse(getFirstParam(request, SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_PARAM).orElse(null))) + .filter(StringUtils::hasText).map(URI::create).ifPresent(postLoginFailureUri -> { + session.setAttribute(SpringAddonsOidcClientProperties.POST_AUTHENTICATION_FAILURE_URI_SESSION_ATTRIBUTE, postLoginFailureUri); + }); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + savePostLoginUrisInSession(request); + final var registrationId = resolveRegistrationId(request); + delegate.setAuthorizationRequestCustomizer(authRequestCustomizers.getOrDefault(registrationId, b -> { + })); + final var resolved = delegate.resolve(request); + final var absolute = toAbsolute(resolved, request); + return absolute; + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) { + savePostLoginUrisInSession(request); + delegate.setAuthorizationRequestCustomizer(authRequestCustomizers.getOrDefault(clientRegistrationId, b -> { + })); + final var resolved = delegate.resolve(request, clientRegistrationId); + final var absolute = toAbsolute(resolved, request); + return absolute; + } + + private OAuth2AuthorizationRequest toAbsolute(OAuth2AuthorizationRequest defaultAuthorizationRequest, HttpServletRequest request) { + if (defaultAuthorizationRequest == null || clientUri == null) { + return defaultAuthorizationRequest; + } + + final var original = URI.create(defaultAuthorizationRequest.getRedirectUri()); + final var redirectUri = + UriComponentsBuilder.fromUri(clientUri).path(original.getPath()).query(original.getQuery()).fragment(original.getFragment()).build().toString(); + return OAuth2AuthorizationRequest.from(defaultAuthorizationRequest).redirectUri(redirectUri) + .authorizationRequestUri(defaultAuthorizationRequest.getAuthorizationRequestUri()).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; + } + + private static Consumer requestParamAuthorizationRequestCustomizer(RequestParam[] additionalParams) { + return customizer -> customizer.additionalParameters(params -> { + for (var reqParam : additionalParams) { + params.put(reqParam.getName(), reqParam.getValue()); + } + }); + } }