How to protect the same resource using both spring-session and spring-security-oauth - spring-boot

I have a requirement to use two kinds of authentication,
for web we #EnableRedisHttpSession and for other consumers like mobile we use #EnableAuthorizationServer with #EnableResourceServer.
suppose we try to protect a controller common to both the authentication mechanisms for e.g /api/v1/test
i have hit a roadblock.
i am only able to use one kind of authentication scheme
if i set #WebSecurityConfigurerAdapter #order(2) and #ResourceServerConfigurerAdapter #order(3) then i can only access the resource via web
and if i set #ResourceServerConfigurerAdapter #order(2) and #WebSecurityConfigurerAdapter #order(3) then only OAuth works.
i am unable to use both the mechanism at the same time.how can we make the two work together, for e.g if the request comes from web use the filter responsible for that and if the request comes from mobile use the appropriate filter. web uses cookies and API Consumers use Authorization : Bearer header.
please help

It's sounds very strange. I suggest you to review how REST API is used and why it should be used by browser users. Better to separate web views and REST API, don't mix it.
However, answering your question "can I use two kinds of authentication for some URI at once" - yes, you can.
You need custom RequestMatcher that will decide how to route incoming request.
So:
for "API consumers" - check existence of Authorization header
contains "Bearer"
for "browser users" - just inverse first rule
Code example:
public abstract class AntPathRequestMatcherWrapper implements RequestMatcher {
private AntPathRequestMatcher delegate;
public AntPathRequestMatcherWrapper(String pattern) {
this.delegate = new AntPathRequestMatcher(pattern);
}
#Override
public boolean matches(HttpServletRequest request) {
if (precondition(request)) {
return delegate.matches(request);
}
return false;
}
protected abstract boolean precondition(HttpServletRequest request);
}
OAuth2 authentication
#EnableResourceServer
#Configuration
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatcher(new AntPathRequestMatcherWrapper("/api/v1/test") {
#Override
protected boolean precondition(HttpServletRequest request) {
return String.valueOf(request.getHeader("Authorization")).contains("Bearer");
}
}).authorizeRequests().anyRequest().authenticated();
}
}
Web authentication
#Configuration
#EnableWebSecurity
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatcher(new AntPathRequestMatcherWrapper("/api/v1/test") {
#Override
protected boolean precondition(HttpServletRequest request) {
return !String.valueOf(request.getHeader("Authorization")).contains("Bearer");
}
}).authorizeRequests().anyRequest().authenticated();
}
}
Using this configuration it's possible to use two different authentication types for one URI /api/v1/test.
In addition, I highly recommended to read the article about Spring Security architecture by Dave Syer, to understand how does it work:
https://spring.io/guides/topicals/spring-security-architecture/#_web_security

Related

REST API with both apiKey and username/password authentication

I'm a Spring's beginner and I'm trying to build a REST API, connected to a React frontend in order to learn these technologies.
In order to secure this API, I added an apiKey mechanism with Spring Security, by creating a filter that checks a specific header key (API-KEY in this case), and that only allows requests that match the correct api key value.
I added this filter in my security config, which extends WebSecurityConfigurerAdapter. However, I'd like to add another authentication mechanism just to authenticate my users, in a traditional username/password way. I'm a bit lost, I read a lot of articles but all of these are using the same mechanism (filter + configure the security component). But I really don't know how to gather these two mechanisms.
I would like all requests are intercepted to check the API-KEY value, but I also would like to have an anonymous and authenticated parts in my app.
How could I achieve this ? I found some elements like interceptors but it seems to be only available for spring-mvc app.
Here's the filter I'm using :
public class ApiKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
/**
* The request header we want to check with our apiKey
*/
private String principalRequestHeader;
public ApiKeyAuthFilter(String principalRequestHeader) {
this.principalRequestHeader = principalRequestHeader;
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
return request.getHeader(principalRequestHeader);
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return "N/A";
}
}
And here's my security config :
#Configuration
#EnableWebSecurity
public class ApiSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* The header corresponding to our apiKey
*/
#Value("${application.security.requestKey}")
private String apiKeyHeader;
/**
* The api key value we want to test with the header value
*/
#Value("${application.security.apiKey}")
private String apiKeyValue;
Logger logger = LoggerFactory.getLogger(ApiSecurityConfig.class);
#Override
protected void configure(HttpSecurity http) throws Exception {
ApiKeyAuthFilter filter = new ApiKeyAuthFilter(this.apiKeyHeader);
filter.setAuthenticationManager(new AuthenticationManager() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
final String principal = (String) authentication.getPrincipal();
if (!apiKeyValue.equals(principal)) {
throw new BadCredentialsException("The API key was not found or doesn't match the correct value");
}
logger.info("Connexion autorisée");
authentication.setAuthenticated(true);
return authentication;
}
});
http.cors().and().
antMatcher("/api/**").
csrf().disable().
sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
and().
addFilter(filter).
authorizeRequests().anyRequest().authenticated();
}
}
Do you have any clue to setup this kind of authentication ? I saw that we could define an order in our filter with methods like addFilterAfter() or addFilterBefore(), but I don't know how to setup this with my usecase.
I also found this post : How to config multiple level authentication for spring boot RESTful web service?
which seems to have the same requirements, I tried the solution provided but the authentication isn't dynamic (it's only using a string "valid-user" for its authentication filter, and I need to authenticate through my User entity stored in an in-memory h2 database. How to achieve this ?
Thank's a lot for your answers and have a nice day !

