I have a spring security application and I'm trying implement websockets using Stomp.
The application is mainly REST based, using tokens for security. All requests coming in have to a security token in the header.
The problem is when setting up a simple Stomp client using basic html, spring appears to not be seeing any headers.
The client works fine if I disable the security, in which case no headers are passed in.
var socket = new SockJS('http://localhost:8080/project/ws/wsendpoint');
var headers = {'Auth': 'some_auth_token'}
writeConsole("Created socket");
stompClient = Stomp.over(socket);
stompClient.connect(headers, function(frame) {
writeConsole("Connected to via WebSocket");
stompClient.subscribe('/topic/push', function(message)
{ writeConsole(message.body);}, headers );
});
window.onbeforeunload = disconnectClient;
Heres the relevant spring configuration
protected void configure(final HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.headers().frameOptions().sameOrigin()
.and()
.authorizeRequests()
.anyRequest().authenticated() authenticated.
.and()
.anonymous().disable()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint());
http.addFilterBefore(authenticationTokenFilter(), BasicAuthenticationFilter.class);
}
The doFilter in the authenticationTokenFilter class should see the header field 'Auth', as set in the client, however nothing is there.
Instead of sending header, you can replace sessionId with your own ID.
var sessionId = utils.random_string(36);
var socket = new SockJS('/socket', [], {
sessionId: () => {
return sessionId
}
});
stompClient = Stomp.over(socket);
stompClient.connect(headers, function(frame) {
writeConsole("Connected to via WebSocket");
stompClient.subscribe('/topic/push', function(message)
{ writeConsole(message.body);}, headers );
});
Stomp cannot send any custom headers during the initial authentication stage. The way round this was to send the authentication token as a query parameter (would not recommended for non-closed systems).
Related
This is causing me quite a headache. All I find is how to disable the redirect.
I'm trying to do the opposite and can't seem to figure out how!
What I'm trying to achieve is: after a successful logout, redirect the user to another page (be that the login page or a logout success page, whatever).
What happens is: after a successful logout, I stay on the same page, even though I can see the correct response under network the network tab of the developer tools.
Here's what I currently have:
security config:
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf(c -> c.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.oauth2Login()
.loginPage("/login").permitAll()
.and()
.logout()
.logoutSuccessUrl("/logged-out").permitAll()
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID");
}
controller method:
#GetMapping("logged-out")
public ModelAndView loggedOut(ModelMap model) {
return new ModelAndView("logged-out", model);
}
js script hooked to the button:
let logout = function() {
let request = new XMLHttpRequest();
request.open("POST", "/logout");
request.setRequestHeader("X-XSRF-TOKEN", Cookies.get('XSRF-TOKEN'));
request.setRequestHeader("Accept", "text/html");
request.send();
}
the log events after clicking the button:
DEBUG 15539 --- [nio-8080-exec-9] o.s.web.servlet.DispatcherServlet : GET "/logged-out", parameters={}
DEBUG 15539 --- [nio-8080-exec-9] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.hamargyuri.petprojectjava2021.controller.HomeController#loggedOut(ModelMap)
DEBUG 15539 --- [nio-8080-exec-9] o.s.w.s.v.ContentNegotiatingViewResolver : Selected 'text/html' given [text/html]
DEBUG 15539 --- [nio-8080-exec-9] o.s.web.servlet.DispatcherServlet : Completed 200 OK
and here's what I see in the browser
I'm sure there must be some very stupid thing I'm missing here, but I'm at the stage of banging my head to the wall, so I'd appreciate any guidance!
So, it seems like my frontend code was at fault, as it's the one that should do something if the logout call was successful.
I know it's very basic, but this is what I've changed in the js:
let goHome = function() {
window.location.href = "/";
}
let logout = function() {
let request = new XMLHttpRequest();
request.open("POST", "/logout");
request.setRequestHeader("X-XSRF-TOKEN", Cookies.get('XSRF-TOKEN'));
request.onload = goHome;
request.send();
}
I also removed the explicit logoutSuccessUrl, the default is fine, all I need a successful response, then the above function will take me "home".
I have security configuration for my webflux server:
#Bean
fun httpTestFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http
.authorizeExchange()
.pathMatchers("/actuator/**").permitAll()
.pathMatchers("/webjars/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyExchange().access(authManager)
.and().cors()
.and()
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable()
return http.build()
}
#Bean
fun userDetailsService(): ReactiveUserDetailsService {
val user: UserDetails = User.builder()
.username(userName)
.password(passwordEncoder().encode(password))
.roles("ADMIN")
.build()
return MapReactiveUserDetailsService(user)
}
#Bean
fun passwordEncoder() = BCryptPasswordEncoder()
#Bean("CustomAuth")
fun authManager():
ReactiveAuthorizationManager<AuthorizationContext> {
return ReactiveAuthorizationManager<AuthorizationContext> { mono, context ->
val request = context.exchange.request
val mutateExchange = context.exchange.mutate()
val token = request.headers[AUTHORIZATION] ?: throw
AccessDeniedException(ERROR_MESSAGE)
mono
// go to other service to check token
.then(webClient.checkToken(token.first().toString()))
.doOnError {
throw AccessDeniedException(ERROR_MESSAGE)
}
.cast(ResponseEntity::class.java)
.map { it.body as AuthDto }
.doOnNext { auth ->
mutateExchange.request {
it.header(USER_ID, auth.userId.toString())
it.header(AUTH_SYSTEM, auth.authSystem)
}
}
.map { AuthorizationDecision(true) }
}
}
As you can see httpBasic() option is disabled. When I go to any secure url, browser shows http basic window. Then I can enter valid or INVALID login and password and if authManager returns good result authentication will be successful or 401 will thrown in other case and auth window in browser will reopen.
Why it happens? Is it bug?
P.S. Spring boot version 2.5.5
The solution helped me:
#Bean
fun httpTestFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http
.authorizeExchange()
.pathMatchers("/actuator/**").permitAll()
.pathMatchers("/webjars/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyExchange().access(authManager)
.and().cors()
.and()
.exceptionHandling()
.authenticationEntryPoint { exchange, _ ->
val response = exchange.response
response.statusCode = HttpStatus.UNAUTHORIZED
response.headers.set(HttpHeaders.WWW_AUTHENTICATE, "None")
exchange.mutate().response(response)
Mono.empty()
}
.and()
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable()
return http.build()
}
We need change authenticationEntryPoint in part of status code and disabling HttpHeaders.WWW_AUTHENTICATE. Then return changed response.
I have developed a small UI5 application with OData calls and I am implementing the authorizations. On the server I have a spring web security application where have I have restricted the MemebersList.view.xml to users that are logged in. My idea is to create under the folders view and controller a folder user for the logged in users with the files that are restricted.
After the user is successfully logged in I receive the token to make further OData calls.
Question 1:
Can I restrict in UI5 the resources(XML views and JS controllers) so that the user only has direct access when is logged in or the sources have to be visible allways? For example I want to avoid that the user can directly enter
http://localhost:9004/webapp/view/user/MembersList.view.xml or
http://localhost:9004/webapp/controller/user/MembersList.controllers.js and see the code
Question 2:
Can I somehow send the Token in the header when I call the view MembersList with the getRouter().navTo('MembersList')? I guess in this case I would also need somehow to pass the token to the call for the MembersList.controllers.js. Maybe set that all the http calls include the token?
Maybe there is a better approach
WebSecurityConfig.java
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(jwtEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/**/user/**").hasRole("USER")
.antMatchers("/**/admin/**").hasRole("ADMIN")
.antMatchers("/",
"/favicon.ico",
"/**/*.json",
"/**/*.xml",
"/**/*.properties",
"/**/*.woff2",
"/**/*.woff",
"/**/*.ttf",
"/**/*.ttc",
"/**/*.ico",
"/**/*.bmp",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.jpeg",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/**/*.html").permitAll()
.antMatchers("/**/api/auth/**").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
onClickEvent:
onLoginTap : function() {
var navigator_info = window.navigator;
var screen_info = window.screen;
var uid = navigator_info.mimeTypes.length;
uid += navigator_info.userAgent.replace(/\D+/g, '');
uid += navigator_info.plugins.length;
uid += screen_info.height || '';
uid += screen_info.width || '';
uid += screen_info.pixelDepth || '';
this.loginBody.getData().email = this.login.getData().user;
this.loginBody.getData().password = this.login.getData().password;
this.loginBody.getData().deviceId = uid;
// create XHR object
var xhttp = new XMLHttpRequest();
var that = this;
// gets everytime fired when the XHR request state changes
xhttp.onreadystatechange = function() {
// 4 means request is finished and response is ready
// 200 means request is just fine
if (this.readyState == 4 && this.status == 200) {
// "this" refers here to the XHR object
var json = JSON.parse(this.responseText)
if (json.accessToken !== null) {
that.login.getData().accessToken = json.accessToken;
that.getRouter().navTo("MembersList");
}
}
};
// set the XHR request parameters
xhttp.open("POST", "http://localhost:9004/api/auth/login", true);
xhttp.setRequestHeader("Content-Type", "application/json");
// fire the XHR request
xhttp.send(JSON.stringify(this.loginBody.getData()));
}
I have the following controller to get registration ClientA when the endpoint is called.
#GetMapping("/token")
fun token(
#RegisteredOAuth2AuthorizedClient("clientA") authorizedClient: OAuth2AuthorizedClient
): ResponseEntity<String> {
val token = tokenService.getToken()
return ResponseEntity(token, HttpStatus.OK)
}
I want to have the client as a query param and dynamically start the OAuth2 process. How could I achieve it? sth like the following:
#GetMapping("/token?client={client}")
fun token(
#RegisteredOAuth2AuthorizedClient(${client}) authorizedClient: OAuth2AuthorizedClient
): ResponseEntity<String> {
val token = tokenService.getToken()
return ResponseEntity(token, HttpStatus.OK)
}
My solution is to use the default authorization uri and defaultSuccessUrl("/token")
The goal is to scale the client easily, and this solution can achieve the same.
Solution:
Register clientA in application.yml
clientA:
client-id: Any
redirect-uri: http://localhost/index
provider: clientA-provider
scope: launch
client-name: clientA
client-authentication-method: none
authorization-grant-type: authorization_code
Now the endpoint /oauth2/authorization/clientA is created automatically by spring security. For getting a token, call the above url.
After successful authorization, the endpoint goes to /token, due to defaultSuccessUrl("/token").
The following shows the related code snippet:
override fun configure(http: HttpSecurity) {
http.csrf()
.disable()
.oauth2Login()
.authorizationEndpoint()
.and()
.tokenEndpoint()
.and()
.defaultSuccessUrl("/token")
#Controller
#RequestMapping("/token")
class LaunchController(private val tokenService: TokenService) {
#GetMapping
fun token(): ResponseEntity<String> {
val token = tokenService.getToken()
return ResponseEntity.ok(token)
}
}
I need to build a kind of Java Proxy+ that handle the OAuth2 flow. The idea is to login in this "Proxy+" and do the OAuth2 flow until the Proxy+ has received the token + refresh token.
After that you login onto the Proxy+ with Username and Password or something else that give you a session. The proxy will handle generically all your web requests and add the Oauth2 token to each request. If necessary the also refresh the token.
How would you implement the Proxy Part where requests are taken and enhanced and maybe a token refresh is requested. This question is NOT about the OAuth2 flow and NOT about how to get the token in the first place.
I think i try a servlet filter that intercepts all requests and enhance the request. This way it is also generic to all urls called. Any better ideas?
Found a better way, this is the Spring way of a generic Proxy :-)
#RequestMapping("/**")
public ResponseEntity<byte[]> genericRequest(RequestEntity<?> inboundRequestEntity, HttpServletRequest request) {
URI outboundUri = UriComponentsBuilder.fromHttpUrl(this.targetBaseUrl)
.path(removeUrlPart(request))
.query(request.getQueryString())
.build(true)
.toUri();
HttpHeaders headers = filterHeaders(inboundRequestEntity.getHeaders());
BodyBuilder builder = RequestEntity
.method(requireNonNull(inboundRequestEntity.getMethod()), outboundUri)
.headers(headers);
RequestEntity<?> outboundRequestEntity = inboundRequestEntity.hasBody() ? builder.body(requireNonNull(inboundRequestEntity.getBody())) : builder.build();
try {
LOGGER.info("Will call url '{}' with method '{}'", outboundRequestEntity.getUrl(), outboundRequestEntity.getMethod());
ResponseEntity<byte[]> responseEntity = this.restTemplate.exchange(outboundRequestEntity);
return ResponseEntity.status(responseEntity.getStatusCode())
.headers(filterHeaders(responseEntity.getHeaders()))
.body(responseEntity.getBody());
} catch (
HttpStatusCodeException e) {
return ResponseEntity.status(e.getRawStatusCode())
.headers(filterHeaders(e.getResponseHeaders()))
.body(e.getResponseBodyAsByteArray());
}
}
private static HttpHeaders filterHeaders(HttpHeaders originalHeaders) {
HttpHeaders filteredResponseHeaders = new HttpHeaders();
filteredResponseHeaders.putAll(originalHeaders);
filteredResponseHeaders.remove(CONTENT_LENGTH);
filteredResponseHeaders.remove(DATE);
return filteredResponseHeaders;
}