How to configure Spring Boot to use OIDC while app is behind an SSL termination proxy - spring

I am trying to configure a Spring Boot app to use OIDC. The server is behind an SSL termination proxy.
Here are the properties I use:
spring:
security:
oauth2:
client:
provider:
oidc:
authorization-uri: https://example.org/oidc/oauth2/authorize
token-uri: https://example.org/oidc/oauth2/access_token
user-info-uri: https://example.org/oidc/oauth2/userinfo
jwk-set-uri: https://example.org/oidc/oauth2/connect/jwk_uri
custom-params: param_name1,param_value1,param_name2,param_value2,nonce,123456789
registration:
oidc:
client-id: myclientid
client-secret: myclientsecret
authorization-grant-type: authorization_code
scope:
- openid
- email
- profile
redirect-uri: https://mydomain/myapp/login/oauth2/code/oidc
Here's where it goes wrong:
1. The OIDC server requires a nonce param to be added to the request URL
I have solved this by using a custom OAuth2AuthorizationRequest that reads the custom-params property and appends those values to the request URL
2. The OidcAuthorizationCodeAuthenticationProvider throws an exception caused by invalid_redirect_uri_parameter
I have tried many approaches to fix this.
I have tried creating a filter that adds the X-Forwarded-Proto to the request (because the proxy doesn't handle that).
The headers are added, I have also added the following properties:
server:
forward-headers-strategy: native
tomcat.protocol-header: x-forwarded-proto
But it doesn't seem to work.
OidcAuthorizationCodeAuthenticationProvider still throws an exception because this condition is false:
!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())
I have debugged the code and the only difference is one being http and the other https.
I have found a VERY hacky solution that I don't like at all, which is another filter that modifies the URL just for that particular URL.
I would prefer a more elegant solution.
3. When using the custom nonce parameter, the OidcAuthorizationCodeAuthenticationProvider throws an exception cause by invalid_nonce
Now I am stuck. I considered writing my own Authentication Provider, but I have no guarantee that mine will be picked up before the OIDC one provided by Spring.
And with the nonce, it's a catch 22:
if I don't use the custom param, I couldn't find a way to make Spring add the nonce to the request
if I use that one, Spring doesn't recognize it when it's part of the JWT and freaks out
Any help would be GREATLY appreciated, as this has been driving me nuts for days if not weeks.
Thank you.
EDIT
The 2 urls that are compared in case 2 come from:
OAuth2AuthorizationRequest
OAuth2AuthorizationResponse
OAuth2AuthorizationRequest is built in the
OAuth2AuthorizationRequestRedirectFilter at the following line:
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
The redirect uri is built in DefaultOAuth2AuthorizationRequestResolver.expandRedirectUri() which calls
UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
OAuth2AuthorizationResponse is built in the OAuth2LoginAuthenticationFilter.attemptAuthentication() which also calls
UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
and then
OAuth2AuthorizationResponseUtils.convert(params, redirectUri)
I will double check, but I don't remember UriComponentsBuilder.adaptFromForwardedHeaders(HttpHeaders headers) being called when building these URLs.
And even if that works, that still leaves the nonce issue :(

We stumbled upon the same problem , the problem was mainly because our server was behind a reverse proxy, and it seems the proxy changed the url somehow causing this check to fail
!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())
this line was removed in later versions of spring security , at commit
24500fa3ca23aa23ede86dfcfe02113671d5b8bc
commit at github
which was introduced on Dec 6, 2019 and was in spring security release 5.1.13
so the solution was to upgrade spring boot to at least 2.1.17 for spring boot 2.1.X line of versions.
while the OP said he can't upgrade his libraries, I hope this can help those who can.
We also did the solution mentioned above by Kavithakaran Kanapathippilla and configured our reverse proxy to add X-forwarded-proto http headers , also i believed we configured spring boot application.properties to check for them
spring boot documentation for working behind proxies

Related

CSRF protection not working with Spring Security 6

