Can I have Spring Boot create a WebClient which uses OAuth2 without it trying to apply the OAuth flow to my endpoints? - spring-boot

I want to have a WebClient which uses a client_credentials OAuth2 flow to authorize with an API. I've followed various sets of instructions from the documentation to several tutorials.
I feel like I'm pretty close to getting Spring Boot to do what I want, but the default behaviour is doing something I don't want - I get redirected to /login when I make a request to any of my controllers. I want to be able to (at the moment) do an unauthorized request to my API, and have the service-to-service call use the configured OAuth2 flow.
spring:
main:
web-application-type: reactive
security:
oauth2:
client:
registration:
my-private-api:
client-id: <foo>
client-secret: <bar>
authorization-grant-type: client_credentials
provider:
my-private-api:
token-uri: <uri>
#Bean
WebClient webClient( final ReactiveClientRegistrationRepository clientRegistrations,
final ReactiveOAuth2AuthorizedClientService authorizedClientService )
{
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction( new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrations,
authorizedClientService ) );
oauth.setDefaultClientRegistrationId( "my-private-api" );
return WebClient.builder().filter( oauth ).build();
}
Do I need to do lots of manual configuration in order to avoid this default behaviour I don't want?

So turns out my OAuth configuration was fine, what I was actually asking for was "how do I disable the default behaviour of Spring Security's SecurityWebFilterChain" - which is to add a bean overriding the behaviour:
#Bean
SecurityWebFilterChain springWebFilterChain( final ServerHttpSecurity http )
{
http.httpBasic().disable().formLogin().disable().csrf().disable().logout().disable();
return http.build();
}

Related

403 Forbidden Using Springboot When Hitting Okta Userinfo endpoint

