Spring Authorization Server 0.2.2, how to disable a default authentication provider like (OAuth2TokenRevocation) and override it with a custom one? - spring-boot

I am using the new Spring Authorization Server 0.2.2 and I want to change the logic of the OAuth2TokenRevocationAuthenticationProvider and make my own implementation for the Token Revocation endpoint.
I added a new CustomRevocationAuthenticationProvider
public class CustomRevocationAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//My implementation
try {
//My implementation
} catch (Exception e) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
//My implementation
}
#Override
public boolean supports(Class<?> authentication) {
return OAuth2TokenRevocationAuthenticationToken.class.isAssignableFrom(authentication);
}
and I added this provider to the SecurityFilterChain like this:
#Bean
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults())
.authenticationProvider(new CustomRevocationAuthenticationProvider())
.build();
}
It works good but when I throw a OAuth2AuthenticationException in my implementation, the default OAuth2TokenRevocationAuthenticationProvider get executed and return 200 OK response.
is there any way to disable the default oauth2 provider from handling my exception and getting executed?

Great question. Since we're working on reference documentation, this is a good topic and I'll make a note to cover it in the configuration overview.
Take a look at OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http). When customizing Spring Authorization Server, you will typically need to copy that code and use the configurer directly. Here's an example:
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
authorizationServerConfigurer.tokenRevocationEndpoint(tokenRevocationEndpoint -> tokenRevocationEndpoint
.authenticationProvider(new CustomRevocationAuthenticationProvider())
);
// ...
http.apply(authorizationServerConfigurer);

Related

Spring oauth2login oidc grant access based on user info

I'm trying to set up Authentication based on this tutorial: https://www.baeldung.com/spring-security-openid-connect part 7 specifically.
I have filled properties and configured filter chain like this:
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.anyRequest().authenticated())
.oauth2Login(oauthLogin -> oauthLogin.permitAll());
return http.build();
}
which works, but now all users from oidc can connect log in. I want to restrict access based on userinfo. E.g. add some logic like:
if(principal.getName() == "admin") {
//allow authentication
}
are there any way to do it?
I tried to create customer provider like suggested here: Add Custom AuthenticationProvider to Spring Boot + oauth +oidc
but it fails with exception and says that principal is null.
You can retrieve user info when authentication is successful and do further checks based user info.
Here is sample code that clears security context and redirects the request:
#Component
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
if(authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
// OidcUser or OAuth2User
// OidcUser user = (OidcUser) token.getPrincipal();
OAuth2User user = token.getPrincipal();
if(!user.getName().equals("admin")) {
SecurityContextHolder.getContext().setAuthentication(null);
SecurityContextHolder.clearContext();
redirectStrategy.sendRedirect(request, response, "login or error page url");
}
}
}
}
Are you sure that what you want to secure does not include #RestController or #Controller with #ResponseBody? If so, the client configuration you are referring to is not adapted: you need to setup resource-server configuration for this endpoints.
I wrote a tutorial to write apps with two filter-chains: one for resource-server and an other one for client endpoints.
The complete set of tutorials the one linked above belongs to explains how to achieve advanced access-control on resource-server. Thanks to the userAuthoritiesMapper configured in resource-server_with_ui, you can write the same security expressions based on roles on client controller methods as I do on resource-server ones.

Spring Boot REST API disable Form Login redirect