I upgraded my project to Spring Boot 3 and Spring Security 6, but since the upgrade the CSRF protection is no longer working.
I'm using the following configuration:
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated())
.httpBasic(withDefaults())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.build();
}
#Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder().username("user").password("{noop}test").authorities("user").build();
return new InMemoryUserDetailsManager(user);
}
On my webpage I only have a single button:
<button id="test">Test CSRF</button>
And the following JavaScript code:
document.querySelector("#test").addEventListener('click', async function() {
console.log('Clicked');
// This code reads the cookie from the browser
// Source: https://stackoverflow.com/a/25490531
const csrfToken = document.cookie.match('(^|;)\\s*XSRF-TOKEN\\s*=\\s*([^;]+)')?.pop();
const result = await fetch('./api/foo', {
method: 'POST',
headers: {
'X-XSRF-Token': csrfToken
}
});
console.log(result);
});
In Spring Boot 2.7.x this setup works fine, but if I upgrade my project to Spring Boot 3 and Spring Security 6, I get a 403 error with the following debug logs:
15:10:51.858 D o.s.security.web.csrf.CsrfFilter: Invalid CSRF token found for http://localhost:8080/api/foo
15:10:51.859 D o.s.s.w.access.AccessDeniedHandlerImpl: Responding with 403 status code
My guess is that this is related to the changes for #4001. However I don't understand what I have to change to my code or if I have to XOR something.
I did check if it was due to the new deferred loading of the CSRF token, but even if I click the button a second time (and verifying that the XSRF-TOKEN cookie is set), it still doesn't work.
I have recently added a section to the reference documentation for migrating to 5.8 (in preparation to 6.0) that demonstrates a solution for this issue.
TL;DR See I am using AngularJS or another Javascript framework.
The issue here is that AngularJS (and your example code above) are using the XSRF-TOKEN cookie directly. Prior to Spring Security 6, this was fine. But unfortunately, the cookie is actually used to persist the raw token, and with Spring Security 6, the raw token is not accepted by default. Ideally, front-end frameworks would be able to use another source to get the token, such as an X-XSRF-TOKEN response header.
However, even with Spring Security 6, such a response header is not provided out of the box, though it could be a possible enhancement worth suggesting. I have not yet suggested such an enhancement since Javascript frameworks would not be able to use it by default.
For now, you will need to work around the problem by configuring Spring Security 6 to accept raw tokens, as suggested in the section I linked above. The suggestion allows raw tokens to be submitted, but continues to use the XorCsrfTokenRequestAttributeHandler to make available the hashed version of the request attribute (e.g. request.getAttribute(CsrfToken.class.getName()) or request.getAttribute("_csrf")), in case anything renders the CSRF token to an HTML response which could be vulnerable to BREACH.
I would recommend finding a reputable source for researching BREACH more thoroughly, but unfortunately I cannot claim to be such a source.
I would also recommend keeping an eye on Spring Security issues for now, as things may change quickly once the community begins consuming Spring Security 6. You can use this filter as a possible way to keep track of CSRF-related issues.
We have an angular angular application with spring-boot. We tried to migrate to spring-boot 3 (Spring Security 6). And we faced the same problem.
We tried many methods including some of the solutions from this question's answer but we failed. After spending time we found the solution from the spring security doc.
What we need to do is, set the CsrfRequestAttributeName to null in the configuration.
requestHandler.setCsrfRequestAttributeName(null);
What actually happened:
The CsrfToken will be loaded on each request in Spring Security version 5 by default. This means that in a typical setup, every request—even those that are not necessary—must have the HttpSession read.
The default behavior of Spring Security 6 is to postpone looking up the CsrfToken until it is required.
Our application needs the token every time. So, We need to opt into the 5.8 defaults.
The example code is given below (from doc):
#Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
// set the name of the attribute the CsrfToken will be populated on
requestHandler.setCsrfRequestAttributeName(null);
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(requestHandler)
);
return http.build();
}
Thanks for this! I was able to use it to solve a similar project in a JHipster + Spring Boot 3 app. However, it seems the class name might've changed recently. Here's what I had to use:
.csrf(csrf -> csrf
.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new ServerCsrfTokenRequestAttributeHandler()))
I currently worked around the problem by disabling the XorCsrfTokenRequestAttributeHandler like this:
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// Added this:
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()))
However, this means that I'm likely vulnerable against the BREACH attack.
Using the accepted answer breaks tests that require CSRF using Spring Security's SecurityMockMvcRequestPostProcessors.crsf() I can either only use CsrfTokenRequestAttributeHandler, or XorCsrfTokenRequestAttributeHandler in Spring Boot's CSRF configuration, both give positive test results.
Using the accepted answer makes Angular work but breaks tests.
So the only workaround at the moment seems to be using CsrfTokenRequestAttributeHandler and so effectively disabling Spring Security's BREACH-protection.
As of Spring Security 6.0.1 and Spring Boot 3.0.2, following the instructions from the accepted answer fails on the first request but succeeds thereafter. The reason it fails on the first request is because the token's cookie never gets created until a protected method is invoked. This is because the method CookieCsrfTokenRepository.saveToken only gets called when the CsrfFilter calls deferredCsrfToken.get(), which only gets called on POST, PUT, PATCH, and DELETE methods. Unfortunately, under the current implementation, that means the client has to expect a failure on the first request. Under previous versions of Spring Security, we used to be able to count on the token's cookie being included in the response to GET, HEAD, or OPTIONS requests.

