I'm using Spring 6.0 to create an application that can be connected to via browser or a stand-alone application. It uses Keycloak as the OAuth2 authorization server, while the application should serve the data.
Access via browser works fine, but the stand-alone application gets an access token from keycloak (device authorization grant or password grant). When it passes the token to the Spring application, the call gets redirected to the keycloak login site instead of using the valid token.
My configuration is simple:
#Configuration
public class OAuth2Config
{
/**
* The primary client authentication manager.
*
* #param clientRegistrationRepository
* #param authorizedClientRepository
* #return The authorized client manager
*/
#Bean
public OAuth2AuthorizedClientManager clientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository )
{
// Use the default OAuth client manager - configurations can be made in
// the properties - and set the built provider.
//
DefaultOAuth2AuthorizedClientManager clientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository );
return clientManager;
}
}
But I'm clearly missing something. What else do I need to provide in the request or adjust my parameters so that my resource server will check the provided token and not redirect back to the login page?
The logs seem to indicate that it is not reading my token correctly - everything is anonymous, which it should not be.
2023-01-04T15:30:19.996-06:00 TRACE 9680 --- [nio-8081-exec-4] o.s.security.web.FilterChainProxy : Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.session.DisableEncodeUrlFilter#6c6c93f8, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#38e88e13, org.springframework.security.web.context.SecurityContextHolderFilter#89178b4, org.springframework.security.web.header.HeaderWriterFilter#20ad64c, org.springframework.security.web.csrf.CsrfFilter#1512efe9, org.springframework.security.web.authentication.logout.LogoutFilter#7fa86ddd, org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter#59509393, org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter#7ec75228, org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter#1d6dc2b8, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter#3314f179, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter#3a720ae3, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#64836643, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#42684d86, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#1fe37d27, org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter#3fc7abf6, org.springframework.security.web.access.ExceptionTranslationFilter#600b3bee, org.springframework.security.web.access.intercept.AuthorizationFilter#565030b7]] (1/1)
2023-01-04T15:30:19.996-06:00 DEBUG 9680 --- [nio-8081-exec-4] o.s.security.web.FilterChainProxy : Securing GET /users
2023-01-04T15:30:19.996-06:00 TRACE 9680 --- [nio-8081-exec-4] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/17)
:
:
2023-01-04T15:30:20.000-06:00 TRACE 9680 --- [nio-8081-exec-4] o.s.security.web.FilterChainProxy : Invoking AuthorizationFilter (17/17)
2023-01-04T15:30:25.332-06:00 TRACE 9680 --- [nio-8081-exec-4] estMatcherDelegatingAuthorizationManager : Authorizing SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest#60be86e5]
2023-01-04T15:30:25.332-06:00 TRACE 9680 --- [nio-8081-exec-4] estMatcherDelegatingAuthorizationManager : Checking authorization on SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest#60be86e5] using org.springframework.security.authorization.AuthenticatedAuthorizationManager#3ba6b23
2023-01-04T15:30:25.332-06:00 TRACE 9680 --- [nio-8081-exec-4] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
2023-01-04T15:30:25.332-06:00 TRACE 9680 --- [nio-8081-exec-4] .s.s.w.c.SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
2023-01-04T15:30:25.333-06:00 TRACE 9680 --- [nio-8081-exec-4] .s.s.w.c.SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
2023-01-04T15:30:25.333-06:00 TRACE 9680 --- [nio-8081-exec-4] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=192.168.0.119, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]]
2023-01-04T15:30:25.334-06:00 TRACE 9680 --- [nio-8081-exec-4] o.s.s.w.a.ExceptionTranslationFilter : Sending AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=192.168.0.119, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]] to authentication entry point since access is denied
A REST API is a resource-server, not a client.
I guess you used spring-boot-starter-oauth2-client (or wrote quite some conf for spring-security 6 OAuth2 client) when you should use spring-boot-starter-oauth2-resource-server.
If serving html pages in addition to REST endpoints, you'll need two security filter-chain: a resource-server one for REST endpoints and a client one for html pages.
All that is already described in this SO answer "Use Keycloak Spring Adapter with Spring Boot 3"
Related
I have configured spring webflux with Open Id Connect with Keycloak as an IDP.
The problem is that for every call to my application, the oauth2 client does a call to keycloak instead of using the security session.
I have configured my webflux security as follows:
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ServerLogoutSuccessHandler handler) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.securityContextRepository(new WebSessionServerSecurityContextRepository())
.oauth2Login(withDefaults());
return http.build();
}
I have a security context repository saved in the web session.
I have configured my web session in memory like so:
#Bean
public ReactiveSessionRepository reactiveSessionRepository() {
return new ReactiveMapSessionRepository(new ConcurrentHashMap<>());
}
In my logs I can see that a security context has been found:
WebSessionServerSecurityContextRepository : Found SecurityContext 'SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=Name: [alex], Granted Authorities: [[ROLE_USER, SCOPE_email, SCOPE_openid, SCOPE_profile]], User Attributes: [{sub=bdc6b386-623f-4fe4-a013-2c694678797b, email_verified=true, name=Aleksandar KIRILOV, preferred_username=alex, given_name=John, family_name=Doe, email=mymail#mail.com}], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER, SCOPE_email, SCOPE_openid, SCOPE_profile]]]' in WebSession: 'org.springframework.session.web.server.session.SpringSessionWebSessionStore$SpringSessionWebSession#5a17f06f'
Please help on how to avoid calling keycloak if the security context is still valid in the websession.
Best Regards !
a colleague of mine found the solution:
It seems that there is a clock skew protection in spring oauth2 client and I had set my access_token to expire after only one minute.
The clock skew protection was also set to one minute meaning that spring will preemptively go and refresh the token since we are close to expiration time.
I have a resource service behind Cloud Gateway route with RelayToken filter:
routes:
- id: apis
uri: http://rest-app:8080/apis
predicates:
- Path=/apis/**
filters:
- TokenRelay=
GET requests work fine, but on POSTs I get 403 Forbidden with response body containing
CSRF Token has been associated to this client
I've tried to disable CSRF protection adding Bean
#Bean
fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http.csrf().disable().cors().disable().build()
}
But this has no effect and I still get 403. Moreover I cannot debug which exactly filter prevents client from doing POST requests, the only logging information I get with
logging:
level:
root: INFO
org.springframework.web: TRACE
org.springframework.security: TRACE
org.springframework.security.oauth2: TRACE
org.springframework.cloud.gateway: TRACE
org.springframework.security.jwt: TRACE
is just couple of lines saying POST was forbidden
[2020-04-01 13:21:32,635] TRACE o.s.w.s.a.HttpWebHandlerAdapter - [58a0e540-10] HTTP POST "/apis/", headers={masked}
[2020-04-01 13:21:32,640] TRACE o.s.w.s.a.HttpWebHandlerAdapter - [58a0e540-10] Completed 403 FORBIDDEN, headers={masked}
[2020-04-01 13:21:32,640] TRACE o.s.h.s.r.ReactorHttpHandlerAdapter - [58a0e540-10] Handling completed
How do I correctly turn CSRF off?
Correct SecurityWebFilterChain that solved my problem:
#Bean
fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http
.authorizeExchange().anyExchange().authenticated()
.and()
.oauth2Login()
.and()
.csrf().disable()
.build()
}
I am running a fairly basic Azure IoT C# module. Initially, it all worked fine. However, when I set up X.509 certificates for the edge hub, I've been getting module authentication errors as follows. There seems to be some dependence on using Tcp_only or Websocket_only options of the AMQP/MQTT protocols (where the latter does not throw the exception), but this is not consistent across modules.
The offending lines of code are below
..
// Open a connection to the Edge runtime
ioTHubModuleClient = await ModuleClient.CreateFromEnvironmentAsync(settings);
await ioTHubModuleClient.OpenAsync();
The error message is below. Essentially the await times out because authentication is not complete. Far as I know, authentication is derived directly from the iot edge runtime, so not sure why it was specifically trying to authenticate or erroring out...
Unhandled Exception: System.AggregateException: One or more errors
occurred. (The remote certificate is invalid according to the
validation procedure.) --->
System.Security.Authentication.AuthenticationException: The remote
certificate is invalid according to the validation procedure. at
Microsoft.Azure.Devices.Client.InternalClient.<>c.b__62_2(Task
t) at System.Threading.ExecutionContext.Run(ExecutionContext
executionContext, ContextCallback callback, Object state) at
System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task&
currentTaskSlot) --- End of stack trace from previous location where
exception was thrown --- at
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at
System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task
task) at SampleModule.Program.d__10.MoveNext() in
/app/Program.cs:line 69 --- End of inner exception stack trace --- at
System.Threading.Tasks.Task.ThrowIfExceptional(Boolean
includeTaskCanceledExceptions) at
System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout,
CancellationToken cancellationToken) at
SampleModule.Program.Main(String[] args) in /app/Program.cs:line 41
As context, I've been trying to get a fairly simple #SprintBootApplication with an additional #EnableOAuth2Sso annotation integrated with WSO2 Identity Server for quite some time now.
In my mind getting this working should be a matter of configuration (as advertised on Spring Cloud Security) - but I've had no luck thus far.
In an effort to understand what is going on I've used my debugger to step through spring-security-oauth2 code to figure out what is going on. In doing so I've noticed that my AccessTokenRequest's PreservedState is perpetually null with a resultant CSRF related InvalidRequestException. This is the relevant code:
public class AuthorizationCodeAccessTokenProvider extends OAuth2AccessTokenSupport implements AccessTokenProvider {
....
private MultiValueMap<String, String> getParametersForTokenRequest(AuthorizationCodeResourceDetails resource,
AccessTokenRequest request) {
MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
form.set("grant_type", "authorization_code");
form.set("code", request.getAuthorizationCode());
Object preservedState = request.getPreservedState();
if (request.getStateKey() != null || stateMandatory) {
// The token endpoint has no use for the state so we don't send it back, but we are using it
// for CSRF detection client side...
if (preservedState == null) {
throw new InvalidRequestException(
"Possible CSRF detected - state parameter was required but no state could be found");
}
}
When it comes to the above bit of code, I'm at the point where I've approved the claim after having logged in with admin/admin and my web application has received the auth code:
http://localhost:9998/loginstate=Uu8ril&code=20ffbb6e4107ce3c5cf9ee22065f4f2
Given that all I need to do in the first instance is to get the login part working I've tried disabling CSRF, to no avail.
The relevant configuration is as follows:
spring:
profiles: default
security:
oauth2:
client:
accessTokenUri: https://localhost:9443/oauth2/token
userAuthorizationUri: https://localhost:9443/oauth2/authorize
clientId: yKSD9XwET9XJ3srGEFXP6AfHhAka
clientSecret: zuPTcdJH435h3wgl055XNZ5ffNMa
scope: openid
clientAuthenticationScheme: header
resource:
userInfoUri: https://localhost:9443/oauth2/userinfo?schema=openid
In terms of my own investigative efforts, of concern is that in the DefaultOAuthClientContext preserved state gets cleared right before it needs to used, this appears to be a sequencing issue.
DefaultAccessTokenRequest.setPreservedState(http://localhost:9998/login)
DefaultOAuth2ClientContext.state.put(avjDRM,http://localhost:9998/login)
DefaultAccessTokenRequest.setPreservedState(http://localhost:9998/login)
DefaultOAuthClientContext.state.put(MREOgG,http://localhost:9998/login)
LOGIN ON WSO2 FORM
DefaultOAuth2ClientContext.state.remove(avjDRM)
OAUth2RestTemplate…acquireAccessToken…...Object preservedState = oauth2Context.removePreservedState(avjDRM)
I'm using the latest releases of Spring Boot (1.3.0) and WSO2 Identity Server (5.0). Also using spring-security-oauth 2.0.8.
As it turns out, the reason why the cited preservedState is null in the section of code provided is because a new instance of bean Oauth2ClientContext is being created which is precisely what should not be happening - the entire purpose of the OAuth2ClientContext is to store state. In terms of the OAuth2 protocol (RFC 6749), preserving the state is important in terms of Cross-Site Request Forgery prevention (see Section 10.12).
Picking this up is a simple matter of enabling debug logging and also comparing the output generated against WSO2 IS with what one sees with a working example. In my case the working example that I always revert back to is the one provided by the Spring team themselves.
This is the client configuration (application.yml) and then log output testing using the Spring team SSO server:
spring:
profiles: default
security:
oauth2:
client:
accessTokenUri: http://192.168.0.113:32768/uaa/oauth/token
userAuthorizationUri: http://192.168.0.113:32768/uaa/oauth/authorize
clientId: acme
clientSecret: acmesecret
resource:
jwt:
keyValue: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnGp/Q5lh0P8nPL21oMMrt2RrkT9AW5jgYwLfSUnJVc9G6uR3cXRRDCjHqWU5WYwivcF180A6CWp/ireQFFBNowgc5XaA0kPpzEtgsA5YsNX7iSnUibB004iBTfU9hZ2Rbsc8cWqynT0RyN4TP1RYVSeVKvMQk4GT1r7JCEC+TNu1ELmbNwMQyzKjsfBXyIOCFU/E94ktvsTZUHF4Oq44DBylCDsS1k7/sfZC2G5EU7Oz0mhG8+Uz6MSEQHtoIi6mc8u64Rwi3Z3tscuWG2ShtsUFuNSAFNkY7LkLn+/hxLCu2bNISMaESa8dG22CIMuIeRLVcAmEWEWH5EEforTg+QIDAQAB
-----END PUBLIC KEY-----
id: openid
serviceId: ${PREFIX:}resource
Take note that there is no line mentioned the creation of OAuth2ClientContext.
DEBUG o.s.security.web.FilterChainProxy - /login?code=9HLSpP&state=G9kpy3 at position 6 of 12 in additional filter chain; firing Filter: 'OAuth2ClientAuthenticationProcessingFilter'
DEBUG o.s.s.w.u.m.AntPathRequestMatcher - Checking match of request : '/login'; against '/login'
DEBUG o.s.s.o.c.f.OAuth2ClientAuthenticationProcessingFilter - Request is to process authentication
INFO o.s.s.o.c.DefaultOAuth2ClientContext - Getting access token request
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean 'scopedTarget.accessTokenRequest'
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration'
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'scopedTarget.accessTokenRequest'
INFO o.s.s.o.client.OAuth2RestTemplate - Longer lived state key: G9kpy3
INFO o.s.s.o.client.OAuth2RestTemplate - Removing preserved state in oauth2context
INFO o.s.s.o.c.DefaultOAuth2ClientContext - Found preserved state: http://localhost:9999/login
This is the client configuration (application.yml) and then log output testing using WSO2IS 5.0.0:
spring:
profiles: wso2
server:
port: 9998
security:
oauth2:
client:
accessTokenUri: https://localhost:9443/oauth2/token
userAuthorizationUri: https://localhost:9443/oauth2/authorize
clientId: yKSD9XwET9XJ3srGEFXP6AfHhAka
clientSecret: zuPTcdJH435h3wgl055XNZ5ffNMa
scope: openid
clientAuthenticationScheme: header
resource:
userInfoUri: https://localhost:9443/oauth2/userinfo?schema=openid
Take note of the line with Creating instance of bean 'scopedTarget.oauth2ClientContext'.
DEBUG o.s.security.web.FilterChainProxy - /login?state=PWhQwv&code=372ff0c197a4c85a0caf070cc9a6678 at position 6 of 12 in additional filter chain; firing Filter: 'OAuth2ClientAuthenticationProcessingFilter'
DEBUG o.s.s.w.u.m.AntPathRequestMatcher - Checking match of request : '/login'; against '/login'
DEBUG o.s.s.o.c.f.OAuth2ClientAuthenticationProcessingFilter - Request is to process authentication
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean 'scopedTarget.oauth2ClientContext'
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2RestOperationsConfiguration$SessionScopedConfiguration$ClientContextConfiguration'
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'scopedTarget.oauth2ClientContext'
INFO o.s.s.o.c.DefaultOAuth2ClientContext - Getting access token request
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean 'scopedTarget.accessTokenRequest'
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration'
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'scopedTarget.accessTokenRequest'
INFO o.s.s.o.client.OAuth2RestTemplate - Longer lived state key: PWhQwv
INFO o.s.s.o.client.OAuth2RestTemplate - Removing preserved state in oauth2context
INFO o.s.s.o.c.DefaultOAuth2ClientContext - Found preserved state: null
Finally, the next port of call is naturally to ascertain why the OAuth2ClientContext was not created with the WSO2 IS config. Investigation has shown that it is because WSO2 IS is not passing back an expected JSESSIONID, hence the session scoped OAuth2ClientContext will not be found.
A potential hack to fix this situation if desperate is to clone Spring OAuth 2 and do the following:
In class AuthorizationCodeAccessTokenProvider do the following with the hack being to change the preserved state in the request.
private MultiValueMap<String, String> getParametersForTokenRequest(AuthorizationCodeResourceDetails resource,
AccessTokenRequest request) {
MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
form.set("grant_type", "authorization_code");
form.set("code", request.getAuthorizationCode());
request.setPreservedState("http://localhost:9998/login");
Object preservedState = request.getPreservedState();
Just seen a similar issue while trying out https://spring.io/guides/tutorials/spring-boot-oauth2/
and observed that this issue is with chrome browser that wasn't passing the JSESSIONID after facebook redirect .
Same worked when I Used Safari , (just adding it here so that it saves someone's two hours !)
you can see that set-cookie is called twice as second login/facebook call is not passing previously set sessionid
I am pretty new to Spring Security land. I am using programmatic configuration of Spring Security with servletApi() which is pretty neat.
Here is the configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.securityContext().and()
.servletApi().and()
.authorizeUrls()
.antMatchers("/login").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/**").authenticated();
}
I am using http servlet api login I am not using any filter for this.
In case a unauthorised request, ExceptionTranslationFilter uses Http403EntryForbiddenEntryPoint to return 403 forbidden status.
In my scenario:
If user does not authenticated, a 401 status code should return.
If user authenticated but not authorised, a 403 status code should return.
But default configuration creates 403 status for both case.
Here are my questions:
Why is the default entry point is Http403EntryForbiddenEntryPoint? It can be 401?
If I change Http403EntryForbiddenEntryPoint to Http401EntryForbiddenEntryPoint, does It create a problem?
Thanks