Secure spring boot app without user log on - spring

I am creating a spring boot API and react front end in a project I am working on. There is no user logon on the front end, however, I would like to ensure my API endpoints are secure and only accessed by my react front end and I am struggling to come up with solutions.
One possible solution I was thinking is to secure the API with JWT. Create a user table and create a user for the front end client. When the user visits the front end get a token and validate this token on all requests. I’m not sure if this is a valid approach or if there is another solution better fitted.
The app will be hosted on Heroku, stack: spring boot, react and MySQL database. Anyone come across this before or any advice greatly appreciated.

This is not possible.
At its core, this would require the frontend to have access to some secret value with which to authenticate it's request with, i.e. a token. You would then allow requests based on the presence of this secret value, and serve the responses.
However, frontends serve public assets and thus can't have secrets. At the end of the day, any user would be able to inspect their network requests, and extract the token from the requests your frontend makes.
With this information they can then forge their own requests.

I would recommend to add domain at allowedOrigins of CORS config :
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Arrays;
import java.util.List;
#Component
public class CorsFilterConfig {
public static final List<String> allowedOrigins = Arrays.asList("yourDomainsHere");
#Bean
public FilterRegistrationBean<CorsFilter> initCorsFilter() {
// #formatter:off
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));
config.addAllowedMethod("*");
config.setAllowedOrigins(allowedOrigins);
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
// #formatter:on
}
}
More

Related

Multi-tenant Spring Webflux microservice with Keycloak OAuth/OIDC