Client Registration with Spring-boot Oauth2 - tokenUri vs issuerUri

Sorry folks, this may be a newb question. I'm a little lost.
My Spring-boot environment provides me with keycloak for client authorization, it gives me these.
spring.security.oauth2.resourceserver.jwt.issuer-uri
spring.security.oauth2.client.provider.keycloak.issuer-uri
spring.security.oauth2.client.registration.keycloak.* # client-id, secret, provider, grant-type
I noticed on the ClientRegistration that .issuerUri(String uri) is not avaialbe until Spring-Security v5.4.x. I am using 5.3.5, although I could bump up. I am confused what the difference is. As I would expect, I get an error when I do .tokenUri(issuerUri). I believe they are different modes/API, but I am at a loss as to what I should set in the 5.3.5 API.
Caused by: org.springframework.security.oauth2.client.ClientAuthorizationException: [invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: 405 Method Not Allowed: [{"error":"RESTEASY003650: No resource method found for POST, return 405 with Allow header"}]
So as a newb, I don't get why I have 4 choices of URI and what they do. Google and javadoc haven't been much help, so I figure I just don't know the right place to look to learn it. The only way I know how to fix this is to manual make my own HTTP call to the URI and get my Authentication token, but that would defeat the purpose of the Oauth2 library.
tokenUri represents the URI for the token endpoint. For example:
https://authz.example.org/auth/realms/myrealms/protocol/openid-connect/token
Whereas issuerUri is the URI that identifies the Authorization Server:
https://authz.example.org/auth
It's quite common for the issuer URI to be the root for more specific URIs like the token URI.
Regarding your specific error, I'd imagine that Keycloak is stating that you cannot POST to https://authz.example.org/auth, which is true. You should be POSTing to the token endpoint.
The issuer-uri Spring Boot property should cause Spring Security to look up the other endpoints and add them to a default ClientRegistration. Because of that, I'm not sure why you are also trying to programmatically configure ClientRegistration. That said, if you do need to programmatically create a ClientRegistration, you can use the issuer URI like so, and Spring Security will do the rest:
#Bean
ClientRegistrationRepository registrations() {
ClientRegistration registration = ClientRegistrations
.forIssuerLocation("https://authz.example.org/auth")
.build();
return new InMemoryClientRegistrationRepository(registration);
}

Setting end session endpoint

With a Spring Boot client configured in the DMZ and Spring Security OAuth configured using:
issuer-uri: https://authentication_server/auth/realms/my-realm
I get this error from Spring Security:
The Issuer "https://external_url/auth/realms/my-realm" provided in the configuration metadata did not match the requested issuer "https://authentication_server/auth/realms/my-realm
From this post I have learned that I need to specify authorization-uri, token-uri and jwk-set-uri instead of issuer-uri, and then it also works.
authorization-uri: https://external_url/auth/realms/my-realm/protocol/openid-connect/auth
token-uri: https://authentication_server/auth/realms/my-realm/protocol/openid-connect/token
jwk-set-uri: https://authentication_server/auth/realms/my-realm/protocol/openid-connect/certs
(I do not get why Spring Security cannot auto-configure with the same values from the issuer-uri when it works setting the values individually)
Now the problem is that logout stops working. When using issuer-uri the OAuth is auto-configured and end_session_endpoint is fetched from the answer, but when specifying each setting there is no way to specify the end_session_endpoint.
Is this an outstanding issue in Spring Security OAuth, or do I need to configure it differently?
I had to make a work around for this. With little time I started by copying the existing OidcClientInitiatedLogoutSuccessHandler which I already were using in configuring LogoutRedirectUri.
I simply copied the class and changed the implementation of the method endSessionEndpoint() to return the URI which is returned by our OAuth server as end_session_endpoint.
This issue is tracked in spring-security GitHub.
Probable fix will be allowing to add "Additional attributes for ClientRegistration and ProviderDetails".

Configuring spring-boot-starter-oauth2-client to authenticate with Azure AD

I want to add Azure AD as an OAuth2 provider in Spring Boot 2.4. I followed Spring Boot's OAuth2 docs and came up with the following configuration:
spring.security.oauth2.client.provider.azuread.issuer-uri=https://login.microsoftonline.com/<tenant uuid>/v2.0
spring.security.oauth2.client.registration.azuread.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.azuread.client-id=<client uuid>
spring.security.oauth2.client.registration.azuread.client-name=Azure AD
spring.security.oauth2.client.registration.azuread.client-secret=<client secret>
spring.security.oauth2.client.registration.azuread.provider=azuread
spring.security.oauth2.client.registration.azuread.scope=openid
Just for completeness, this is my web security configuration:
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.authorizeRequests(a -> a
.antMatchers("/", "/login", "/error", "/webjars/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login();
// #formatter:on
}
}
When coming back from entering the credentials on https://login.microsoftonline.com, I get the following error:
[invalid_id_token] An error occurred while attempting to decode the Jwt: Signed JWT rejected: Another algorithm expected, or no matching key(s) found.
The problem originates in DefaultJWTProcessor.java from Nimus-JOSE-JWT.
Looking through the requests in Firefox's network inspector, Spring Boot picks up the right URLs from the Issuer URI. I'm at a loss what's going wrong and appreciate any pointers.
Azure AD has some pretty unintuitive (in my opinion) default behaviour - I think this is what you are experiencing:
YOUR PROBLEM CAUSE (I THINK)
You are using standard OpenID Connect scopes
This causes Azure AD to issue an access token intended for the Graph API
This token fails standards based validation in Custom APIs since it is only designed for Graph APIs to use - it is recognisable by the nonce field in the JWT header
WHAT YOU NEED TO DO
Expose an API scope such as 'default'
Use the full value of this scope in your web client, with a value such as 'api://cb398b43-96e8-48e6-8e8e-b168d5816c0e/default', where the long identifier is that of the API
You will then get a normal OAuth token that Spring can validate - with no nonce field in the JWT header
FURTHER INFO
See steps 3 to 8 of my blog post from a couple of years ago
See the OpenID Connect settings of my web code sample
I had a similar problem and if I remember correctly there was an issue with scope. Don't know whether this is also your issue but in any case following configuration is working for me (note I'm using client credentials not auth code):
spring:
security:
oauth2:
client:
provider:
azure:
token-uri: https://login.microsoftonline.com/${custom.azure.account.tenant-id}/oauth2/token
registration:
azure:
client-id: ${custom.azure.service-principal.client-id}
client-secret: ${custom.azure.service-principal.client-secret}
authorization-grant-type: client_credentials
scope:
- https://graph.microsoft.com/.default
According to your description, it seems that you wanna use azure ad as the an OAuth2 provider in Spring Boot, and I found a doc said that 'Spring Starter for Azure Active Directory (AD) is now integrated with Spring Security 5.0'. It also offered a sample of springboot web app. I also tried it and it did work.
What I think need to note is, the tutorial said the redirect url setted in azure ad is 'http://localhost:8080/login/oauth2/code/', but when my tested, it proved to be 'http://localhost:8080/login/oauth2/code/azure'.
Upon your error, I'm not sure but maybe you can change your configuration of 'scope' to 'openid,profile,offline_access'.

