Spring 4.x token-based WebSocket SockJS fallback authentication [duplicate] - spring

This question already has answers here:
JSON Web Token (JWT) with Spring based SockJS / STOMP Web Socket
(5 answers)
Closed 6 years ago.
Getting Spring Boot 1.4 + Spring 4.x + Spring Security 4.x WebSocket authentication to work with stateless token-based authentication seems to be an adventure!
So far, as I understand it, SockJS is not able to set the Authentication header with the token because browser's do not expose that API to Javascript (see https://github.com/sockjs/sockjs-client/issues/196). I have worked around that by setting the authentication token in a query parameter as suggested on the above issue, and then using a Spring HandshakeHandler with determineUser() to map the query parameter to a User entity. Ugly and less secure, but at least it works for WebSockets.
However, when SockJS falls back to another mechanism e.g. XHR streaming, the same mechanism no longer works. A HandshakeInterceptor has access to the request and can obtain the authentication from the query param, but determineUser on the HandshakeHandler is never called for non-WebSocket handshakes.
The closest I have gotten so far is to bypass the built-in connection-level Spring machinery to determine the authentication. Instead, I set the authentication token at the message-level by setting it in the Stomp headers on the client side e.g.:
stompClient.send("/wherever", {token: 'token'}, ...);
and extract it on the server-side with a channel interceptor:
configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
Message<*> preSend(Message<*> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
// not documented anywhere but necessary otherwise NPE in StompSubProtocolHandler!
accessor.setLeaveMutable(true);
List tokenList = accessor.getNativeHeader("token");
if(tokenList == null || tokenList.size < 1) {
return message;
}
Principal yourAuth = [...];
return MessageBuilder.createMessage(message.payload, accessor.messageHeaders)
}
})
Now Spring injects the Principal into any controller methods that require it, BUT the user is still not saved to the websocket session, so messages still cannot be sent to a particular user.
How do I get Spring to "see" the authentication extracted from the query parameter?

Use the Stomp Headers
Bypass the built-in connection-level Spring machinery to determine the authentication. Instead, set the authentication token at the message-level by setting it in the Stomp headers on the client side. See the approach I outlined here:
https://stackoverflow.com/a/39456274/430128

Related

How to handle OAuth2 access token refresh with synchronous API calls, in Spring Security 5

We are using Spring Gateway (Spring Boot 2.4.6) which uses Spring Security 5 and the Weblux/ reactive model within that to provide OAuth2 security and Keycloak as the IDP.
Refreshing of the Access Token is an issue when our front-end application, which has already [successfully] authenticated against the gateway/ IDP, issues multiple API calls after the session's access token has expired.
It appears that out of (for example) five API calls, only the last one gets re-authenticated against the Keycloak provider and the other four get 'lost', thereby causing issues within the front-end.
If the user refreshes the UI's page then the proper authentication flow happens seamlessly and the token stored in the session is refreshed, without a redirect to the Keycloak login screen (as expected), therefore the problem is only with synchronous API calls.
The SecurityWebFilterChain is setup with:
/*
* Enable oauth2 authentication on all requests, but use our custom
* RegistrationRepository
*/
.and()
.oauth2Login()
.authenticationSuccessHandler(new AuthSuccessHandler(requestCache)) // handle success login
.authenticationFailureHandler((exchange, excep) -> {
LOGGER.debug("Authentication failure: {}", excep.getMessage());
return Mono.empty();
})
.clientRegistrationRepository(clientReg);
// Add our custom filter to the security chain
final KeycloakClientLoginFilter keyclockLogin = new KeycloakClientLoginFilter(
clientReg,
redirectStrategy,
requestCache,
authClientService);
clientReg.setKeycloakClientLoginFilter(keyclockLogin);
http.addFilterBefore(keyclockLogin, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
return http.build();
With the ServerAuthenticationSuccessHandler configured with this:
private class AuthSuccessHandler implements ServerAuthenticationSuccessHandler {
private final ServerRequestCache requestCache;
private final URI defaultLocation = URI.create("/login");
private AuthSuccessHandler(ServerRequestCache requestCache) {
this.requestCache = requestCache;
}
#Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
final ServerWebExchange exchange = webFilterExchange.getExchange();
return requestCache.getRedirectUri(exchange)
.defaultIfEmpty(defaultLocation)
.flatMap(location -> {
LOGGER.debug("Authentication success. Redirecting request to {}", location.toASCIIString());
return redirectStrategy.sendRedirect(exchange, location);
});
}
}
Within the KeycloakClientLoginFilter there is a ServerWebExchangeMatcher that checks if the required details are present on the inbound exchange, and whether the AccessToken has (or is about to) expire. If it is, it runs through this code to redirect the request off to Keycloak for authentication and/ or refresh:
final ClientRegistration keycloakReg = clientReg.getRegistration(tenantId, appId);
if (!isError && loginRedirects.containsKey(keycloakReg.getRegistrationId())) {
final String contextPath = exchange.getRequest().getPath().contextPath().value();
final URI redirect = URI.create(contextPath + loginRedirects.get(keycloakReg.getRegistrationId()));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("About to redirect to keycloak; for method {}, tenant={}",
exchange.getRequest().getMethod(),
tenantId);
}
// Save the request so the URL can be retreived on successful login
return requestCache.saveRequest(exchange)
.then(redirectStrategy.sendRedirect(exchange, redirect));
}
So, all API calls hit the above code, require a refresh, have their original exchanges saved in the requestCache and are then directed to Keycloak. When Keycloak responds with the updated token, the exchange(s) run through the AuthSuccessHandler, which pulls the original request URL from the requestCache and redirects the call to that original URL.
This part works for web requests and the one in five API calls.
The other four API calls never make it to the AuthSuccessHandler - They simply get 'lost'.
There are some ugly hacks that could be done, like blocking all calls until the one first one is re-authenticated, but that just isn't right and would be hard to get right anyway.
So can the gateway, CookieServerRequestCache or AuthenticationWebFilter only handle one request at a time? Is there a 'simple' implementation of waiting on one call from the same session to re-authenticate?
Any help would be greatly appreciated as the application simply doesn't work (from a user's perspective) until this is resolved.
I know quite some tutorials do so, but in my opinion, authenticating against the gateway is a mistake (see this answer for details why). Why not using an OAuth2 client library on your client(s) instead?
I personnaly use angular-auth-oidc-client, and I am convinced that there must be equivalents for React, Vue, Flutter, Android or iOS.
Such libraries can handle access-tokens refreshing for you (provided that you requested the offline_access scope and that the authorization-server supports refresh-token for your client).
Authenticate users on the client(s) with the help of a certified lib, have your gateway just forward Authorization header and configure your micro-services as resource-servers.