Use Case:
I am building couple of reactive microservices using spring webflux.
I am using Keycloak as authentication and authorization server.
Keycloak realms are used as tenants where tenant/realm specific clients and users are configured.
The client for my reactive microservice is configured in each realm of Keycloak with the same client id & name.
The microservice REST APIs would be accessed by users of different realms of Keycloak.
The APIs would be accessed by user using UX (developed in React) publicly as well as by other webflux microservices as different client internally.
The initial part of the REST API would contain tenant information e.g. http://Service-URI:Service-Port/accounts/Keycloak-Realm/Rest-of-API-URI
Requirements:
When the API is called from UX, I need to invoke authorization code grant flow to authenticate the user using the realm information present in the request URI. The user (if not already logged in) should be redirected to the login page of correct realm (present in the request URI)
When the API is called from another webflux microservice, it should invoke client credential grant flow to authenticate and authorize the caller service.
Issue Faced:
I tried to override ReactiveAuthenticationManagerResolver as below:
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders;
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
#Component
public class TenantAuthenticationManagerResolver implements ReactiveAuthenticationManagerResolver<ServerWebExchange> {
private static final String ACCOUNT_URI_PREFIX = "/accounts/";
private static final String ACCOUNTS = "/accounts";
private static final String EMPTY_STRING = "";
private final Map<String, String> tenants = new HashMap<>();
private final Map<String, JwtReactiveAuthenticationManager> authenticationManagers = new HashMap<>();
public TenantAuthenticationManagerResolver() {
this.tenants.put("neo4j", "http://localhost:8080/realms/realm1");
this.tenants.put("testac", "http://localhost:8080/realms/realm2");
}
#Override
public Mono<ReactiveAuthenticationManager> resolve(ServerWebExchange exchange) {
return Mono.just(this.authenticationManagers.computeIfAbsent(toTenant(exchange), this::fromTenant));
}
private String toTenant(ServerWebExchange exchange) {
try {
String tenant = "system";
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if (path.startsWith(ACCOUNT_URI_PREFIX)) {
tenant = extractAccountFromPath(path);
}
return tenant;
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
private JwtReactiveAuthenticationManager fromTenant(String tenant) {
return Optional.ofNullable(this.tenants.get(tenant))
.map(ReactiveJwtDecoders::fromIssuerLocation)
.map(JwtReactiveAuthenticationManager::new)
.orElseThrow(() -> new IllegalArgumentException("Unknown tenant"));
}
private String extractAccountFromPath(String path) {
String removeAccountTag = path.replace(ACCOUNTS, EMPTY_STRING);
int indexOfSlash = removeAccountTag.indexOf("/");
return removeAccountTag.substring(indexOfSlash + 1, removeAccountTag.indexOf("/", indexOfSlash + 1));
}
}
Then I used the overridden TenantAuthenticationManagerResolver class in to SecurityWebFilterChain configuration as below:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfig {
#Autowired
TenantAuthenticationManagerResolver authenticationManagerResolver;
#Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/health").hasAnyAuthority("ROLE_USER")
.anyExchange().authenticated()
.and()
.oauth2Client()
.and()
.oauth2Login()
.and()
.oauth2ResourceServer()
.authenticationManagerResolver(authenticationManagerResolver);
return http.build();
}
}
Blow is the configuration in application.properties:
server.port=8090
logging.level.org.springframework.security=DEBUG
spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.registration.keycloak.client-id=test-client
spring.security.oauth2.client.registration.keycloak.client-secret=ZV4kAKjeNW2KEnYejojOCsi0vqt9vMiS
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.scope=openid
spring.security.oauth2.client.registration.keycloak.redirect-uri={baseUrl}/login/oauth2/code/keycloak
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/realms/master
When I call the API using master realm e.g. http://localhost:8090/accounts/master/health it redirects the user to Keycloak login page of master realm and once I put the user id and password of a user of master realm, the API call is successful.
When I call the API using any other realm e.g. http://localhost:8090/accounts/realm1/health it still redirects the user to Keycloak login page of master realm and if I put the user id and password of a user of realm1 realm, the login is not successful.
So it seems that multi-tenancy is not working as expected and it is only working for the tenant configured in application.properties.
What is missing in my implementation w.r.t. multi-tenancy?
How to pass the client credentials for different realms?
I tried to use JWKS in place of client credentials but somehow it is not working. Below is the configuration used for JWKS in application.properties.
spring.security.oauth2.client.registration.keycloak.keystore=C:\\Work\\test-client.jks
spring.security.oauth2.client.registration.keycloak.keystore-type=JKS
spring.security.oauth2.client.registration.keycloak.keystore-password=changeit
spring.security.oauth2.client.registration.keycloak.key-password=changeit
spring.security.oauth2.client.registration.keycloak.key-alias=proactive-outreach-admin
spring.security.oauth2.client.registration.keycloak.truststore=C:\\Work\\test-client.jks
spring.security.oauth2.client.registration.keycloak.truststore-password=changeit
Note: JWKS is not event working for master realm configured in application.properties.
Need help here as I am stuck for many days without any breakthrough. Let me know if any more information is required.
Client V.S. resource-server configuration
Authentication is not the responsibility of the resource-servers: request arrive authorized (or not if some endpoints accept anonymous requests) with a Bearer access-token. Do not implement redirection to login there, just return 401 if authentication is missing.
It is the responsibility of OAuth2 clients to acquire (and maintain) access-tokens, with sometimes other flows than authorization-code, even to authenticate users: won't you use refresh-token flow for instance?
This OAuth2 client could be either a "public" client in a browser (your react app) or a BFF in between the browser and the resource-servers (spring-cloud-gateway for instance). The later is considered more secure because tokens are kept on your servers and is a rather strong recommendation lately.
If you want both client (for oauth2Login) and resource-server (for securing a REST API) configurations in your app, define two separated security filter-chains as exposed in this other answer: Use Keycloak Spring Adapter with Spring Boot 3
Multi-tenancy in Webflux with JWT decoder
The recommended way is to override the default authentication-manager resolver with one capable of providing the right authentication-manager depending on the access-token issuer (or header or whatever from the request): http.oauth2ResourceServer().authenticationManagerResolver(authenticationManagerResolver)
To easily configure multi-tenant reactive resource-servers (with JWT decoder and an authentication-manager switching the tenant based on token issuer), you should have a look at this Spring Boot starter I maintain: com.c4-soft.springaddons:spring-addons-webflux-jwt-resource-server. sample usage here and tutorials there. As it is open-source, you can also browse the source to see in details how this is done and what this reactive multi-tenant authentication-manager looks like.

Spring cloud gateway GET api is working fine but POST is not

I'm new to Spring cloud and spring backend development. I'm trying to develop a simple microservice with spring cloud gateway.
Github links to Discovery Server, Spring Cloud Api Gateway and User micro service. Astonishingly when I execute the APIs in Postman, the GET API works but not the POST.
I tried to debug, It didn't come to my Controller for POST. What am I missing? Or doing wrong?
Any help or suggestion would be helpful.
It may be a cross-domain problem
Here's an example
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* #author beichenhpy
* #version 0.0.1
* #apiNote CorsConfig description:
* #since 2021/5/9 9:45 下午
*/
#Configuration
public class CorsConfig {
private static final String ALLOWED_HEADERS = "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN,token,username,client";
private static final String ALLOWED_METHODS = "*";
private static final String ALLOWED_ORIGIN = "*";
private static final String ALLOWED_EXPOSE = "*";
private static final Long MAX_AGE = 18000L;
#Bean
public CorsWebFilter corsWebFilter(){
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader(ALLOWED_HEADERS);
corsConfiguration.addAllowedMethod(ALLOWED_METHODS);
corsConfiguration.addAllowedOrigin(ALLOWED_ORIGIN);
corsConfiguration.addExposedHeader(ALLOWED_EXPOSE);
corsConfiguration.setMaxAge(MAX_AGE);
UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource(new PathPatternParser());
configurationSource.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(configurationSource);
}
}
Found the problem.
If anyone else has a similar problem.
I missed out on routes for POST API. Thanks, #spencergibb for the hint.

Spring Security Oauth2 + OIDC (OpenID Connect) OP Initiated/Back Channel logout with Multiple RPs

An Authentication server (OP) with multiple RPs is the architecture. Please check the image below.
I was able to successfully log out with RP initiated logout which is if I log out from any of the clients the client(RP) gets logged out and the OP also gets logged out.
Please find OpenID Provider Discovery Metadata;
{
"jwks_uri":"https://<Auth Server URL>/token_keys",
"subject_types_supported":["public"],
"end_session_endpoint":"https://<Auth Server URL>/logout",
"issuer":"https://<Auth Server URL>/oauth/token",
"authorization_endpoint":"https://<Auth Server URL>/oauth/authorize",
"token_endpoint":"https://<Auth Server URL>/oauth/token"
}
with "end_session_endpoint" present in the discovery I was able to do the RP initiated logout using the following code in the clients,
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.header.writers.StaticHeadersWriter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import com.nimbusds.jose.shaded.json.JSONArray;
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ClientRegistrationRepository clientRegistrationRepository;
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**").antMatchers("/assets/bundles/**");
}
#Bean(name = "oidcUserService")
OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
return new CustomOidcUserService();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().headers(headers -> headers.cacheControl().and()
.addHeaderWriter(new StaticHeadersWriter("X-UA-Compatible", "IE=edge"))
.frameOptions(frameOptions -> frameOptions.sameOrigin()).httpStrictTransportSecurity())
.authorizeRequests(authorize -> authorize
.antMatchers("/error").permitAll()
.anyRequest().authenticated())
.oauth2Login(oauthLogin -> oauthLogin
.userInfoEndpoint()
.oidcUserService(this.oidcUserService()))
.logout(logout -> logout
.logoutSuccessHandler(oidcLogoutSuccessHandler())
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSIONID","CSRF-TOKEN","XSRF-TOKEN")
.permitAll())
.csrf((csrf) -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
}
private OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);
oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
return oidcLogoutSuccessHandler;
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
But I have no idea how to implement OP initiated Logot, which logs out all the clients under the OP if the OP session expires or OP gets logged out. Are there any examples available? I have been researching for this for few days and found about having another key called "check_session_iframe" but could not find proper documentation from spring. Please share if you have any examples. Thank you.
If the OP's metadata does not include a check_session_iframe frontchannel_logout_supported or backchannel_logout_supported, then it simply does not support single logout, which would trigger a logout at the clients aka "OP initiated logout".
The check_session_iframe contains an URL under the control of the OP. The client will embed this URL as an iframe, also called OP iframe. The OP iframe has access to the User Agent state at the OpenID provider (since it is the same domain). The client will create another iframe, the RP iframe, to send postMessage requests to the OP iframe for checking the session status. The OP iframe will then respond with a postMessage request to the RP iframe with the result (changed, unchanged or error).
Regarding the client part of the session management specification the only thing you have to do is to embed the OP iframe, give it an id so that you can send postMessage requests to it and handle postMessage requests from it within your RP iframe. Check out the pseudo code in the specification:
https://openid.net/specs/openid-connect-session-1_0.html#RPiframe

How to Solve 403 Error in Spring Boot Post Request

I am newbie in spring boot rest services. I have developed some rest api in spring boot using maven project.
I have successfully developed Get and Post Api. My GET Method working properly in postman and mobile. when i am trying to hit post method from postman its working properly but from mobile its gives 403 forbidden error.
This is my Configuration:
spring.datasource.url = jdbc:mysql://localhost/sampledb?useSSL=false
spring.datasource.username = te
spring.datasource.password = test
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQL5InnoDBDialect
Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update
Please Suggest me how to solve error.
you have to disable csrf Protection because it is enabled by default in spring security: here you can see code that allow cors origin.
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception{
http.cors().and().csrf().disable();
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
In Spring Security Cross-site check is by default enable, we need to disable it by creating a separate class to stop cross-checking.
package com.baba.jaxws;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
#Override
//we have stopped the csrf to make post method work
protected void configure(HttpSecurity http) throws Exception{
http.cors().and().csrf().disable();
}
}
Possible causes:
Requests done from postman are different to the one done from mobile (uri, method, headers)
Invalid token
CORS (read something about it, google is full of articles) add #CrossOrigin annotation to your controller.
mobile app is doing an OPTION request before performing the POST, and you block OPTION requests. If also from postman the OPTION requests are blocked, add the property spring.mvc.dispatch-options-request=true. Moreover, in case you are using spring security, you have to explicitly allow OPTION requests also for it.
I was able to solve this by using:
<form th:action="#{url}" method="post">
Instead of:
<form action="url" method="post">
It seems the th:action tag does url rewriting to enable csrf validation.
CSRF is enabled by default in Spring Security. Having this enabled ensures a 403 error on HTTP requests that would change (object) states.
For more information please visit: https://docs.spring.io/spring-security/site/docs/3.2.x/reference/htmlsingle/html5/#csrf
It is possible to disable CSRF in the Spring Security. However, it is enabled by default (convention over configuration) and for a good reason. This is also explained in the link provided to Spring's Security.
A working example, using Thymeleaf, might be:
HTML
<head>
<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
</head>
JS
function postExample() {
let token = $("meta[name='_csrf']").attr("content");
let header = $("meta[name='_csrf_header']").attr("content");
let data = {username: "", password: "", firstname: "", lastname: ""};
// Object key string interpolation by {[header]:token} works with ES6
fetch(window.location+"/addnote", {
method:"POST",
headers: {
[header]: token,
"charset": "UTF-8",
"Content-Type": "application/json"
},
body: JSON.stringify(data)
}).then(res => console.log(res)).catch(err => console.log(err))
}
CONTROLLER per request of #mahmoud-magdy
#PostMapping("/addnote")
public Long addNote(#RequestBody() String data) {
Gson gson = new Gson();
JSONAddNote json = gson.fromJson(data, JSONAddNote.class);
return <service>.addNote(json.username, json....);
}
class JSONAddNote {
public String username;
public String ...etc
}
Or a more direct CONTROLLER:
#PostMapping("/addnote")
public Long addNote(#RequestBody Data data) {
return <service>.addNote(data);
}
class Data {
public String username;
public String ...etc
}
To build on the accepted answer
Many HTTP client libraries (eg Axios) implicitly set a Content-Type: JSON header for POST requests. In my case, I forgot to allow that header causing only POSTS to fail.
#Bean
CorsConfigurationSource corsConfigurationSource() {
...
configuration.addAllowedHeader("Content-Type"); // <- ALLOW THIS HEADER
...
}
This answer is related to this question if you are deploying to Open/WAS Liberty server.
If so, you might get 403 error even though your code works perfectly fine if deploying to embedded Tomcat that comes with Spring boot.
Liberty does not read (or considers) your
server.servlet.context-path=/myapi/v1
that you set in your application.properties or application.yml file for some reason. Or, it just overwrites it, not sure. So, the above context-path will work just fine if deployment in Spring Boot embeded Tomcat container.
However, when you deploy it to OpenLiberty/WASLiberty, you might find that your endpoints will stop working and you get 403 and/or 404 errors.
In my example, I have api where I have /auth endpoint in my WebSecurityConfiguration class:
//Customize the /login url to overwrite the Spring default provided /login url.
private AuthenticationFilter authenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager());
// This works fine on embedded tomcat, but not in Liberty where it returns 403.
// To fix, in server.xml <appllication> block, add
// <application context-root="/myapi/v1" ... and then both
// auth and other endpoints will work fine in Liberty.
filter.setFilterProcessesUrl("/auth");
// This is temporary "fix" that creates rather more issues, as it
// works fine with Tomcat but fails in Liberty and all other
// endpoints still return 404
//filter.setFilterProcessesUrl("/v1/auth");
return filter;
}
Based on the above context-path, on Tomcat, it becomes /myapi/v1/auth while on Liberty, it ends up being just /myapi/auth which is wrong. I think what Liberty does, it will just take the name of the api and add to it the endpoint, therefore ignoring the versioning.
As a result of this, AntPathRequestMatcher class matches() method will result in a non-matching /auth end point and you will get 403 error. And the other endpoints will result in 404 error.
SOLUTION
In your application.properties, leave
server.servlet.context-path=/myapi/v1
, this will be picked up by embedded Tomcat and your app will continue to work as expected.
In your server.xml configuration for Open/WAS Liberty, add matching context-root to the section like:
<application context-root="/myapi/v1" id="myapi" location="location\of\your\myapi-0.0.1.war" name="myapi" type="war">
, this will be picked up by Open/WASLiberty and your app will continue to work as expected on Liberty container as well.

Spring 5 reactive websocket and multiple endpoints?

We are doing a little hackathon at work and I wanted to try some new technology to get away from the usual controller.
I started using Spring Webflux with reactive WebSockets and everything is working fine so far. I configured my WebSocket handler as follows:
import my.handler.DomWebSocketHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
import java.util.HashMap;
import java.util.Map;
#Configuration
public class AppConfig implements WebFluxConfigurer {
#Autowired
private WebSocketHandler domWebSocketHandler;
#Bean
public HandlerMapping webSocketMapping() {
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/event-emitter", domWebSocketHandler);
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(1);
mapping.setUrlMap(map);
return mapping;
}
#Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}
After some more research, I learned that it is best practice to work with one connection per client.
Furthermore using more than a web-socket per browsing session for the same application seems overkill since you can use pub/sub channels. See answer here
Is there a way to restrict the connections per client and use only one endpoint for all required client "requests" or would it be better to create additional endpoints (like you would with a normal controller)?
Thank you in advance for the help.

Resources