I try to reproduce a https://www.baeldung.com/spring-boot-keycloak
tutorial with some simplifications. Also my Keycloak server is on another machine. Config is also slightly changed due to deprecation of the antMatchers
:
@Configuration
@EnableWebSecurity
class SecurityConfig {
private final KeycloakLogoutHandler keycloakLogoutHandler;
SecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler) {
this.keycloakLogoutHandler = keycloakLogoutHandler;
}
@Bean
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests
.requestMatchers("/customers*")
.hasRole("user")
.anyRequest()
.permitAll()
);
http.oauth2Login()
.and()
.logout()
.addLogoutHandler(keycloakLogoutHandler)
.logoutSuccessUrl("/");
http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
}
Properties:
spring.security.oauth2.client.registration.keycloak.client-id=***
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.scope=openid
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://192.168.254.1:8184/realms/***
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/***
Unprotected resource works, login works, but after login redirect I get 403. I tried to disable CORS and/or CSRF, the same.
Log:
2023-03-10T17:33:57.727+03:00 DEBUG 54908 --- [o-8080-Acceptor] o.apache.tomcat.util.threads.LimitLatch : Counting up[http-nio-8080-Acceptor] latch=1
2023-03-10T17:33:57.728+03:00 DEBUG 54908 --- [nio-8080-exec-6] o.a.coyote.http11.Http11InputBuffer : Before fill(): parsingHeader: [true], parsingRequestLine: [true], parsingRequestLinePhase: [0], parsingRequestLineStart: [0], byteBuffer.position(): [0], byteBuffer.limit(): [0], end: [515]
2023-03-10T17:33:57.728+03:00 DEBUG 54908 --- [nio-8080-exec-6] o.a.tomcat.util.net.SocketWrapperBase : Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@7ab77668:org.apache.tomcat.util.net.NioChannel@136f346d:java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:47080]], Read from buffer: [0]
2023-03-10T17:33:57.729+03:00 DEBUG 54908 --- [nio-8080-exec-6] org.apache.tomcat.util.net.NioEndpoint : Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@7ab77668:org.apache.tomcat.util.net.NioChannel@136f346d:java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:47080]], Read direct from socket: [567]
2023-03-10T17:33:57.729+03:00 DEBUG 54908 --- [nio-8080-exec-6] o.a.coyote.http11.Http11InputBuffer : Received [GET /customers?continue HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: Idea-296f3fac=79e0126b-1518-4c2a-b4a3-1f158c95489b; JSESSIONID=A71AB029616D5092A00BE2B11E978123
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
]
2023-03-10T17:33:57.729+03:00 DEBUG 54908 --- [nio-8080-exec-6] o.a.t.util.http.Rfc6265CookieProcessor : Cookies: Parsing b[]: Idea-296f3fac=79e0126b-1518-4c2a-b4a3-1f158c95489b; JSESSIONID=A71AB029616D5092A00BE2B11E978123
2023-03-10T17:33:57.729+03:00 DEBUG 54908 --- [nio-8080-exec-6] o.a.catalina.connector.CoyoteAdapter : Requested cookie session id is A71AB029616D5092A00BE2B11E978123
2023-03-10T17:33:57.730+03:00 DEBUG 54908 --- [nio-8080-exec-6] o.a.c.authenticator.AuthenticatorBase : Security checking request GET /customers
2023-03-10T17:33:57.730+03:00 DEBUG 54908 --- [nio-8080-exec-6] org.apache.catalina.realm.RealmBase : No applicable constraints defined
2023-03-10T17:33:57.730+03:00 DEBUG 54908 --- [nio-8080-exec-6] o.a.c.authenticator.AuthenticatorBase : Not subject to any constraint
2023-03-10T17:33:57.730+03:00 DEBUG 54908 --- [nio-8080-exec-6] o.s.security.web.FilterChainProxy : Securing GET /customers?continue
2023-03-10T17:33:57.731+03:00 DEBUG 54908 --- [nio-8080-exec-6] org.apache.tomcat.util.http.Parameters : Set encoding to UTF-8
2023-03-10T17:33:57.731+03:00 DEBUG 54908 --- [nio-8080-exec-6] org.apache.tomcat.util.http.Parameters : Decoding query null UTF-8
2023-03-10T17:33:57.731+03:00 DEBUG 54908 --- [nio-8080-exec-6] org.apache.tomcat.util.http.Parameters : Start processing with input [continue]
2023-03-10T17:33:57.732+03:00 DEBUG 54908 --- [nio-8080-exec-6] org.apache.tomcat.util.http.Parameters : Parameter starting at position [0] and ending at position [8] with a value of [continue] was not followed by an '=' character
2023-03-10T17:33:57.732+03:00 DEBUG 54908 --- [nio-8080-exec-6] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.dvperv.kkauth.KkauthController#customers(Principal, Model)
2023-03-10T17:33:57.733+03:00 DEBUG 54908 --- [nio-8080-exec-6] w.c.HttpSessionSecurityContextRepository : Retrieved SecurityContextImpl [Authentication=OAuth2AuthenticationToken [Principal=Name: [user1], Granted Authorities: [[OIDC_USER, SCOPE_email, SCOPE_openid, SCOPE_profile]], User Attributes: [{at_hash=cOAUGbpbGKcvhnfekDHyEA, sub=09581f25-c22d-4192-89e0-f90af6c8757c, email_verified=false, iss=http://192.168.254.1:8184/realms/***, typ=ID, preferred_username=user1, given_name=, nonce=M-_DevVz-pT0R6tT9SDg6Wd3S9DBb72mxDH-gBBk0Cc, sid=b75bcd4a-c656-4572-b095-450a038e137d, aud=[***], acr=0, azp=***, auth_time=2023-03-10T14:08:13Z, exp=2023-03-10T14:36:51Z, session_state=b75bcd4a-c656-4572-b095-450a038e137d, family_name=, iat=2023-03-10T14:31:51Z, jti=4607af9f-8e7a-4dfe-8c70-5b8b3e5982e3}], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=432BECAABE9CFCC7EC117409C3E982E9], Granted Authorities=[OIDC_USER, SCOPE_email, SCOPE_openid, SCOPE_profile]]]
2023-03-10T17:33:57.735+03:00 DEBUG 54908 --- [nio-8080-exec-6] o.a.coyote.http11.Http11InputBuffer : Before fill(): parsingHeader: [true], parsingRequestLine: [true], parsingRequestLinePhase: [0], parsingRequestLineStart: [0], byteBuffer.position(): [0], byteBuffer.limit(): [0], end: [567]
2023-03-10T17:33:57.735+03:00 DEBUG 54908 --- [nio-8080-exec-6] o.a.tomcat.util.net.SocketWrapperBase : Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@7ab77668:org.apache.tomcat.util.net.NioChannel@136f346d:java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:47080]], Read from buffer: [0]
2023-03-10T17:33:57.735+03:00 DEBUG 54908 --- [nio-8080-exec-6] org.apache.tomcat.util.net.NioEndpoint : Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@7ab77668:org.apache.tomcat.util.net.NioChannel@136f346d:java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:47080]], Read direct from socket: [0]
2023-03-10T17:33:57.735+03:00 DEBUG 54908 --- [nio-8080-exec-6] o.a.coyote.http11.Http11InputBuffer : Received []
2023-03-10T17:33:57.736+03:00 DEBUG 54908 --- [nio-8080-exec-6] o.apache.coyote.http11.Http11Processor : Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@7ab77668:org.apache.tomcat.util.net.NioChannel@136f346d:java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:47080]], Status in: [OPEN_READ], State out: [OPEN]
2023-03-10T17:33:57.736+03:00 DEBUG 54908 --- [nio-8080-exec-6] org.apache.tomcat.util.net.NioEndpoint : Registered read interest for [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@7ab77668:org.apache.tomcat.util.net.NioChannel@136f346d:java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:47080]]
2023-03-10T17:33:59.794+03:00 DEBUG 54908 --- [alina-utility-2] o.apache.catalina.session.ManagerBase : Start expire sessions StandardManager at 1678458839794 sessioncount 1
2023-03-10T17:33:59.794+03:00 DEBUG 54908 --- [alina-utility-2] o.apache.catalina.session.ManagerBase : End expire sessions StandardManager processingTime 0 expired sessions: 0
2
Answers
I looked throw @ch4mp tutorials, official docs and got a converter from another tutorial. Final schematic solution without SSL and flexibility, but more short, is below.
Props:
Converter:
Config:
Please be careful - role name is a case sensitive! USER != user Full code is here
According to your logs, the user is granted with
OIDC_USER
when your conf expectsROLE_user
. ChanginghasRole("user")
tohasAuthority("OIDC_USER")
should fix your current issue, but you’ll probably face other ones because, in my opinion, the tutorial you link is bad. I advise you refer to "my" tutorials instead: https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorialsWhat I find mostly problematic in the tutorial you link is the confusion between client and resource server. In OAuth2, the authorization server, the resource server and the client are different actors with different responsibilities and security requirements.
Spring provides with different boot starters for OAuth2 clients and OAuth2 resource servers for a reason.
Sessions
Resource server can (should?) usually be configured stateless (without sessions) which makes it insensible to CSRF attacks and also easier to scale and more fault tolerant. "State" is maintained by the access token: the security context of a request is built from this token, not from a session.
Client with OAuth2 login need sessions (it couldn’t even complete an authorization-code flow without it) and as a consequence, must be protected against CSRF attacks. Requests received by a client are usually secured with a session cookie, not an access token, and the request security context is built from the session.
Authentication
User login and logout are the responsibility of the client, not of the resource server. It makes very little sense to have
oauth2Login
(orlogout
) in aSecurityFilterChain
withoauth2ResourceServer
.The resource server does not care about login, logout nor OAuth2 flows. All that matters to it is if a request is authorized (has an
Authorization
header) with a validBearer
access token and if it should grant access to the requested resource based on the claims contained in that token (or introspected from it).The client is responsible for authorizing the requests it sends to resource servers. For that, it uses one of the OAuth2 flow to retrieve tokens from the authorization server. When a client needs to send a request on behalf of a user, it uses authorization-code flow (which requires user to login only if he does not have an opened session on the authorization server already), but can also use other flows depending on the context (refresh-token to renew an access token or client-credentials to issue a request without the context of a user).
Security In Bealdung Tutorial
As opposed to what is stated in the article you linked, the requests send with a browser to get Thymeleaf pages are secured with sessions, not JWTs. Check with your browser development tools, you’ll find session cookies but no
Authorization
header with aBearer
JWT.Also, the type of
Authentication
in the security context is specific to OAuth2 clients: look at your logs, it’s anOAuth2AuthenticationToken
instance. For a resource server with a JWT decoder, you should have aJwtAuthenticationToken
instance (unless you explicitly configure something else like I do in some of my tutorials).Logout Handler
Keycloak complies with RP-Initiated Logout and
spring-security-oauth2-client
provides withOidcClientInitiatedLogoutSuccessHandler
(OidcClientInitiatedServerLogoutSuccessHandler
for webflux apps). Just use that one.Actions You Should Take
Depending on your use case:
securityMatcher
to separate it as explained in my answer to this question: Use Keycloak Spring Adapter with Spring Boot 3spring-cloud-gateway
configured as OAuth2 confidential client (with login and logout) and with theTokenRelay
filter (I have a tutorial on that subject in the collection already linked above)