GraphQL Subscriptions - Spring Boot Websocket Authentication

We are using the Netflix DGS framework to build our backend to provide a GraphQL API.
In addition to that we use Keykloak as an identity provider which comes with a handy Spring module to add support for authentication and authorization out of the box.
Every request contains a JWT token, which gets validated and from there a SecurityContext object is being generated which is then available in every endpoint.
This is working great for HTTP requests. GraphQL queries and mutations are sent via HTTP, therefore no problem here.
Subscriptions on the other hand use the web socket protocol. A WS request does not contain additional headers, therefore no JWT token is sent with the request.
We can add the token via a payload, the question is now how to set up a Spring Security Filter which creates a Security Context out of the payload.
I guess this is rather Spring specific, basically a filter which intercepts any web socket request (ws://... or wss://...) is needed.
Any help or hint is very much appreciated!
The only way to use headers in web socket messages is in the connection_init message. the headers will be sent by the client in the payload of the message.
The solution I propose is done in 2 steps (We will assume that the name of the header element is "token"):
Intercept the connection_init message, then force the insertion of a new element (token) in the subscription request.
Retrieve the element (token) of the header during the interception of the subscription and feed the context.
Concretely, the solution is the implementation of WebSocketGraphQlInterceptor interface
#Configuration
class SubscriptionInterceptor implements WebSocketGraphQlInterceptor {
#Override
public Mono<Object> handleConnectionInitialization(WebSocketSessionInfo sessionInfo, Map<String, Object> connectionInitPayload) {
sessionInfo.getHeaders().add("token", connectionInitPayload.get("token").toString());
return Mono.just(connectionInitPayload);
}
#Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
List<String> token = request.getHeaders().getOrEmpty("token");
return chain.next(request).contextWrite(context -> context. Put("token", token.isEmpty() ? "" : token.get(0)));
}
}

Spring Authorization Server: How to use login form hosted on a separate application?