disable spring formlogin and basic auth

I have the following spring boot 2.0 config but I am still getting the basic auth login screen. I DO NOT want to disable all spring security like almost every post on the internet suggests. I only want to stop the form login page and basic auth so I can use my own.
I have seen all the suggestions with permitAll and exclude = {SecurityAutoConfiguration.class} and a few others that I can't remember anymore. Those are not what I want. I want to use spring security but I wan my config not Spring Boots. Yes I know many people are going to say this is a duplicate but I disagree because all the other answers are to disable spring security completely and not just stop the stupid login page.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class CustomSecurity extends WebSecurityConfigurerAdapter {
private final RememberMeServices rememberMeService;
private final AuthenticationProvider customAuthProvider;
#Value("${server.session.cookie.secure:true}")
private boolean useSecureCookie;
#Inject
public CustomSecurity(RememberMeServices rememberMeService, AuthenticationProvider customAuthProvider) {
super(true);
this.rememberMeService = rememberMeService;
this.bouncerAuthProvider = bouncerAuthProvider;
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v2/**").antMatchers("/webjars/**").antMatchers("/swagger-resources/**")
.antMatchers("/swagger-ui.html");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().disable().formLogin().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).headers().frameOptions().disable();
http.authenticationProvider(customAuthProvider).authorizeRequests().antMatchers("/health").permitAll()
.anyRequest().authenticated();
http.rememberMe().rememberMeServices(rememberMeService).useSecureCookie(useSecureCookie);
http.exceptionHandling().authenticationEntryPoint(new ForbiddenEntryPoint());
}
}
If you want to redirect to your own login page, i can show your sample code and configuration
remove the http.httpBasic().disable().formLogin().disable();, you should set your own login page to redirect instead of disable form login
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/my_login").permitAll().and().authorizeRequests().anyRequest().authenticated();
http.formLogin().loginPage("/my_login");
}
then create your own LoginController
#Controller
public class LoginController {
#RequestMapping("/my_login")
public ModelAndView myLogin() {
return new ModelAndView("login");
}
}
you can specified the login with thymeleaf view resolver

Can not retrieve Principal from Spring Okta (CRA SPA)

I am currently trying to test out Okta with SPA front end (Create-React-App) and a Spring Boot application.
Currently I have the apps working, in that a user logins on the front end (via okta). The user can then access protected resources from server (spring boot). Hence the integration works well and nice.
My issue is I can't access the Principal on my Rest Controller.
ENV
Note: Spring-Security-Starter is NOT on the classpath just the OAuth2 autoconf
Spring Boot 2.0.6.RELEASE
okta-spring-boot-starter:0.6.1
spring-security-oauth2-autoconfigure:2.0.6.RELEASE'
Spring Configuration
okta.oauth2.issuer=https://dev-886281.oktapreview.com/oauth2/default
okta.oauth2.clientId={ clientId }
okta.oauth2.audience=api://default
okta.oauth2.scopeClaim=scp
okta.oauth2.rolesClaim=groups
security.oauth2.resource.user-info-uri=https://dev-886281.oktapreview.com/oauth2/default/v1/userinfo
Okta Service Configuration
Application type: Single Page App (SPA)
Allowed grant types: Implicit
Allow Access Token with implicit grant type: true
Controller
#RestController
#RequestMapping("/products")
public class ProductController {
...
#GetMapping
public ResponseEntity<List<ProductEntity>> getAllProducts(Principal principal) {
SpringBoot
#EnableResourceServer
#SpringBootApplication
public class CartyApplication {
public static void main(String[] args) {
SpringApplication.run(CartyApplication.class, args);
}
#EnableGlobalMethodSecurity(prePostEnabled = true)
protected static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration {
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}
#Bean
protected ResourceServerConfigurerAdapter resourceServerConfigurerAdapter() {
return new ResourceServerConfigurerAdapter() {
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS,"/**").permitAll()
.anyRequest().authenticated();
}
};
}
Once again the overall integration is working fine, users can only access protected resources once they've signed in via okta, I'm just wondering how to get the users details from okta on the controller.
Thanks in advance.
P.S soz for the code dump
EDIT: Removed snippets and added full CartyApplication class
EDIT2: Added repo - https://github.com/Verric/carty-temp
I have a feeling you might be missing this:
#EnableGlobalMethodSecurity(prePostEnabled = true)
protected static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration {
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}
I'm guessing should remove the .antMatchers("/**").permitAll() line.
See: https://docs.spring.io/spring-security/site/docs/current/reference/html/jc.html#CO3-2
I'm guessing you want to protect all/most of your endpoints? I'd recommend only allowing specific routes, and protecting everything else.

Spring Boot 1.3.3 #EnableResourceServer and #EnableOAuth2Sso at the same time

I want my server be a ResourceServer, which can accept a Bearer Access token
However, If such token doesn't exist, I want to use the OAuth2Server to authenticate my user.
I try to do like:
#Configuration
#EnableOAuth2Sso
#EnableResourceServer
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
}
However, in this case, only the #EnableResourceServer annotation works. It returns
Full authentication is required to access this resource
And do not redirect me to the login page
I mentioned that the #Order is important, if I add the #Order(0) annotation,
I will be redirect to the login page, however, I cannot access my resource with the access_token in Http header:
Authorization : Bearer 142042b2-342f-4f19-8f53-bea0bae061fc
How can I achieve my goal? I want it use Access token and SSO at the same time.
Thanks~
Using both configuration on same request would be ambiguous. There could be some solution for that, but more clear to define separate request groups:
OAuth2Sso: for users coming from a browser, we want to redirect them to the authentication provider for the token
ResourceServer: usually for api requests, coming with a token they got from somewhere (most probably from same authentication provider)
For achieving this, separate the configurations with request matcher:
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Bean("resourceServerRequestMatcher")
public RequestMatcher resources() {
return new AntPathRequestMatcher("/resources/**");
}
#Override
public void configure(final HttpSecurity http) throws Exception {
http
.requestMatcher(resources()).authorizeRequests()
.anyRequest().authenticated();
}
}
And exclude these from the sso filter chain:
#Configuration
#EnableOAuth2Sso
public class SsoSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
#Qualifier("resourceServerRequestMatcher")
private RequestMatcher resources;
#Override
protected void configure(final HttpSecurity http) throws Exception {
RequestMatcher nonResoures = new NegatedRequestMatcher(resources);
http
.requestMatcher(nonResoures).authorizeRequests()
.anyRequest().authenticated();
}
}
And put all your resources under /resources/**
Of course in this case both will use the same oauth2 configuration (accessTokenUri, jwt.key-value, etc.)
UPDATE1:
Actually you can achieve your original goal by using this request matcher for the above configuration:
new RequestHeaderRequestMatcher("Authorization")
UPDATE2:
(Explanation of #sid-morad's comment)
Spring Security creates a filter chain for each configuration. The request matcher for each filter chain is evaluated in the order of the configurations.
WebSecurityConfigurerAdapter has default order 100, and ResourceServerConfiguration is ordered 3 by default. Which means ResourceServerConfiguration's request matcher evaluated first. This order can be overridden for these configurations like:
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Autowired
private org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfiguration configuration;
#PostConstruct
public void setSecurityConfigurerOrder() {
configuration.setOrder(3);
}
...
}
#Configuration
#EnableOAuth2Sso
#Order(100)
public class SsoSecurityConfiguration extends WebSecurityConfigurerAdapter {
...
}
So yes, request matcher is not needed for SsoSecurityConfiguration in the above sample. But good to know the reasons behind :)

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