I have been banging my head against the wall for hours trying to figure out something that I would expect to work out of the box these days.
I am building an API with Spring Boot backend and I will create a react front end.
I only have one server so I dont need to use tokens. I want the same normal server side sessions and cookies.
I managed to get the Authentication to work but for some reason it keeps redirecting after success to the default / endpoint.
I really do not want this to happen and can't figure out why this is the case. I also can't find any decent resources on the internet of people that have encountered this issue.
I have seen a few videos where I have seen people handling the login in a Rest Controller end point rather than using filters. I assume this could work but then how would I implement session management?
Here is the code so far:
#Configuration
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Autowired
private AuthUserService authUserService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(authUserService);
}
#Bean
public CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/api/**").hasRole("AUTH_USER")
.mvcMatchers("/**").permitAll();
http.cors();
http.addFilterAfter(new CsrfHandlerFilter(), CsrfFilter.class);
AuthenticationFilter filter = new AuthenticationFilter();
filter.setAuthenticationManager(authenticationManager());
http.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class);
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
Authentication Filter:
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public AuthenticationFilter(){
super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("Custom Authentication Filter fired!");
ObjectMapper mapper = new ObjectMapper();
Login login = new Login();
try {
login = mapper.readValue(request.getInputStream(), Login.class);
} catch (StreamReadException e) {
e.printStackTrace();
} catch (DatabindException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
login.getUsername(),
login.getPassword()
);
return this.getAuthenticationManager().authenticate(token);
}
}
Login Model class:
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Login {
private String username;
private String password;
}
I want a normal server side session. I am not using JWT just because it is a JavaScript client. But all I want is for it to not redirect. Is this possible?
Any advice would be appreciated
There are a few ways to approach this, depending on your preference.
Certainly, you can stand up your own Spring MVC endpoint and set the SecurityContext yourself. Spring Security's SecurityContextPersistenceFilter will store the SecurityContext in an HttpSessionSecurityContextRepository by default, which induces the container to write a JSESSIONID session cookie that can be used on subsequent requests.
The main reason to go this route is if you want to have access to the MVC feature set when writing this endpoint.
One downside of this route is that Spring Security 6 will no longer save the security context for you when it comes to custom MVC endpoints, so you would need to be aware of that when upgrading.
HTTP Basic
That said, it doesn't seem like your requirements are so sophisticated that you can't use Spring Security's OOTB behavior.
One way to do this is with HTTP Basic. Note that for simplicity, I'll publish the SecurityFilterChain as a #Bean instead of using the now-deprecated WebSecurityConfigurerAdapter:
#Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.mvcMatchers("/api/**").hasRole("AUTH_USER")
.mvcMatchers("/**").permitAll()
)
.httpBasic(Customizer.withDefaults())
.cors(Customizer.witHDefaults())
.addFilterAfter(new CsrfHandlerFilter(), CsrfFilter.class);
return http.build();
}
This will allow you to send the username/password using the Authorization: Basic header. There's no need in this case for you to stand up anything custom. The filter chain will store the security
context in the session, and your Javascript can call endpoints using the JSESSIONID or by resending the username/password creds.
AuthenticationSuccessHandler
If for some reason you want to use form login (what your sample is customizing right now), instead of creating a custom filter, you can configure the existing form login filter with an AuthenticationSuccessHandler that does not redirect:
#Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.mvcMatchers("/api/**").hasRole("AUTH_USER")
.mvcMatchers("/**").permitAll()
)
.formLogin((form) -> form
.successHandler((request, response, authentication) ->
response.setStatusCode(200)
)
)
.cors(Customizer.witHDefaults())
.addFilterAfter(new CsrfHandlerFilter(), CsrfFilter.class);
return http.build();
}
Once again, the filter chain will save the subsequent UsernamePasswordAuthenticationToken to the session and issue a JSESSIONID for subsequent requests.

Throw custom exception from AuthenticationProvider to client(spring security)

I develop a spring boot REST service. I use #ControllerAdvice for exception catching. Also, I have a custom AuthenticationProvider and check a license in it.
#Component
public class MyAuthenticationProvider implements AuthenticationProvider {
private final LicenseService licenseService;
public MyAuthenticationProvider(LicenseService licenseService) {
this.licenseService = licenseService;
}
#Override
public boolean supports(Class<?> authentication) {
//...some code
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try {
//...some code
licenseService.checkLicense(userDetails.getSomeId(), LicenseCode.FOO);
//..some code
} catch (LicenseException error) {
throw new AccessDeniedException(error.getMessage());
}
}
}
My licenseService throws LicenseException if the license does not exist or incorrect. I catch it and wrap to AccessDeniedException
at first, I wanted to catch LicenseException in #ControllerAdvice but quickly understood that it could be wrong. #ControllerAdvice catches exceptions in the controller layer.
That is why I wrap my exception to AccessDeniedException. But I want another logic: I want to throw a custom exception to the frontend. The frontend must understand this exception and show a special dialog to the client(License is required... bla-bla-bla). But I don't know how to do it on this step(AuthenticationProvider)
Your Controller catching the exception from your AuthenticationProvider would send a ResponseEntity with HttpStatus.FORBIDDEN to your client. You can wrap your exception or an errormessage in the body of the HTTP response.

Spring security java config basic authentication filter

I am trying to add basic authentication for my APIs where the users will be authenticated based on their credential stored in MongoDB. I want to use java config instead of XML based config. So far what I have learnt is I have to create #Configuration by extending WebSecurityConfigurerAdapter and override configure method. In that I can add a custom filter by addFilterBefore().
But how can I get a Authentication header information in the filter, how to proceed if the user is authenticated. I have been googling a lot but didn't find any good example that will help a novice like me whose been into spring just for 1 week.
Does any one have a good tutorial or sample that can help me get started with this? Thanks in advance.
As example, you can use next solution.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().authenticationEntryPoint(getBasicAuthenticationEntryPoint());
}
#Bean
public BasicAuthenticationEntryPoint getBasicAuthenticationEntryPoint(){
BasicAuthenticationEntryPoint basicAuthenticationEntryPoint = new BasicAuthenticationEntryPoint();
basicAuthenticationEntryPoint.setRealmName("Basic Authentication");
return basicAuthenticationEntryPoint;
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
It works for me. But you need to implement UserDetailsService interface.
Spring automatically checks is user authenticated and tries to proceed authentication if not.

JSON Web Token (JWT) with Spring based SockJS / STOMP Web Socket

Background
I am in the process of setting up a RESTful web application using Spring Boot (1.3.0.BUILD-SNAPSHOT) that includes a STOMP/SockJS WebSocket, which I intend to consume from an iOS app as well as web browsers. I want to use JSON Web Tokens (JWT) to secure the REST requests and the WebSocket interface but I’m having difficulty with the latter.
The app is secured with Spring Security:-
#Configuration
#EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
public WebSecurityConfiguration() {
super(true);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("steve").password("steve").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling().and()
.anonymous().and()
.servletApi().and()
.headers().cacheControl().and().and()
// Relax CSRF on the WebSocket due to needing direct access from apps
.csrf().ignoringAntMatchers("/ws/**").and()
.authorizeRequests()
//allow anonymous resource requests
.antMatchers("/", "/index.html").permitAll()
.antMatchers("/resources/**").permitAll()
//allow anonymous POSTs to JWT
.antMatchers(HttpMethod.POST, "/rest/jwt/token").permitAll()
// Allow anonymous access to websocket
.antMatchers("/ws/**").permitAll()
//all other request need to be authenticated
.anyRequest().hasRole("USER").and()
// Custom authentication on requests to /rest/jwt/token
.addFilterBefore(new JWTLoginFilter("/rest/jwt/token", authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)
// Custom JWT based authentication
.addFilterBefore(new JWTTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
The WebSocket configuration is standard:-
#Configuration
#EnableScheduling
#EnableWebSocketMessageBroker
public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
}
I also have a subclass of AbstractSecurityWebSocketMessageBrokerConfigurer to secure the WebSocket:-
#Configuration
public class WebSocketSecurityConfiguration extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.anyMessage().hasRole("USER");
}
#Override
protected boolean sameOriginDisabled() {
// We need to access this directly from apps, so can't do cross-site checks
return true;
}
}
There is also a couple of #RestController annotated classes to handle various bits of functionality and these are secured successfully via the JWTTokenFilter registered in my WebSecurityConfiguration class.
Problem
However I can't seem to get the WebSocket to be secured with JWT. I am using SockJS 1.1.0 and STOMP 1.7.1 in the browser and can't figure out how to pass the token. It would appear that SockJS does not allow parameters to be sent with the initial /info and/or handshake requests.
The Spring Security for WebSockets documentation states that the AbstractSecurityWebSocketMessageBrokerConfigurer ensures that:
Any inbound CONNECT message requires a valid CSRF token to enforce Same Origin Policy
Which seems to imply that the initial handshake should be unsecured and authentication invoked at the point of receiving a STOMP CONNECT message. Unfortunately I can't seem to find any information with regards to implementing this. Additionally this approach would require additional logic to disconnect a rogue client that opens a WebSocket connection and never sends a STOMP CONNECT.
Being (very) new to Spring I'm also not sure if or how Spring Sessions fits into this. While the documentation is very detailed there doesn't appear to a nice and simple (aka idiots) guide to how the various components fit together / interact with each other.
Question
How do I go about securing the SockJS WebSocket by providing a JSON Web Token, preferably at the point of handshake (is it even possible)?
Current Situation
UPDATE 2016-12-13 : the issue referenced below is now marked fixed, so the hack below is no longer necessary which Spring 4.3.5 or above. See https://github.com/spring-projects/spring-framework/blob/master/src/docs/asciidoc/web/websocket.adoc#token-authentication.
Previous Situation
Currently (Sep 2016), this is not supported by Spring except via query parameter as answered by #rossen-stoyanchev, who wrote a lot (all?) of the Spring WebSocket support. I don't like the query parameter approach because of potential HTTP referrer leakage and storage of the token in server logs. In addition, if the security ramifications don't bother you, note that I have found this approach works for true WebSocket connections, but if you are using SockJS with fallbacks to other mechanisms, the determineUser method is never called for the fallback. See Spring 4.x token-based WebSocket SockJS fallback authentication.
I've created a Spring issue to improve support for token-based WebSocket authentication: https://jira.spring.io/browse/SPR-14690
Hacking It
In the meantime, I've found a hack that works well in testing. Bypass the built-in Spring connection-level Spring auth machinery. Instead, set the authentication token at the message-level by sending it in the Stomp headers on the client side (this nicely mirrors what you are already doing with regular HTTP XHR calls) e.g.:
stompClient.connect({'X-Authorization': 'token'}, ...);
stompClient.subscribe(..., {'X-Authorization': 'token'});
stompClient.send("/wherever", {'X-Authorization': 'token'}, ...);
On the server-side, obtain the token from the Stomp message using a ChannelInterceptor
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
Message<*> preSend(Message<*> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
List tokenList = accessor.getNativeHeader("X-Authorization");
String token = null;
if(tokenList == null || tokenList.size < 1) {
return message;
} else {
token = tokenList.get(0);
if(token == null) {
return message;
}
}
// validate and convert to a Principal based on your own requirements e.g.
// authenticationManager.authenticate(JwtAuthentication(token))
Principal yourAuth = [...];
accessor.setUser(yourAuth);
// not documented anywhere but necessary otherwise NPE in StompSubProtocolHandler!
accessor.setLeaveMutable(true);
return MessageBuilder.createMessage(message.payload, accessor.messageHeaders)
}
})
This is simple and gets us 85% of the way there, however, this approach does not support sending messages to specific users. This is because Spring's machinery to associate users to sessions is not affected by the result of the ChannelInterceptor. Spring WebSocket assumes authentication is done at the transport layer, not the message layer, and thus ignores the message-level authentication.
The hack to make this work anyway, is to create our instances of DefaultSimpUserRegistry and DefaultUserDestinationResolver, expose those to the environment, and then use the interceptor to update those as if Spring itself was doing it. In other words, something like:
#Configuration
#EnableWebSocketMessageBroker
#Order(HIGHEST_PRECEDENCE + 50)
class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer() {
private DefaultSimpUserRegistry userRegistry = new DefaultSimpUserRegistry();
private DefaultUserDestinationResolver resolver = new DefaultUserDestinationResolver(userRegistry);
#Bean
#Primary
public SimpUserRegistry userRegistry() {
return userRegistry;
}
#Bean
#Primary
public UserDestinationResolver userDestinationResolver() {
return resolver;
}
#Override
public configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue", "/topic");
}
#Override
public registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/stomp")
.withSockJS()
.setWebSocketEnabled(false)
.setSessionCookieNeeded(false);
}
#Override public configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
Message<*> preSend(Message<*> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
List tokenList = accessor.getNativeHeader("X-Authorization");
accessor.removeNativeHeader("X-Authorization");
String token = null;
if(tokenList != null && tokenList.size > 0) {
token = tokenList.get(0);
}
// validate and convert to a Principal based on your own requirements e.g.
// authenticationManager.authenticate(JwtAuthentication(token))
Principal yourAuth = token == null ? null : [...];
if (accessor.messageType == SimpMessageType.CONNECT) {
userRegistry.onApplicationEvent(SessionConnectedEvent(this, message, yourAuth));
} else if (accessor.messageType == SimpMessageType.SUBSCRIBE) {
userRegistry.onApplicationEvent(SessionSubscribeEvent(this, message, yourAuth));
} else if (accessor.messageType == SimpMessageType.UNSUBSCRIBE) {
userRegistry.onApplicationEvent(SessionUnsubscribeEvent(this, message, yourAuth));
} else if (accessor.messageType == SimpMessageType.DISCONNECT) {
userRegistry.onApplicationEvent(SessionDisconnectEvent(this, message, accessor.sessionId, CloseStatus.NORMAL));
}
accessor.setUser(yourAuth);
// not documented anywhere but necessary otherwise NPE in StompSubProtocolHandler!
accessor.setLeaveMutable(true);
return MessageBuilder.createMessage(message.payload, accessor.messageHeaders);
}
})
}
}
Now Spring is fully aware of the the authentication i.e. it injects the Principal into any controller methods that require it, exposes it to the context for Spring Security 4.x, and associates the user to the WebSocket session for sending messages to specific users/sessions.
Spring Security Messaging
Lastly, if you use Spring Security 4.x Messaging support, make sure to set the #Order of your AbstractWebSocketMessageBrokerConfigurer to a higher value than Spring Security's AbstractSecurityWebSocketMessageBrokerConfigurer (Ordered.HIGHEST_PRECEDENCE + 50 would work, as shown above). That way, your interceptor sets the Principal before Spring Security executes its check and sets the security context.
Creating a Principal (Update June 2018)
Lots of people seem to be confused by this line in the code above:
// validate and convert to a Principal based on your own requirements e.g.
// authenticationManager.authenticate(JwtAuthentication(token))
Principal yourAuth = [...];
This is pretty much out of scope for the question as it is not Stomp-specific, but I'll expand on it a little bit anyway, because its related to using auth tokens with Spring. When using token-based authentication, the Principal you need will generally be a custom JwtAuthentication class that extends Spring Security's AbstractAuthenticationToken class. AbstractAuthenticationToken implements the Authentication interface which extends the Principal interface, and contains most of the machinery to integrate your token with Spring Security.
So, in Kotlin code (sorry I don't have the time or inclination to translate this back to Java), your JwtAuthentication might look something like this, which is a simple wrapper around AbstractAuthenticationToken:
import my.model.UserEntity
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.core.GrantedAuthority
class JwtAuthentication(
val token: String,
// UserEntity is your application's model for your user
val user: UserEntity? = null,
authorities: Collection<GrantedAuthority>? = null) : AbstractAuthenticationToken(authorities) {
override fun getCredentials(): Any? = token
override fun getName(): String? = user?.id
override fun getPrincipal(): Any? = user
}
Now you need an AuthenticationManager that knows how to deal with it. This might look something like the following, again in Kotlin:
#Component
class CustomTokenAuthenticationManager #Inject constructor(
val tokenHandler: TokenHandler,
val authService: AuthService) : AuthenticationManager {
val log = logger()
override fun authenticate(authentication: Authentication?): Authentication? {
return when(authentication) {
// for login via username/password e.g. crash shell
is UsernamePasswordAuthenticationToken -> {
findUser(authentication).let {
//checkUser(it)
authentication.withGrantedAuthorities(it).also { setAuthenticated(true) }
}
}
// for token-based auth
is JwtAuthentication -> {
findUser(authentication).let {
val tokenTypeClaim = tokenHandler.parseToken(authentication.token)[CLAIM_TOKEN_TYPE]
when(tokenTypeClaim) {
TOKEN_TYPE_ACCESS -> {
//checkUser(it)
authentication.withGrantedAuthorities(it).also { setAuthenticated(true) }
}
TOKEN_TYPE_REFRESH -> {
//checkUser(it)
JwtAuthentication(authentication.token, it, listOf(SimpleGrantedAuthority(Authorities.REFRESH_TOKEN)))
}
else -> throw IllegalArgumentException("Unexpected token type claim $tokenTypeClaim.")
}
}
}
else -> null
}
}
private fun findUser(authentication: JwtAuthentication): UserEntity =
authService.login(authentication.token) ?:
throw BadCredentialsException("No user associated with token or token revoked.")
private fun findUser(authentication: UsernamePasswordAuthenticationToken): UserEntity =
authService.login(authentication.principal.toString(), authentication.credentials.toString()) ?:
throw BadCredentialsException("Invalid login.")
#Suppress("unused", "UNUSED_PARAMETER")
private fun checkUser(user: UserEntity) {
// TODO add these and lock account on x attempts
//if(!user.enabled) throw DisabledException("User is disabled.")
//if(user.accountLocked) throw LockedException("User account is locked.")
}
fun JwtAuthentication.withGrantedAuthorities(user: UserEntity): JwtAuthentication {
return JwtAuthentication(token, user, authoritiesOf(user))
}
fun UsernamePasswordAuthenticationToken.withGrantedAuthorities(user: UserEntity): UsernamePasswordAuthenticationToken {
return UsernamePasswordAuthenticationToken(principal, credentials, authoritiesOf(user))
}
private fun authoritiesOf(user: UserEntity) = user.authorities.map(::SimpleGrantedAuthority)
}
The injected TokenHandler abstracts away the JWT token parsing, but should use a common JWT token library like jjwt. The injected AuthService is your abstraction that actually creates your UserEntity based on the claims in the token, and may talk to your user database or other backend system(s).
Now, coming back to the line we started with, it might look something like this, where authenticationManager is an AuthenticationManager injected into our adapter by Spring, and is an instance of CustomTokenAuthenticationManager we defined above:
Principal yourAuth = token == null ? null : authenticationManager.authenticate(new JwtAuthentication(token));
This principal is then attached to the message as described above. HTH!
With the latest SockJS 1.0.3 you can pass query parameters as a part of connection URL. Thus you can send some JWT token to authorize a session.
var socket = new SockJS('http://localhost/ws?token=AAA');
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
stompClient.subscribe('/topic/echo', function(data) {
// topic handler
});
}
}, function(err) {
// connection error
});
Now all the requests related to websocket will have parameter "?token=AAA"
http://localhost/ws/info?token=AAA&t=1446482506843
http://localhost/ws/515/z45wjz24/websocket?token=AAA
Then with Spring you can setup some filter which will identify a session using provided token.
Seems like support for a query string was added to the SockJS client, see https://github.com/sockjs/sockjs-client/issues/72.
As of now, it is possible either to add auth token as a request parameter and handle it on a handshake, or add it as a header on a connection to stomp endpoint, and handle it on the CONNECT command in the interceptor.
Best thing would be to use header, but the problem is that you can't access native header on the handshake step, so you wouldn't be able to handle the auth there then.
Let me give some example code:
Config:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-test")
.setHandshakeHandler(new SecDefaultHandshakeHandler())
.addInterceptors(new HttpHandshakeInterceptor())
.withSockJS()
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new JwtChannelInterceptor())
}
}
Handshake interceptor:
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler, Map<String, Object> attributes) {
attributes.put("token", request.getServletRequest().getParameter("auth_token")
return true
}
}
Handshake handler:
public class SecDefaultHandshakeHandler extends DefaultHandshakeHandler {
#Override
public Principal determineUser(ServerHttpRequest request, WebSocketHandler handler, Map<String, Object> attributes) {
Object token = attributes.get("token")
//handle authorization here
}
}
Channel Interceptor:
public class JwtChannelInterceptor implements ChannelInterceptor {
#Override
public void postSend(Message message, MessageChannel channel, Boolean sent) {
MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class)
if (StompCommand.DISCONNECT == accessor.getCommand()) {
//retrieve Principal here via accessor.getUser()
//or get auth header from the accessor and handle authorization
}
}
}
Sorry for possible compile mistakes, I was converting manually from Kotlin code =)
As you mentioned that you have both web and mobile clients for your WebSockets, please mind that there are some difficulties maintaining same codebase for all clients. Please see my thread: Spring Websocket ChannelInterceptor not firing CONNECT event
I spend a lot of time to find simple solution. For me solution of Raman didn't work.
All you need is define custom bearerTokenResolver method and put access token into cookies or parameter.
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**")
.hasAuthority("SCOPE_read")
.antMatchers(HttpMethod.POST, "/api/foos")
.hasAuthority("SCOPE_write")
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer()
.jwt().and().bearerTokenResolver(this::tokenExtractor);
}
...
}
public String tokenExtractor(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header != null)
return header.replace("Bearer ", "");
Cookie cookie = WebUtils.getCookie(request, "access_token");
if (cookie != null)
return cookie.getValue();
return null;
}

Resources