Reactive spring cloud security (with Keycloak): Session expiration?

I try implementing the following: I want a distributed environment with one or more spring cloud gateway(s), behind these are several micro services (partly) exposed to the outside. For user authentication I want to use OIDC (just moved to Keycloak).
I actually just sticked to the standard configuration from the reference documentations of spring security, webflux and boot.
In detail, I have in the gateway:
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/login").permitAll()
.pathMatchers("/actuator/**").permitAll()
.anyExchange().authenticated()
.and()
.oauth2Login()
.and()
.logout(logout -> logout.logoutSuccessHandler(oidcLogoutSuccessHandler()));
return http.build();
}
private ServerLogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedServerLogoutSuccessHandler oidcLogoutSuccessHandler =
new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository);
oidcLogoutSuccessHandler.setPostLogoutRedirectUri(URI.create("https://<host>/login?logout"));
return oidcLogoutSuccessHandler;
}
with application.yml (only essential part):
spring:
cloud:
gateway:
default-filters:
- TokenRelay=
- RemoveRequestHeader=Cookie
discovery:
locator:
enabled: true
consul:
host: 127.0.0.1
port: 8500
loadbalancer:
ribbon:
enabled: false
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: ${KEYCLOAK_ISSUER_URI}
registration:
keycloak:
client-id: ${KEYCLOAK_CLIENT_ID}
client-secret: ${KEYCLOAK_CLIENT_SECRET}
resourceserver:
jwt:
jwk-set-uri: ${KEYCLOAK_ISSUER_URI}/protocol/openid-connect/certs
server:
forward-headers-strategy: framework
For the proof of concept I have an "environment-test-application" with a controller just returning the claims as a json. It is configured like:
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.pathMatchers("/actuator/**").permitAll()
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt(Customizer.withDefaults());
return http.build();
}
The application.yml only contains the consul and security part from above.
Basically this works. If I try to access the environment-test-application, I get redirected to Keycloak, I have to enter my credentials and get redirected back to the environment-test-application and see the claims. I can logout by means of "/logout".
The problem: After 5 minutes the keycloak session expires, I get a 401 and chrome says "this page is not working". This happens even, if I am reloading all the time (user activity).
To my understanding the session should:
be extended as long as there is activity (I thought this is what the refresh token is used for, and assumed spring security to handle this automatically)
expire when the user is inactive for some time. When the user is active again, he/she should be redirected to the login and then back to the original resource.
when hitting "remember me", the an expired session should be reinitiated without user activity (I think, this is what the offline token from Keycloak is meant for).
I guess all this is possible by adding just some simple lines of configuration, I just can't figure which. The reference documentation wasn't entirely clear to me, but I got the feeling this is all handled like so, by default. However, it is not.
Note: I am using spring reactive stack with the default Reactor Netty. Thus, neither the spring security, nor the spring boot plugin provided by Keycloak, can be used. Plus, it is not clear to me, how they interact with the standard configuration scheme.
Note: Before I was trying all this with okta (using their starter). The problem described above magically seemed to be working with okta. However, I had different issues and thus moved to open source now.
TokenRelayGatewayFilterFactory adds the access token but doesn't refresh it when it expires...which is why you get the 401 I believe. There's an open spring cloud gateway issue asking for a solution that refreshes as well. One of the comments on that issue provides an implementation: https://github.com/spring-cloud/spring-cloud-security/issues/175#issuecomment-557135243.
When the refresh token filter is working, the keycloak session only becomes important after your spring cloud gateway "session" expires because, if the keycloak session is still good, it allows the oauth2 redirect to re-establish the session seemlessly (i.e. without having to enter your credentials again).
If you're looking to customize the session of your webflux application, there are some solutions including this one: https://stackoverflow.com/a/62344617/1098564
Not sure what you're using for your frontend but he's how I use spring security with spring cloud gateway and react: https://sdoxsee.github.io/blog/2019/12/17/merry-microservices-part2-ui-gateway

Resources