I'm trying to set up Okta as a sign on for a set of subpaths in my spring boot app.
I'm configuring the auth resource details with:
#Bean(name = "oktaOAuthClient")
public AuthorizationCodeResourceDetails oktaOAuthAdminClient(#Qualifier("oktaAdminConfiguration") OktaConfigurationProperties oktaAdminCongfig,
ICredentialsApi credentialsApi) {
String redirectUrl = UriComponentsBuilder.fromUriString("http://localhost:8091/")
.path(ConfigurationRequestPaths.ADMINISTRATION_LANDING)
.build(false)
.toUriString();
AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();
client.setClientId(oktaAdminCongfig.getClientId());
client.setClientSecret(oktaAdminCongfig.getClientSecret());
client.setAccessTokenUri(oktaAdminCongfig.getAccessTokenUri());
client.setUserAuthorizationUri(oktaAdminCongfig.getUserAuthorizationUri());
client.setClientAuthenticationScheme(AuthenticationScheme.header);
client.setPreEstablishedRedirectUri(redirectUrl);
client.setScope(OKTA_SCOPES);
client.setUseCurrentUri(false);
client.setScope(OKTA_SCOPES);
return client;
}
These and other settings are found from the application.properties manually and are set as:
okta.admin.clientId={id}
okta.admin.clientSecret={secret}
okta.admin.accessTokenUri=https://dev-{value}.okta.com/oauth2/default/v1/token
okta.admin.userAuthorizationUri=https://dev-{value}.okta.com/oauth2/default/v1/authorize
okta.admin.issuer=https://dev-{value}.okta.com/oauth2/default
okta.admin.userInfoUrl=https://dev-{value}.okta.com/oauth2/default/v1/userinfo
Then I've made a filter with (Note, is the clientId set in the UserTokenInfoServices meant to be the client id from the okta client id/client secret?):
#Bean(name = "oktaFilter")
public Filter oktaFilter(#Qualifier("oktaOAuthClient") AuthorizationCodeResourceDetails oktaOAuthClient,
#Qualifier("oktaOAuthResource") ResourceServerProperties resource,
#Qualifier("oktaOAuthRestTemplate") OAuth2RestTemplate oktaOAuthRestTemplate) {
ExceptionMappingAuthenticationFailureHandler failureHandler = new ExceptionMappingAuthenticationFailureHandler();
failureHandler.setDefaultFailureUrl("/");
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(ConfigurationRequestPaths.ADMINISTRATION_LANDING);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(resource.getUserInfoUri(), oktaOAuthClient.getClientId());
tokenServices.setRestTemplate(oktaOAuthRestTemplate);
filter.setRestTemplate(oktaOAuthRestTemplate);
filter.setTokenServices(tokenServices);
SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setUseReferer(true);
filter.setAuthenticationSuccessHandler(successHandler);
filter.setAuthenticationFailureHandler(failureHandler);
return filter;
}
Finally I've set up the WebSecurityConfigurerAdapter with the following:
http.antMatcher("/config/**")
.authorizeRequests()
.antMatchers("/config")
.permitAll()
.anyRequest().authenticated().and()
.exceptionHandling()
.authenticationEntryPoint(oktaLoginHandler)SimpleUrlAuthenticationSuccessHandler(ConfigurationRequestPaths.ADMINISTRATION_LANDING))
.and()
.logout().addLogoutHandler(oktaLogoutHandler).logoutSuccessUrl(externalAccessUrl).permitAll().and()
.addFilterBefore(oktaFilter, BasicAuthenticationFilter.class);
}
The redirect for the subpath works correctly and goes to a sign in page, but I get an error after signing in that warns:
org.springframework.security.authentication.BadCredentialsException: Could not obtain user details from token...Caused by: org.springframework.security.oauth2.common.exceptions.InvalidTokenException:
I believe this likely has to do with getting a 403 when hitting the okta userinfo endpoint:
Request is to process authentication
Retrieving token from https://dev-{value}.okta.com/oauth2/default/v1/token
Encoding and sending form: {grant_type=[authorization_code], code=[{code}], redirect_uri=[http://localhost:8091/config], client_id=[{id}], client_secret=[{secret}]}
HTTP GET https://dev-{value}.okta.com/oauth2/default/v1/userinfo
Accept=[application/json, application/*+json]
Response 403
I've also tried the okta starter but it seems to break when used with another oauth login to github for another set of subpaths in the application. The spring version I'm using doesn't include the .oauthLogin() and other settings for httpsecurity that I've seen some guides on.
edit: Adding my spring dependency list for more clarification:
org.springframework:spring-beans:5.1.20.RELEASE
org.springframework:spring-context:5.1.20.RELEASE
org.springframework:spring-jdbc:5.1.20.RELEASE
org.springframework:spring-tx:5.1.20.RELEASE
org.springframework:spring-web:5.1.20.RELEASE
org.springframework:spring-webmvc:5.1.20.RELEASE
org.springframework:spring-test:5.1.20.RELEASE
org.springframework.boot:spring-boot-actuator:2.1.18.RELEASE
org.springframework.boot:spring-boot-autoconfigure:2.1.18.RELEASE
org.springframework.boot:spring-boot-configuration-processor:2.1.18.RELEASE
org.springframework.boot:spring-boot-starter:2.1.18.RELEASE
org.springframework.boot:spring-boot-starter-actuator:2.1.18.RELEASE
org.springframework.boot:spring-boot-starter-security:2.1.18.RELEASE
org.springframework.boot:spring-boot-starter-thymeleaf:2.1.18.RELEASE
org.springframework.boot:spring-boot-starter-web:2.1.18.RELEASE
org.springframework.boot:spring-boot-starter-test:2.1.18.RELEASE
org.springframework.retry:spring-retry:1.3.1
org.springframework.security:spring-security-config:5.1.13.RELEASE
org.springframework.security:spring-security-core:5.1.13.RELEASE
org.springframework.security:spring-security-ldap:5.1.13.RELEASE
org.springframework.security:spring-security-web:5.1.13.RELEASE
org.springframework.security.oauth:spring-security-oauth2:2.3.8.RELEASE
org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.1.18.RELEASE
It sounds like you might be using the older Spring Security OAuth project:
spring-security-oauth?
This project has been deprecated. The newer Spring Security OAuth2 modules are great, and they are now first-class citizens, in Spring Security (they live in the official project now). Along with this Spring Boot 1.x is EoL, and is no longer getting patches and security updates.
Most of the guides you are seeing likely reference the newer libraries (e.g. things like .oauthLogin()).
Sorry for the typical StackOverflow answer of "don't do X", but here is what I would recommend:
Update your Spring Boot version
Migrate the newer OAuth libraries
Then add your new logic (this should be much easier after updating)
If you are already on Spring Boot 2 and the newer OAuth lib, let me know, and we can try to figure out why you don't have the newer HttpSecurity methods.

Authorization not working in Gateway with OAuth2 Client + Resource Server

I am using the following dependencies in one application: Spring-Cloud-Gateway, Spring Boot OAuth2 Client, Spring Boot OAuth2 Resource Server.
I use the following security config:
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveClientRegistrationRepository clientRegistrationRepository) {
http.oauth2Login();
http.logout(logout -> logout.logoutSuccessHandler(
new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));
http.authorizeExchange()
.pathMatchers("/actuator/health").permitAll()
.pathMatchers("/auth/realms/ahearo/protocol/openid-connect/token").permitAll()
.pathMatchers("/v3/api-docs").permitAll()
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(userJwtAuthenticationConverter());
http.csrf().disable().formLogin().disable().httpBasic().disable();
return http.build();
}
#Bean
public UserJwtAuthenticationConverter userJwtAuthenticationConverter() {
return new UserJwtAuthenticationConverter();
}
When I execute calls I am correctly advised to login which works fine. But it's just the Authentication that works, not the Authorization. When I use the debugger I can see that the userJwtAuthenticationConverter() method is never called to use roles out of the JWT.
When I use the same method in another application/microservice which is just a OAuth2 Resource Server, but not a OAuth2 Client the method is correctly called and executed.
The security config in the application.yaml looks like the following in the Spring Cloud Gateway application:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost/auth/realms/example-realm
jwk-set-uri: http://localhost/auth/realms/example-realm/protocol/openid-connect/certs
client:
registration:
keycloak:
client-id: 'example-proxy-client'
client-secret: 'xxx'
authorizationGrantType: authorization_code
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
scope: openid,profile,email
provider:
keycloak:
issuer-uri: http://localhost/auth/realms/example-realm
user-name-attribute: preferred_username
Isn't it possible for the Spring Cloud Gateway application to perform as a OAuth2 Client and Resource Server at the same time or am I doing a mistake regarding the configuring of the application?
Turns out that I misunderstood some basic OAuth2 concepts.
The Authorization itself is working correctly when I send a JWT in the Authorization header (Implicit Flow). What did not work is the case when I tried to access a resource via the browser.
I got redirected to the login page of Keycloak (Authorization Code Flow). After you sign in through the login page of Keycloak you do not receive the JWT but a Keycloak Session ID. Spring Cloud Gateway cannot perform Authorization on the basis of a Keycloak Session ID (do not know how that would work if I wanted to use Authorization Code Flow but I am using Implicit flow, so I am fine for now).

Reactive spring cloud security (with Keycloak): Session expiration?

I try implementing the following: I want a distributed environment with one or more spring cloud gateway(s), behind these are several micro services (partly) exposed to the outside. For user authentication I want to use OIDC (just moved to Keycloak).
I actually just sticked to the standard configuration from the reference documentations of spring security, webflux and boot.
In detail, I have in the gateway:
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/login").permitAll()
.pathMatchers("/actuator/**").permitAll()
.anyExchange().authenticated()
.and()
.oauth2Login()
.and()
.logout(logout -> logout.logoutSuccessHandler(oidcLogoutSuccessHandler()));
return http.build();
}
private ServerLogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedServerLogoutSuccessHandler oidcLogoutSuccessHandler =
new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository);
oidcLogoutSuccessHandler.setPostLogoutRedirectUri(URI.create("https://<host>/login?logout"));
return oidcLogoutSuccessHandler;
}
with application.yml (only essential part):
spring:
cloud:
gateway:
default-filters:
- TokenRelay=
- RemoveRequestHeader=Cookie
discovery:
locator:
enabled: true
consul:
host: 127.0.0.1
port: 8500
loadbalancer:
ribbon:
enabled: false
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: ${KEYCLOAK_ISSUER_URI}
registration:
keycloak:
client-id: ${KEYCLOAK_CLIENT_ID}
client-secret: ${KEYCLOAK_CLIENT_SECRET}
resourceserver:
jwt:
jwk-set-uri: ${KEYCLOAK_ISSUER_URI}/protocol/openid-connect/certs
server:
forward-headers-strategy: framework
For the proof of concept I have an "environment-test-application" with a controller just returning the claims as a json. It is configured like:
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.pathMatchers("/actuator/**").permitAll()
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt(Customizer.withDefaults());
return http.build();
}
The application.yml only contains the consul and security part from above.
Basically this works. If I try to access the environment-test-application, I get redirected to Keycloak, I have to enter my credentials and get redirected back to the environment-test-application and see the claims. I can logout by means of "/logout".
The problem: After 5 minutes the keycloak session expires, I get a 401 and chrome says "this page is not working". This happens even, if I am reloading all the time (user activity).
To my understanding the session should:
be extended as long as there is activity (I thought this is what the refresh token is used for, and assumed spring security to handle this automatically)
expire when the user is inactive for some time. When the user is active again, he/she should be redirected to the login and then back to the original resource.
when hitting "remember me", the an expired session should be reinitiated without user activity (I think, this is what the offline token from Keycloak is meant for).
I guess all this is possible by adding just some simple lines of configuration, I just can't figure which. The reference documentation wasn't entirely clear to me, but I got the feeling this is all handled like so, by default. However, it is not.
Note: I am using spring reactive stack with the default Reactor Netty. Thus, neither the spring security, nor the spring boot plugin provided by Keycloak, can be used. Plus, it is not clear to me, how they interact with the standard configuration scheme.
Note: Before I was trying all this with okta (using their starter). The problem described above magically seemed to be working with okta. However, I had different issues and thus moved to open source now.
TokenRelayGatewayFilterFactory adds the access token but doesn't refresh it when it expires...which is why you get the 401 I believe. There's an open spring cloud gateway issue asking for a solution that refreshes as well. One of the comments on that issue provides an implementation: https://github.com/spring-cloud/spring-cloud-security/issues/175#issuecomment-557135243.
When the refresh token filter is working, the keycloak session only becomes important after your spring cloud gateway "session" expires because, if the keycloak session is still good, it allows the oauth2 redirect to re-establish the session seemlessly (i.e. without having to enter your credentials again).
If you're looking to customize the session of your webflux application, there are some solutions including this one: https://stackoverflow.com/a/62344617/1098564
Not sure what you're using for your frontend but he's how I use spring security with spring cloud gateway and react: https://sdoxsee.github.io/blog/2019/12/17/merry-microservices-part2-ui-gateway

Use oauth2 scope instead of role to secure spring actuator management endpoints

I've upgraded to Spring Cloud Dalston recently, that means Spring Boot 1.5.1, and I can not secure the management endpoints by checking an oauth2 scope anymore. It worked before in Spring Cloud Camden.
This is the configuration that worked before :
#Configuration
public class SecurityConfiguration extends ResourceServerConfigurerAdapter {
#Value("${management.context-path}")
private String managementContextPath;
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// some paths I don't want to secure at all
.antMatchers("/path1/**", "/path2/**").permitAll()
// access to health endpoint is open to anyone
.antMatchers(HttpMethod.GET, managementContextPath + "/health").permitAll()
// but app.admin scope is necessary for other management endpoints
.antMatchers(managementContextPath + "/**").access("#oauth2.hasScope('my-super-scope')") //
// And we make sure the user is authenticated for all the other cases
.anyRequest().authenticated();
}
}
And this is the important part of the config :
security:
oauth2:
client:
clientId: a-client
clientSecret: the-client-password
resource:
tokenInfoUri: http://my-spring-oauth2-provider/oauth/check_token
management:
context-path: /my-context
security:
enabled: true
endpoints:
health:
sensitive: false
When I try to POST on /my-context/refresh I get a HTTP 401 "Full authentication is needed" even though I give a valid OAuth2 token
Looking through the log I saw that my request was considered anonymous, and checking the FilterChainProxy log saw that the OAuth2AuthenticationProcessingFilter was not called. After a bit of digging I found that I could change the oauth2 resource filter order, so I tried that and now I have an OAuth2 authentication, yeah, finished right ?
Hum, no, now I have an Access is denied. User must have one of the these roles: ACTUATOR error.
I tried a few other things, including disabling management security (but my rules are not applied and access is open to everyone), playing with (ugh) #Order (no change), and even, lo and behold, reading and applying the documentation which says :
To override the application access rules add a #Bean of type
WebSecurityConfigurerAdapter and use
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) if you don’t want to
override the actuator access rules, or
#Order(ManagementServerProperties.ACCESS_OVERRIDE_ORDER) if you do
want to override the actuator access rules.
But this did not change the error : User must have one of these roles: ACTUATOR
Does anybody have a workaround/idea ?
Update : I'm also using Zuul, so I finally created a specific zuul route to the endpoint I needed (cloud bus refresh in my case), unprotected on an other backend service that was not exposed otherwise, and protected that zuul route with oauth2.
I'm leaving this question here nevertheless, if anyone finds a workaround, could be useful.
Probably being captain obvious, but see http://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-monitoring.html. You can override the role with management.security.roles and simply add whatever role your Oauth2 credentials have.
I confronted with this issue also. The workaround that I used was to expose the actuator action on a new endpoint which I defined, and just call the actuator bean to handle request.
For example to secure /my-context/refresh with Oauth2 , I just expose a new resource at {whatever-api-prefix}/refreshConfig and I exposed a request handler on the rest controller for this URL; in the rest controller I wire the RefreshEndpoint bean and in the request handler I just call the refreshEndpoint.refresh().

Spring Oauth for Web Application

I have a query I am struggling to come to a decent answer with and hoping by vocalizing it someone might be able to guide me or advise me in the right direction.
All our current web applications are build using Spring, which includes the security which is handled by Spring Security module of course.
We are exploring the opportunities of integrating some of our new android projects into these web applications.
After some research and guidance, all flags point towards OAuth2 implementation in Android App and using that to obtain access to relevant parts of the web application server via Restfull calls.
What I am trying to understand now is can we and should we replace our existing Spring Security implementation in our web application and replace it with Spring Oauth2 equivalent.
The overall goal would to be able to have a single security solution that we would use for both website login, app login, and any API implementations that would be exposed to other web applications.
If anyone can also provide a link to a Spring Oauth2 Java Config (not-XML) setup where a user logs in and accesses a page based on their role in a unrestful manner, would also be extremely helpful. Most examples we have found were either extremely complex, focused solely on restfull calls, or only XML configurations.
Seems you did not hear about spring-cloud-security project which extremely simplifies working with oauth2 providers. Take a look at:
http://cloud.spring.io/spring-cloud-security/
There are samples available on github ( right side of above page ). It shows how to set up in case you want to use just one oauth2 provider - check project which shows how to do that for github https://github.com/spring-cloud-samples/sso . You do most of this stuff through configuration in application.yml file + add #EnableOAuth2Sso annotation so all machinery starts.
For 'normal' interaction with your web app its pretty straighforward. If you need your app to work as an api for others then you have to maintain tokens, add them to request etc. If client of your api is also your code which you can change then you may use OAuth2RestTemplate class which take care about tokens, so you do queries to your api as it was usual/not secured endpoint.
If you need to use for example two different providers for different paths then its more complicated, you have to configure them all like:
#Bean
public OAuth2RestTemplate facebookOAuth2RestTemplate(OAuth2ClientContext clientContext) {
return new OAuth2RestTemplate(facebookOAuth2ResourceDetails(), clientContext);
}
#Bean
public OAuth2ProtectedResourceDetails facebookOAuth2ResourceDetails() {
AuthorizationCodeResourceDetails resource = new AuthorizationCodeResourceDetails();
resource.setAccessTokenUri(tokenUri);
resource.setId("id");
resource.setUserAuthorizationUri(authorizationUri);
resource.setUseCurrentUri(false);
resource.setPreEstablishedRedirectUri(redirectUri);
resource.setClientId(clientId);
resource.setClientSecret(clientSecret);
resource.setTokenName("tokenname");
resource.setAuthenticationScheme(AuthenticationScheme.query);
resource.setClientAuthenticationScheme(AuthenticationScheme.form);
return resource;
}
and decide which instance of OAuth2RestTemplate to use in which case.
//update
If you want to exchange spring security core with authorizing users by some oauth2 provider you can extends OAuth2SsoConfigurerAdapter class:
#Configuration
#EnableOAuth2Sso
public class WebSecurityConfig extends OAuth2SsoConfigurerAdapter {
#Override
public void match(RequestMatchers matchers) {
matchers.antMatchers("/admin");
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin").hasRole("ADMIN")
.and()
.logout().logoutUrl("/logout").permitAll()
.logoutSuccessUrl("/");
}
So /admin will be now protected, and user redirected to any authorization server you specify. It requires oauth configuration in application.yml.
spring:
oauth2:
client: # Sauron
clientId: app_clientid
clientSecret: app_secret
accessTokenUri: http://authorizationserver/oauth/token
userAuthorizationUri: http://authorizationserver/oauth/authorize
clientAuthenticationScheme: header
resource:
tokenInfoUri: http://authorizationserver/oauth/check_token
preferTokenInfo: false
Thats why I wrote before that its easy to use just one auhorization server, but in case you need more then its more complicated.

Resources