Spring WebClient Configuration Okta OAuth2 - java

I'm building an API Gateway which uses Spring Webflux, Spring Cloud Gateway, Spring Cloud Security & Okta for OAuth2.
Here's my RouteLocator, through which I can call my Foo Microservice.
#Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder, TokenRelayGatewayFilterFactory filterFactory) {
return builder.routes()
.route("foo", r ->
r.path("/foo")
.filters(f -> f
.rewritePath("/foo", "/api/v1/foo")
.filter(filterFactory.apply()))
.uri("lb://foo-service")
)
.build();
}
This works perfectly fine.
However, since I need to aggregate the results of different microservices, let's say Foo and Bar, I'm creating a load balanced Spring WebClient bean that I can use to make http calls:
#Bean
#LoadBalanced
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
How can I configure the WebClient to pass the Token on every request the same way as the TokenRelayGatewayFilterFactory does in the RouteLocator?
EDIT:
Here's my updated WebClient bean:
#Bean
#LoadBalanced
public WebClient.Builder webClientBuilder(ReactiveClientRegistrationRepository clientRegistrations,
ServerOAuth2AuthorizedClientRepository authorizedClients) {
var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);
oauth.setDefaultOAuth2AuthorizedClient(true);
oauth.setDefaultClientRegistrationId("okta");
return WebClient
.builder()
.filter(oauth);
}
Now it seems like it is working on the Chrome browser. After logging in on Okta I can access /foo on Foo microservice through Http GET. Although when I try an Http POST on /foo through Postman, (while adding the Authorization header), I'm getting a 302 response that redirects me to an Okta html page.
Funnily enough, using the RouteLocator I don't get any redirection and both GET and POST work through Postman. The redirection seems to be happening only when using WebClient.
Any idea why?
EDIT #2:
My Security config file:
#Configuration
#EnableWebFluxSecurity
class SecurityConfig {
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.authorizeExchange().anyExchange().authenticated()
.and()
.oauth2Login()
.and()
.oauth2ResourceServer().jwt()
.and()
.and().build();
}
#Bean
CorsWebFilter corsWebFilter(){
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.setAllowedOrigins(List.of("*"));
corsConfig.setMaxAge(3600L);
corsConfig.addAllowedMethod("*");
corsConfig.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);
return new CorsWebFilter(source);
}
}

Take a look at ServletOAuth2AuthorizedClientExchangeFilterFunction (or the reactive equivalent)
This video covers it in more detail: https://youtu.be/v2J32nd0g24?t=2168

Related

Handling CORS and then request not passing to #WebFilter?