I am using Spring Security along with Spring Authorization Server and experimenting with creating an auth server.
I have a basic flow allowing me to login with the pre-built login page (from a baledung guide - this is the code I'm working off ). I'm assuming this login page form comes from formLogin() like so:
http.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
//.formLogin(withDefaults());
return http.build();
I would like to not use this pre-built form as I have a need to host and run the login form front-end application completely separately. ie on a different server, domain and codebase.
Another way to ask this question could be:
How do I disable the built in form in authorization-server so I can use it with a completely separate form?
Are there any recommended ways of learning about how customise my SecurityFilterChain along these lines? Is this the correct place to look? I find the baledung article (and articles like that) helpful as a starting point, but seldom works for more practical use case. I'm confident Spring Security and the oauth2 libraries will allow me to do what I want, but not entirely clear.
After discussing this with you, I've gathered that what you're trying to do is essentially pre-authenticate the user that was authenticated through another (separately hosted) login page, actually a separate system. The idea is that the other system would redirect back with a signed JWT in a query parameter.
This really becomes more of a federated login problem at that point, which is what SAML 2.0 and OAuth 2.0 are aimed at solving. However, if you have to stick with things like a signed JWT (similar to a SAML assertion), we could model a fairly simple pre-authenticated authorization_code flow using the Spring Authorization Server.
Note: I haven't explored options for JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants but it could be a viable alternative. See this issue (#59).
Additional note: There are numerous security considerations involved with the approach outlined below. What follows is a sketch of the approach. Additional considerations include CSRF protection, using Form Post Response Mode (similar to SAML 2.0) to protect the access token instead of a query parameter, aggressively expiring the access token (2 minutes or less), and others. In other words, using a federated login approach like SAML 2.0 or OAuth 2.0 will always be RECOMMENDED over this approach when possible.
You could to start with the existing Spring Authorization Server sample and evolve it from there.
Here's a variation that redirects to an external authentication provider and includes a pre-authentication mechanism on the redirect back:
#Bean
#Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// #formatter:off
http
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("https://some-other-sso.example/login"))
);
// #formatter:on
return http.build();
}
#Bean
#Order(2)
public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception {
// #formatter:off
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
// #formatter:on
return http.build();
}
#Bean
public JwtDecoder jwtDecoder(PublicKey publicKey) {
return NimbusJwtDecoder.withPublicKey((RSAPublicKey) publicKey).build();
}
#Bean
public BearerTokenResolver bearerTokenResolver() {
DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
bearerTokenResolver.setAllowUriQueryParameter(true);
return bearerTokenResolver;
}
The first filter chain operates on authorization server endpoints, such as /oauth2/authorize, /oauth2/token, etc. Note that the /oauth2/authorize endpoint requires an authenticated user to function, meaning that if the endpoint is invoked, the user has to be authenticated, or else the authentication entry point is invoked, which redirects to the external provider. Also note that there must be a trusted relationship between the two parties, since we're not using OAuth for the external SSO.
When a redirect from the oauth client comes to the /oauth2/authorize?... endpoint, the request is cached by Spring Security so it can be replayed later (see controller below).
The second filter chain authenticates a user with a signed JWT. It also includes a customized BearerTokenResolver which reads the JWT from a query parameter in the URL (?access_token=...).
The PublicKey injected into the JwtDecoder would be from the external SSO provider, so you can plug that in however it makes sense to in your setup.
We can create a stub authentication endpoint that converts the signed JWT into an authenticated session on the authorization server, like this:
#Controller
public class SsoController {
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
#GetMapping("/login")
public void login(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
this.successHandler.onAuthenticationSuccess(request, response, authentication);
}
}
The .oauth2ResourceServer() DSL causes the user to be authenticated when the /login endpoint is invoked. It requires an access_token parameter (used by the BearerTokenResolver) to pre-authenticate the user by validating the signed JWT as an assertion that the user has been externally authenticated. At this point, a session is created that will authenticate all future requests by this browser.
The controller is then invoked, and simply redirects back to the real authorization endpoint using the SavedRequestAwareAuthenticationSuccessHandler, which will happily initiate the authorization_code flow.
Re your comnent: "I'm attempting to build an Authorization Server":
Coding your own Authorization Server (AS) or having to build its code yourself is highly inadvisable, since it is easy to get bogged down in plumbing or to make security mistakes.
By all means use Spring OAuth Security in your apps though. It is hard enough to get these working as desired, without taking on extra work.
SUGGESTED APPROACH
Choose a free AS and run it as a Docker Container, then connect to its endpoints from your apps.
If you need to customize logins, use a plugin model, write a small amount of code, then deploy a JAR file or two to the Docker container.
This will get you up and running very quickly. Also, since Spring Security is standards based, you are free to change your mind about providers, and defer decisions on the final one.
EXAMPLE IMPLEMENTATION
Curity, along with other good choices like Keycloak or Ory Hydra are Java based and support plugins:
Curity Community Edition
Custom Authenticator Example

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);
}

Spring Security Saml forceAuthn specific requests

Is there a way in spring security Saml to set forceAuthn=true based on request parameters.
https://stackoverflow.com/a/27833355/2587871 override the value based on the request ?
Basically as a Service provide we work with both Oauth and SAML idp. So when the client send prompt=login we want to honor it in SAML side too.
I was able to implement this using the following option.
Extend SAMLEntryPoint
Override getProfileOptions
WebSSOProfileOptions webProfile = super.getProfileOptions(context, exception);
if(prompt=login){
webProfile.setForceAuthN(true);
}
return webProfile;

Resources