I've been working on a Java Spring Boot application for a while and everything was working correctly with it, including authorisation / authentication.
I recently started working on a front end application in Vue to work with the Spring Boot endpoints and started having CORS error responses whenever sending requests to the endpoints as running both locally.
I utilised the approach from Dan Vega's youtube channel, adding a 'WebSecurityConfig' class with the following code:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.cors(Customizer.withDefaults())
.authorizeRequests(auth -> auth.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.build();
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:8081"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("Authorization"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
After implementing this, I no longer get CORS related error responses, however I now receive 401 errors to all requests, making me think that requests now aren't flowing through my #WebFilter AuthorisationFilter class after the CORS filter.
My Java code is up at https://github.com/Bungle1981/Java_SpringBoot_Gym_Backend/tree/master/src/main/java/com/example/assessment
Has anyone encountered this or have any solutions please?

How to permitAll anonymous access in Spring Boot 3 or above?

After upgraded to Spring Boot 3.0.0 from 2.7.6, public API are not accessible.
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(CsrfConfigurer::disable)
.authorizeHttpRequests(requests -> requests
.anyRequest().authenticated())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.build();
}
#Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web
.ignoring()
.requestMatchers(CorsUtils::isPreFlightRequest)
.requestMatchers("/actuator/**", "/graphiql/**", "/voyager/**", "/vendor/**", "/rest/**",
"/swagger-ui/**", "/v3/api-docs/**");
}
You are almost there: move requestMatchers with permitAll to SecurityFilterChain definition (and remove WebSecurityCustomizer):
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.csrf(CsrfConfigurer::disable)
.authorizeHttpRequests(requests -> requests
.requestMatchers("/resources/**", "/signup", "/about").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.build();
}
Also, make sure the configuration class containing your SecurityFilterChain bean declaration is decorated with #Configuration (set a breakpoint or log line to ensure it is instantiated).
Few notes:
never disable CSRF protection if you don't also disable sessions with http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
CORS is configured with SecurityFilterChain too: http.cors().configurationSource(corsConfigurationSource())
you might need to map roles or groups or whatever your authorization-server populates as private-claim to Spring authorities by providing your own jwtAuthenticationConverter to JWT oauth2ResourceServer configurer
You can do all of above with no Java conf (just a few properties) with thin wrappers around spring-boot-starter-oauth2-resource-server I wrote:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<version>6.0.9</version>
</dependency>
#Configuration
#EnableMethodSecurity
public static class SecurityConfig {
}
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
# This is adapted to Keycloak with a client-id spring-addons-public. Replace with the claim(s) your authorization-server puts roles into
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.spring-addons-public.roles
# you might want something more restrictive as allowed origins (accepts a list), at least for a part of your endpoints
com.c4-soft.springaddons.security.cors[0].path=/**
com.c4-soft.springaddons.security.cors[0].allowed-origins=*
Complete (short) tutorials there (with spring libs only or "my" wrappers): https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials
The methods have not changed too much.
Sample :
#Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
// ...
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/resources/**", "/signup", "/about").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/db/**").access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and hasRole('DBA')"))
// .requestMatchers("/db/**").access(AuthorizationManagers.allOf(AuthorityAuthorizationManager.hasRole("ADMIN"), AuthorityAuthorizationManager.hasRole("DBA")))
.anyRequest().denyAll()
);
return http.build();
}
Provided of https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html
EDIT : To complete my answer, you have access to all methods, so to set up a resource server you need to add this :
#Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
// ...
.authorizeHttpRequests(authorize -> authorize
// ...
)
.and()
.oauth2ResourceServer( OAuth2ResourceServerConfigurer::jwt )
return http.build();
}
Remember to add the variables corresponding to your OAuth2 provider
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://{your-ouath2-provider}/{issuer-uri}
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://{your-ouath2-provider}/{jwk-uri} (optionnal)
The links depend on your oauth2 provider.
For more information about the resource owner server
: https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html

How to enable OAuth on a specific endpoint using spring security

I am trying to familiarize myself with Spring Security, in particular migrating from Spring Security OAuth to Soring Security (as in the following example/guide https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide).
However, I am seeming to only get 403 Forbidden errors. I am accessing from Postman and am using my company's existing OAuth server. I am able to get a token from the auth server, so I know I have those credentials correct and I have verified what roles the OAuth user has.
I am using the following dependencies:
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-web'
This is the simple endpoint I am attempting to access:
#RestController
public class AppController
{
#GetMapping("/hello")
public String hello()
{
return "hello";
}
}
This is my application.yml file:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: <<company-website-here>>/uaa/oauth/token_keys
And this is my security configuration class:
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
#Override
protected void configure(HttpSecurity http) throws Exception
{
http.authorizeRequests()
.antMatchers(HttpMethod.GET, "/hello").hasRole("MY_ROLE")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt();
}
}
I can't seem to figure out why I seem to only get 403 errors. I have also tried adding #EnableWebSecurity to the security config class, but that didn't make a difference. Adding the auth server URL explicitly to the server and/or manually creating a JwtDecoder didn't do the trick either; it appears the url is being automatically picked up from the yml file, based on its property name.
I am trying to move away from using the org.springframework.security.oauth.boot dependency and ResourceServerConfigurerAdapter.
I had to add my own converter like so:
private static class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken>
{
private final Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter;
public JwtAuthenticationConverter()
{
this.jwtGrantedAuthoritiesConverter = jwt -> jwt
.getClaimAsStringList("authorities")
.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
#Override
public final AbstractAuthenticationToken convert(#NonNull Jwt jwt)
{
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
return new JwtAuthenticationToken(jwt, authorities, jwt.getClaimAsString("client_id"));
}
}
Then had to add this to the main security config:
.jwtAuthenticationConverter(new JwtAuthenticationConverter());
There may be a couple of things happening.
As you're migrating to Spring Security 5, you may need to extract your authorities manually. Check this post and it's correct answer.
You are using hasRole function and this will append "ROLE_" before your authority/role. So if the role on your JWT token is not ROLE_JWT_ROLE you should use
hasTransaction.

Basic Authentication using Swagger UI

I am trying to develop a spring-boot based rest API service with API documentation through Swagger UI. I want to enable basic authentication via the swagger UI so that the user can only run the API's once he/she authenticates using the Authorize button on swagger UI (by which a "authorization: Basic XYZ header is added to the API Call
At the front end (in the .json file for the Swagger UI I have added basic authentication for all the APIs using the following code (as per the documentation):
"securityDefinitions": {
"basic_auth": {
"type": "basic"
}
},
"security": [
{
"basic_auth": []
}
]
How should I implement the backend logic for the use case mentioned above (user can only run the API's once he/she authenticates using the Authorize button on swagger UI and it otherwise shows a 401 Error on running the API)
Some documentation or sample code for the same would be helpful
One option is to use the browser pop up authorization.
When you enable basic auth for your spring boot app, swagger ui will automatically use the browser's pop up window in order to use it for basic auth. This means that the browser will keep the credentials for making requests just like when you trying to access a secured GET endpoint until you close it.
Now, let's say you DON'T want to use the above and want swagger-ui for basic authentication as you say, you have to enable auth functionality on swagger-ui and optionally add security exception when accessing swagger-ui url.
To enable the basic auth functionality to swagger UI (with the "Authorize button" in UI) you have to set security Context and Scheme to your Swagger Docket (This is a simplified version):
#Configuration
#EnableSwagger2
public class SwaggerConfig implements WebMvcConfigurer{
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
.securityContexts(Arrays.asList(securityContext()))
.securitySchemes(Arrays.asList(basicAuthScheme()));
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(Arrays.asList(basicAuthReference()))
.forPaths(PathSelectors.ant("/api/v1/**"))
.build();
}
private SecurityScheme basicAuthScheme() {
return new BasicAuth("basicAuth");
}
private SecurityReference basicAuthReference() {
return new SecurityReference("basicAuth", new AuthorizationScope[0]);
}
}
This enables the authorization button in ui.
Now you probably want for your users to access the swagger-ui freely and use this button for authorization. To do this you have to exempt swagger for app's basic auth. Part of this configuration is Security config and you have to add following code:
public class SecurityConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers(
"/", "/csrf",
"/v2/api-docs",
"/swagger-resources/**",
"/swagger-ui.html",
"/webjars/**"
).permitAll()
.anyRequest().authenticated();
}
}
A similar problem I was facing was that when using springfox documentation with Swagger OAS 3.0, the "Authenticate" button would not appear on the swagger UI.
Turns out there was a bug created for this very issue-
https://github.com/springfox/springfox/issues/3518
The core of the problem-
Class BasicAuth is deprecated.
The solution as found in the bug report above is to use HttpAuthenticationScheme instead to define the SecurityScheme object.
The Docket configuration then looks like so-
return new Docket(DocumentationType.OAS_30)
.groupName("Your_Group_name")
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.mypackage"))
.paths(PathSelectors.regex("/.*"))
.build().securitySchemes(Arrays.asList(HttpAuthenticationScheme.BASIC_AUTH_BUILDER.name("basicAuth").description("Basic authorization").build()))
.securityContexts(); //define security context for your app here
Use a following dependency in build.gradle to enable a security:
"org.springframework.boot:spring-boot-starter-security"
In application.properties you can define your own username and password using:
spring.security.user.name=user
spring.security.user.password=password
Those who want to basic auth only for endpoints should do everything what #Sifis wrote but need to change antMatchers as:
public class SecurityConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers(
"/",
"/v2/api-docs/**",
"/v3/api-docs/**",
"/swagger-resources/**",
"/swagger-ui/**",
"/swagger-ui.html").permitAll()
.anyRequest().authenticated();
}
}

Spring Security .permitAll() no longer effective after upgrading to Spring Boot 2.0.2

I work with a web app that exposes a REST API to mobile apps. I upgraded my Spring Boot version from 1.5.3.RELEASE to 2.0.2.RELEASE and after fixing a few breaking changes I am facing one that I cannot solve.
I followed this Spring Boot 2.0 Migration Guide and Spring Boot Security 2.0 and also looked into Security changes in Spring Boot 2.0 M4.
The issue is that the app uses JWT authentication and there is an endpoint (/auth/login) accepts user credentials and generates a long-lived JWT in return.
There is a filter that examines the JWT token sent by the client and determines whether the client can access the requested resource.
Custom security config is like this:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfiguration {
#Configuration
#Order(1)
public class AuthenticationConfiguration extends WebSecurityConfigurerAdapter {
// Some dependencies omitted
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// we don't need CSRF because JWT token is invulnerable
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// don't create session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers(HttpMethod.GET, "/version/**").permitAll()
// Some more antMatchers() lines omitted
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated();
// Custom JWT based security filter
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity.headers().cacheControl();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter(jwtTokenUtil);
}
}
#Configuration
#Order(2)
public class ClientVersionSupportConfiguration extends WebMvcConfigurerAdapter {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry
.addInterceptor(versionCheckingFilter())
.addPathPatterns("/**")
.excludePathPatterns("/error"); // Some more endpoints omitted
}
#Bean
public VersionCheckingInterceptor versionCheckingFilter() {
return new VersionCheckingInterceptor();
}
}
}
Note the .antMatchers("/auth/**").permitAll() line. /auth endpoints should be accessible without JWT since the JWT has not yet been generated when the user has not yet logged in.
Before upgrading Spring Boot, it worked fine, now it is not working. Login attemps are rejected by the filter that checks the JWT. Looks like .permitAll() is not making the requests pass through. /version/** does not work either. Hitting it from the browser gives an error page.
I also tried to delete lines from the config until this remained:
httpSecurity
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
It did not help. Could you please help with restoring the original behavior?
Do you have a base path for you api, e.g. /api ?
The server.contextPath default Spring property name has changed to server.servlet.context-path.
So if you use a default base path for you api, you won't find the endpoints where you expect them. Unless you update the property ;)

Categories

Resources