Oauth2 bad credentials Spring Boot - oauth

I am getting the error:
"error": "invalid_grant",
"error_description": "Bad credentials"
Here is the request which I make:
POST /oauth/token HTTP/1.1
Host: localhost:8443
Authorization: Basic bW9iaWxlOg==
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded
username=admin&password=pass&client_id=mobile&grant_type=password&client_secret=
My code is from here: https://github.com/juleswhite/mobilecloud-14/tree/master/examples/9-VideoServiceWithOauth2
Here is the code:
Application.java:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import com.capstone.auth.OAuth2SecurityConfiguration;
import com.google.common.io.BaseEncoding;
#Configuration
#EnableAutoConfiguration
#ComponentScan
#EnableWebMvc
#Import(OAuth2SecurityConfiguration.class)
public class Application extends RepositoryRestMvcConfiguration{
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Application.class, args);
}
}
ClientAndUserDetailsService.java
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService;
/**
* A class that combines a UserDetailsService and ClientDetailsService
* into a single object.
*
* #author jules
*
*/
public class ClientAndUserDetailsService implements UserDetailsService,
ClientDetailsService {
private final ClientDetailsService clients_;
private final UserDetailsService users_;
private final ClientDetailsUserDetailsService clientDetailsWrapper_;
public ClientAndUserDetailsService(ClientDetailsService clients,
UserDetailsService users) {
super();
clients_ = clients;
users_ = users;
clientDetailsWrapper_ = new ClientDetailsUserDetailsService(clients_);
}
#Override
public ClientDetails loadClientByClientId(String clientId)
throws ClientRegistrationException {
return clients_.loadClientByClientId(clientId);
}
#Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserDetails user = null;
try{
user = users_.loadUserByUsername(username);
}catch(UsernameNotFoundException e){
user = clientDetailsWrapper_.loadUserByUsername(username);
}
return user;
}
}
OAuth2SecurityConfiguration.java
#Configuration
public class OAuth2SecurityConfiguration {
// This first section of the configuration just makes sure that Spring
// Security picks
// up the UserDetailsService that we create below.
#Configuration
#EnableWebSecurity
protected static class WebSecurityConfiguration extends
WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
protected void registerAuthentication(
final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
/**
* This method is used to configure who is allowed to access which parts of
* our resource server (i.e. the "/video" endpoint)
*/
#Configuration
#EnableResourceServer
protected static class ResourceServer extends
ResourceServerConfigurerAdapter {
// This method configures the OAuth scopes required by clients to access
// all of the paths in the video service.
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().antMatchers("/oauth/token").anonymous();
// If you were going to reuse this class in another
// application, this is one of the key sections that you
// would want to change
// Require all GET requests to have client "read" scope
http.authorizeRequests().antMatchers(HttpMethod.GET, "/**")
.access("#oauth2.hasScope('read')");
// Require all other requests to have "write" scope
http.authorizeRequests().antMatchers("/**")
.access("#oauth2.hasScope('write')");
}
}
/**
* This class is used to configure how our authorization server (the
* "/oauth/token" endpoint) validates client credentials.
*/
#Configuration
#EnableAuthorizationServer
#Order(Ordered.LOWEST_PRECEDENCE - 100)
protected static class OAuth2Config extends
AuthorizationServerConfigurerAdapter {
// Delegate the processing of Authentication requests to the framework
#Autowired
private AuthenticationManager authenticationManager;
// A data structure used to store both a ClientDetailsService and a
// UserDetailsService
private ClientAndUserDetailsService combinedService_;
/**
*
* This constructor is used to setup the clients and users that will be
* able to login to the system. This is a VERY insecure setup that is
* using hard-coded lists of clients / users / passwords and should
* never be used for anything other than local testing on a machine that
* is not accessible via the Internet. Even if you use this code for
* testing, at the bare minimum, you should consider changing the
* passwords listed below and updating the VideoSvcClientApiTest.
*
* #param auth
* #throws Exception
*/
public OAuth2Config() throws Exception {
// If you were going to reuse this class in another
// application, this is one of the key sections that you
// would want to change
// Create a service that has the credentials for all our clients
ClientDetailsService csvc = new InMemoryClientDetailsServiceBuilder()
// Create a client that has "read" and "write" access to the
// video service
.withClient("mobile")
.authorizedGrantTypes("password")
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
.scopes("read", "write")
.resourceIds("test")
.and()
// Create a second client that only has "read" access to the
// video service
.withClient("mobileReader")
.authorizedGrantTypes("password")
.authorities("ROLE_CLIENT").scopes("read")
.resourceIds("test").accessTokenValiditySeconds(3600)
.and().build();
// Create a series of hard-coded users.
UserDetailsService svc = new InMemoryUserDetailsManager(
Arrays.asList(
User.create("admin", "pass", "ADMIN", "USER"),
User.create("user0", "pass", "USER"),
User.create("username", "password", "USER")));
// Since clients have to use BASIC authentication with the client's
// id/secret,
// when sending a request for a password grant, we make each client
// a user
// as well. When the BASIC authentication information is pulled from
// the
// request, this combined UserDetailsService will authenticate that
// the
// client is a valid "user".
combinedService_ = new ClientAndUserDetailsService(csvc, svc);
}
/**
* Return the list of trusted client information to anyone who asks for
* it.
*/
#Bean
public ClientDetailsService clientDetailsService() throws Exception {
return combinedService_;
}
/**
* Return all of our user information to anyone in the framework who
* requests it.
*/
#Bean
public UserDetailsService userDetailsService() {
return combinedService_;
}
/**
* This method tells our AuthorizationServerConfigurerAdapter to use the
* delegated AuthenticationManager to process authentication requests.
*/
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager);
}
/**
* This method tells the AuthorizationServerConfigurerAdapter to use our
* self-defined client details service to authenticate clients with.
*/
#Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.withClientDetails(clientDetailsService());
}
}
// This version uses the Tomcat web container and configures it to
// support HTTPS. The code below performs the configuration of Tomcat
// for HTTPS. Each web container has a different API for configuring
// HTTPS.
//
// The app now requires that you pass the location of the keystore and
// the password for your private key that you would like to setup HTTPS
// with. In Eclipse, you can set these options by going to:
// 1. Run->Run Configurations
// 2. Under Java Applications, select your run configuration for this app
// 3. Open the Arguments tab
// 4. In VM Arguments, provide the following information to use the
// default keystore provided with the sample code:
//
// -Dkeystore.file=src/main/resources/private/keystore
// -Dkeystore.pass=changeit
//
// 5. Note, this keystore is highly insecure! If you want more securtiy, you
// should obtain a real SSL certificate:
//
// http://tomcat.apache.org/tomcat-7.0-doc/ssl-howto.html
//
#Bean
EmbeddedServletContainerCustomizer containerCustomizer(
#Value("${keystore.file:src/main/resources/private/keystore}") String keystoreFile,
#Value("${keystore.pass:changeit}") final String keystorePass)
throws Exception {
// If you were going to reuse this class in another
// application, this is one of the key sections that you
// would want to change
final String absoluteKeystoreFile = new File(keystoreFile)
.getAbsolutePath();
return new EmbeddedServletContainerCustomizer() {
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
TomcatEmbeddedServletContainerFactory tomcat = (TomcatEmbeddedServletContainerFactory) container;
tomcat.addConnectorCustomizers(new TomcatConnectorCustomizer() {
#Override
public void customize(Connector connector) {
connector.setPort(8443);
connector.setSecure(true);
connector.setScheme("https");
Http11NioProtocol proto = (Http11NioProtocol) connector
.getProtocolHandler();
proto.setSSLEnabled(true);
proto.setKeystoreFile(absoluteKeystoreFile);
proto.setKeystorePass(keystorePass);
proto.setKeystoreType("JKS");
proto.setKeyAlias("tomcat");
}
});
}
};
}
}
Thanks for your attention and time!

Short answer: you can't #Autowired an AuthenticationManager into a AuthorizationServerConfigurerAdapter if you are using Spring Boot (yet).
Long anwswer: this sample works though because it autowires the AuthenticationManagerBuilder instead, and constructs a lazy-init version of the AuthenticationManager for the token granter to use. With Spring OAuth2 2.0.3 you will have to create that lazy AuthenticationManager yourself (like this:
authenticationManager = new AuthenticationManager() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return auth.getOrBuild().authenticate(authentication);
}
};
With snapshots (or 2.0.4 when it is released) you can just use the new overloaded method in AuthorizationServerConfigurerAdapter.

Related

My SparkJava resource server gets 403 errors when trying to validate access tokens

I want to set up a very basic REST API using Spark-java, which just checks an access token obtained from my own authorisation server. It creates a GET request to the authorisation server's /oauth/authorize endpoint followed by ?token=$ACCESS_TOKEN.
Whenever I try this, I get diverted to the /error endpoint and a 403 error.
Here's my API class:
import org.apache.http.client.methods.HttpGet;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.utils.StringUtils;
import java.io.IOException;
import static spark.Spark.*;
public class SampleAPI {
private static final Logger logger = LoggerFactory.getLogger("SampleAPI");
public static void main(String[] args) {
// Run on port 9782
port(9782);
// Just returns "Hello there" to the client's console
before((preRequest, preResponse) -> {
System.out.println("Getting token from request");
final String authHeader = preRequest.headers("Authorization");
//todo validate token, don't just accept it because it's not null/empty
if(StringUtils.isEmpty(authHeader) || !isAuthenticated(authHeader)){
halt(401, "Access not authorised");
} else {
System.out.println("Token = " + authHeader);
}
});
get("/", (res, req) -> "Hello there");
}
private static boolean isAuthenticated(final String authHeader) {
String url = "http://localhost:9780/oauth/authorize";
//"Bearer " is before the actual token in authHeader, so we need to extract the token itself as a substring
String token = authHeader.substring(7);
HttpGet getAuthRequest = new HttpGet(url + "?token=" + token);
getAuthRequest.setHeader("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
CloseableHttpClient httpClient = HttpClients.createMinimal();
try {
CloseableHttpResponse response = httpClient.execute(getAuthRequest);
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("Status code " + statusCode + " returned for access token " + authHeader);
return statusCode == 200;
} catch (IOException ioException) {
System.out.println("Exception when trying to validate access token " + ioException);
}
return false;
}
}
The System.out.println statements are just for debugging.
Here's my authorisation server's WebSecurityConfigurerAdapter class:
package main.config;
import main.service.ClientAppDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
#Configuration
#EnableWebSecurity(debug = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
//returns AuthenticationManager from the superclass for authenticating users
return super.authenticationManagerBean();
}
#Bean
public PasswordEncoder getPasswordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
#Override
public void configure(WebSecurity web) throws Exception {
//Allow for DB access without any credentials
web.ignoring().antMatchers("/h2-console/**");
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//configures user details, and uses the custom UserDetailsService to check user credentials
auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
//disable CORS and CSRF protection for Postman testing
http.cors().disable().anonymous().disable();
http.headers().frameOptions().disable();
http.csrf().disable();
}
}
And here's my authorisation server's application.properties:
server.port=9780
#in-memory database, will get populated using data.sql
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=admin
spring.datasource.password=syst3m
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.properties.hibernate.format_sql=true
#adds to existing DB instead of tearing it down and re-populating it every time the app is started
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.h2.console.settings.trace=false
spring.h2.console.settings.web-allow-others=false
What have I done wrong? Do I need to specify my API as a resource server using Spring Security? Do I need to add it to the authorisation server's application.properties?
If you want to use Spring as a security framework then the most common option is to configure it as a resource server. Here is a getting started tutorial. The API will then never get redirected.
With Spark another option is to just provide a basic filter that uses a JWT validation library, such as jose4j. This tends to provide better control over error responses and gives you better visibility over what is going on. See this Kotlin example, which will be easy enough to translate to Java.

Spring fails for userinfo endpoint returning signed JWT

We're working on a Spring Boot application that is an OIDC client. The identity provider (IdP) is a third-party service and fully OpenID Connect and OAuth 2.0 compliant (as far as we can tell). As it's built with high security in mind, its UserInfo endpoint returns a signed JWT (instead of a regular one).
It seems that Spring Security does not support it. The authentication flow ends with an error message (displayed in an HTML page generated by our Spring application):
[invalid_user_info_response] An error occurred while attempting to
retrieve the UserInfo Resource: Could not extract response: no
suitable HttpMessageConverter found for response type
[java.util.Map] and content type
[application/jwt;charset=UTF-8]
My questions:
Is it correct that Spring does not currently support UserInfo endpoints returning signed JWTs?
If so, how can we add support for signed JWTs (including verfication of the signature)?
Our analysis has shown that the DefaultOAuth2UserService requests (Accept: application/json) and expects a JSON response from the IdP. However, being configured for high security, the IdP returns a signed JWT with the content type application/jwt. The response looks like the example on jwt.io. As the RestTemplate has no message converter capable of handling the content type application/jwt, the authenticaton fails.
Our sample app is as simple as it gets:
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.2.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}
DemoApplication.java
package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
application.yml
server:
port: 8081
spring:
security:
oauth2:
client:
registration:
demo:
client-id: our-client-id
client-secret: our-client-secret
clientAuthenticationMethod: post
provider: our-idp
scope:
- profile
- email
provider:
our-idp:
issuer-uri: https://login.idp.com:443/idp/oauth2
HomeController.java
package demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class HomeController {
#GetMapping("/")
String hello() { return "hello"; }
}
After more analysis, it seems Spring Boot does not support UserInfo endpoints returning signed JWTs. It's obviously an unusual setup (but still within the OAuth 2.0 / OIDC specification). What I haven't mentioned so far is that the JWT is signed with the client secret.
While Spring Boot doesn't support it, it can be added. The solution consists of:
A user service supporting signed JWTs (as a replacment for DefaultOAuth2UserService)
A HttpMessageConverter supporting JWTs (used in the user service's RestTemplate)
A JwtDecoder using the client secret
A security configuration that puts the pieces together
Note that we have changed from OAuth 2.0 to OIDC in the mean-time, thus our application.yml now includes the openid scope.
spring:
security:
oauth2:
client:
registration:
demo:
client-id: our-client-id
client-secret: our-client-secret
clientAuthenticationMethod: post
provider: our-idp
scope:
- profile
- email
provider:
our-idp:
issuer-uri: https://login.idp.com:443/idp/oauth2
The security configuration is:
package demoapp;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final ClientRegistrationRepository clientRegistrationRepository;
public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.oidcUserService(oidcUserService());
}
#Bean
OidcUserService oidcUserService() {
OidcUserService userService = new OidcUserService();
userService.setOauth2UserService(new ValidatingOAuth2UserService(jwtDecoderUsingClientSecret("demo")));
return userService;
}
JwtDecoder jwtDecoderUsingClientSecret(String registrationId) {
ClientRegistration registration = clientRegistrationRepository.findByRegistrationId(registrationId);
SecretKeySpec key = new SecretKeySpec(registration.getClientSecret().getBytes(StandardCharsets.UTF_8), "HS256");
return NimbusJwtDecoder.withSecretKey(key).build();
}
}
If you are using OAuth 2.0 instead of OIDC (i.e. you don't use the scope 'openid'), the configuration is simpler:
package demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final ClientRegistrationRepository clientRegistrationRepository;
public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(new ValidatingOAuth2UserService(jwtDecoderUsingClientSecret("demo")));
}
JwtDecoder jwtDecoderUsingClientSecret(String registrationId) {
ClientRegistration registration = clientRegistrationRepository.findByRegistrationId(registrationId);
SecretKeySpec key = new SecretKeySpec(registration.getClientSecret().getBytes(StandardCharsets.UTF_8), "HS256");
return NimbusJwtDecoder.withSecretKey(key).build();
}
}
The ValidatingOAuth2UserService class is - for the most part - a copy of DefaultOAuth2UserService:
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package demo;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequestEntityConverter;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
/**
* An implementation of an {#link OAuth2UserService} that supports standard OAuth 2.0 Provider's.
* <p>
* This provider supports <i>UserInfo</i> endpoints returning user details
* in signed JWTs (content-type {#code application/jwt}).
* </p>
* <p>
* For standard OAuth 2.0 Provider's, the attribute name used to access the user's name
* from the UserInfo response is required and therefore must be available via
* {#link ClientRegistration.ProviderDetails.UserInfoEndpoint#getUserNameAttributeName() UserInfoEndpoint.getUserNameAttributeName()}.
* <p>
* <b>NOTE:</b> Attribute names are <b>not</b> standardized between providers and therefore will vary.
* Please consult the provider's API documentation for the set of supported user attribute names.
*
* #see org.springframework.security.oauth2.client.userinfo.OAuth2UserService
* #see OAuth2UserRequest
* #see OAuth2User
* #see DefaultOAuth2User
*/
public class ValidatingOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri";
private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();
private RestOperations restOperations;
private JwtDecoder jwtDecoder;
public ValidatingOAuth2UserService(JwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
restTemplate.getMessageConverters().add(new JwtHttpMessageConverter());
this.restOperations = restTemplate;
}
#Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
Assert.notNull(userRequest, "userRequest cannot be null");
if (!StringUtils.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_USER_INFO_URI_ERROR_CODE,
"Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: " +
userRequest.getClientRegistration().getRegistrationId(),
null
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
if (!StringUtils.hasText(userNameAttributeName)) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: " +
userRequest.getClientRegistration().getRegistrationId(),
null
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
ResponseEntity<String> response;
try {
response = this.restOperations.exchange(request, String.class);
} catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
StringBuilder errorDetails = new StringBuilder();
errorDetails.append("Error details: [");
errorDetails.append("UserInfo Uri: ").append(
userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri());
errorDetails.append(", Error Code: ").append(oauth2Error.getErrorCode());
if (oauth2Error.getDescription() != null) {
errorDetails.append(", Error Description: ").append(oauth2Error.getDescription());
}
errorDetails.append("]");
oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the UserInfo Resource: " + errorDetails.toString(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
} catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the UserInfo Resource: " + ex.getMessage(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
}
Jwt jwt = decodeAndValidateJwt(response.getBody());
Map<String, Object> userAttributes = jwt.getClaims();
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OAuth2UserAuthority(userAttributes));
OAuth2AccessToken token = userRequest.getAccessToken();
for (String authority : token.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
}
private Jwt decodeAndValidateJwt(String token) {
return jwtDecoder.decode(token);
}
/**
* Sets the {#link Converter} used for converting the {#link OAuth2UserRequest}
* to a {#link RequestEntity} representation of the UserInfo Request.
*
* #since 5.1
* #param requestEntityConverter the {#link Converter} used for converting to a {#link RequestEntity} representation of the UserInfo Request
*/
public final void setRequestEntityConverter(Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter) {
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
this.requestEntityConverter = requestEntityConverter;
}
/**
* Sets the {#link RestOperations} used when requesting the UserInfo resource.
*
* <p>
* <b>NOTE:</b> At a minimum, the supplied {#code restOperations} must be configured with the following:
* <ol>
* <li>{#link ResponseErrorHandler} - {#link OAuth2ErrorResponseErrorHandler}</li>
* </ol>
*
* #since 5.1
* #param restOperations the {#link RestOperations} used when requesting the UserInfo resource
*/
public final void setRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
}
}
And finally the JwtHttpMessageConverter class:
package demo;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
/**
* Message converter for reading JWTs transmitted with content type {#code application/jwt}.
* <p>
* The JWT is returned as a string and not validated.
* </p>
*/
public class JwtHttpMessageConverter extends AbstractGenericHttpMessageConverter<String> {
public JwtHttpMessageConverter() {
super(MediaType.valueOf("application/jwt"));
}
#Override
protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return getBodyAsString(inputMessage.getBody());
}
#Override
public String read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return readInternal(null, inputMessage);
}
private String getBodyAsString(InputStream bodyStream) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] chunk = new byte[64];
int len;
while ((len = bodyStream.read(chunk)) != -1) {
buffer.write(chunk, 0, len);
}
return buffer.toString(StandardCharsets.US_ASCII);
}
#Override
protected void writeInternal(String stringObjectMap, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
throw new UnsupportedOperationException();
}
}
Thanks #Codo. You saved my day. I did not have to use JwtHttpMessageConverter class though. I added the below in SecurityConfig class. I had to use JwksUri and SignatureAlgorithm.RS512 in my situation.
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.
authorizeRequests().
anyRequest().authenticated().
and().
oauth2Login().userInfoEndpoint().oidcUserService(oidcUserService());
return http.build();
}
OidcUserService oidcUserService() {
logger.info("OidcUserService bean");
OidcUserService userService = new OidcUserService();
//userService.setOauth2UserService(new ExampleOAuth2UserService(jwtDecoderUsingClientSecret("web-client")));
userService.setOauth2UserService(new ExampleOAuth2UserService(jwtDecoder()));
return userService;
}
public JwtDecoder jwtDecoder() {
String jwksUri = Config.getJwksUri();
System.out.println("jwtDecoder jwksUri="+jwksUri);
return NimbusJwtDecoder.withJwkSetUri(jwksUri).jwsAlgorithm(SignatureAlgorithm.RS512).build();
}
public class ExampleOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
...............
...............
private JwtDecoder jwtDecoder;
public ExampleAuth2UserService(JwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
this.restOperations = restTemplate;
}

Very slow performance of spring-amqp comsumer

I've been experiencing troubles with spring-boot consumer. I compared the work of two consumers.
First consumer:
import com.rabbitmq.client.*;
import java.io.IOException;
public class Recv {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
Consumer consumer = new DefaultConsumer(channel) {
#Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
}
};
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
Second consumer:
#Controller
public class Consumer {
#RabbitListener(queues = "hello")
public void processMessage(Message message) {
}
}
There are no config files for spring-boot consumer installed, everything goes by default.
On my computer first one works 10 times faster. What might be the problem?
The default prefetch (basicQos) for Spring AMQP consumers is 1 which means only 1 message is outstanding at the consumer at any one time; configure the rabbitListenerContainerFactory #Bean to set the prefetchCount to something larger.
You will have to override the default boot-configured #Bean.

Spring SockJs RequestHandler doesn't upgrade connection to 101

Even though this is not described in the Spring documentation, a websocket connect should lead to a connection upgrade response (101 status).
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig<S extends ExpiringSession> extends AbstractSessionWebSocketMessageBrokerConfigurer<S>{
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/mobile-server");
config.setUserDestinationPrefix("/mobile-user");
}
#Override
public void configureStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/ws")
.setHandshakeHandler(new DefaultHandshakeHandler(new TomcatRequestUpgradeStrategy()))
.setAllowedOrigins("*")
.withSockJS()
.setSessionCookieNeeded(false)
;
}
}
However, I get a 200 status with a "Welcome to SockJS" message which is generated by TransportHandlingSockJsService in stead of the WebSocketHttpRequestHandler which would generate the upgrade AFAIK
#Configuration
public class WebSocketSecurity extends AbstractSecurityWebSocketMessageBrokerConfigurer{
#Override
protected boolean sameOriginDisabled() {
return true;
}
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.nullDestMatcher().permitAll()
.simpSubscribeDestMatchers("/user/queue/errors").permitAll()
.simpDestMatchers("/mobile-server/**").hasRole("ENDUSER")
.simpSubscribeDestMatchers("/user/**", "/topic/**").hasRole("ENDUSER")
.anyMessage().denyAll();
}
}
When I change the config to
#Override
public void configureStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/ws")
.setHandshakeHandler(new DefaultHandshakeHandler(new TomcatRequestUpgradeStrategy()))
.setAllowedOrigins("*");
}
to my surprise a call to /ws does lead to a connection upgrade 101. I'm surprised, since the documentation and all examples uniformly use the withSockJS() and the start of any websocket connection AFAIK is a request upgrade.
I can choose to force the upgrade by connecting to /ws/websocket (also not documented). So, I'm not sure what is best.
Any suggestions?
This is expected behavior. It's how the SockJS protocol works:
http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html. There is an initial "greeting" request and then the client starts trying transports one at a time.

Spring security OAuth2 - invalidate session after authentication

We are securing out REST services using spring security OAuth2. Applications can call into either the /oauth/authorize, /oauth/token or /rest-api endpoints. The token and rest-api endpoints are stateless and do not need a session.
Can we invalidate the session after the user is authenticated? If so, what is the best approach. We want the user to sign-in always whenever a call to /oauth/authorize is made. Currently, calls to /oauth/authorize are skipping authentication whenever a session exists.
Understanding that the question is a bit old, I hope that the following could be helpful for those who search for the correct answer for the question
OP asked not about tokens invalidation, but how to invalidate httpSession on Spring OAuth2 server right after user authentication successfully passed and a valid access_token or authorization_code (for subsequent getting of access_token) returned to a client.
There is no out-of-the-box solution for this use-case still. But working workaround from the most active contributor of spring-security-oauth, Dave Syer, could be found here on GitHub
Just copy of the code from there:
#Service
#Aspect
public class SessionInvalidationOauth2GrantAspect {
private static final String FORWARD_OAUTH_CONFIRM_ACCESS = "forward:/oauth/confirm_access";
private static final Logger logger = Logger.getLogger(SessionInvalidationOauth2GrantAspect.class);
#AfterReturning(value = "within(org.springframework.security.oauth2.provider.endpoint..*) && #annotation(org.springframework.web.bind.annotation.RequestMapping)", returning = "result")
public void authorizationAdvice(JoinPoint joinpoint, ModelAndView result) throws Throwable {
// If we're not going to the confirm_access page, it means approval has been skipped due to existing access
// token or something else and they'll be being sent back to app. Time to end session.
if (!FORWARD_OAUTH_CONFIRM_ACCESS.equals(result.getViewName())) {
invalidateSession();
}
}
#AfterReturning(value = "within(org.springframework.security.oauth2.provider.endpoint..*) && #annotation(org.springframework.web.bind.annotation.RequestMapping)", returning = "result")
public void authorizationAdvice(JoinPoint joinpoint, View result) throws Throwable {
// Anything returning a view and not a ModelView is going to be redirecting outside of the app (I think).
// This happens after the authorize approve / deny page with the POST to /oauth/authorize. This is the time
// to kill the session since they'll be being sent back to the requesting app.
invalidateSession();
}
#AfterThrowing(value = "within(org.springframework.security.oauth2.provider.endpoint..*) && #annotation(org.springframework.web.bind.annotation.RequestMapping)", throwing = "error")
public void authorizationErrorAdvice(JoinPoint joinpoint) throws Throwable {
invalidateSession();
}
private void invalidateSession() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
HttpSession session = request.getSession(false);
if (session != null) {
logger.warn(String.format("As part of OAuth application grant processing, invalidating session for request %s", request.getRequestURI()));
session.invalidate();
SecurityContextHolder.clearContext();
}
}
}
add pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
Another solution could be to set session time out to some very small value. The simplest way to achieve that is put the following to application.yml config:
server:
session:
timeout: 1
But it's not ideal solution as the minimum value could be provider is 1 (zero is reserved for infinite sessions) and it is in minutes not in seconds
From what I understand, you are trying to programmatically logout after you have undertaken certain set of actions. Probably you should look into the SecurityContextLogoutHandler and see how it works. There is a method for logout there. I think calling it as an advice will solve your problem.
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (invalidateHttpSession) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
}
SecurityContextHolder.clearContext();
}
First: in your configuration declare bean with token store for oauth
#Bean
#Primary
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
For controller approach we made the following class
#Controller
public class TokenController {
#RequestMapping(value = "/oauth/token/revoke", method = RequestMethod.POST)
public #ResponseBody void create(#RequestParam("token") String value) {
this.revokeToken(value);
}
#Autowired
TokenStore tokenStore;
public boolean revokeToken(String tokenValue) {
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
if (accessToken == null) {
return false;
}
if (accessToken.getRefreshToken() != null) {
tokenStore.removeRefreshToken(accessToken.getRefreshToken());
}
tokenStore.removeAccessToken(accessToken);
return true;
}
}
If you don't wan't to use this approach you can grab current user's token autowiring Principal:
OAuth2Authentication authorization = (OAuth2Authentication) principal;
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authorization.getDetails();
String token = details.getTokenValue();
Or even autowiring OAuth2Authentication:
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
String token = details.getTokenValue();
I can offer such an option (according to #de_xtr recomendation):
import static org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes;
#Slf4j
#Component
#Aspect
public class InvalidateSessionAspect {
private final LogoutHandler logoutHandler;
public InvalidateSessionAspect() {
logoutHandler = new SecurityContextLogoutHandler();
}
#Pointcut("execution(* org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(..))")
public void postAccessTokenPointcut() {
}
#AfterReturning(value = "postAccessTokenPointcut()", returning = "entity")
public void invalidateSession(JoinPoint jp, Object entity) {
log.debug("[d] Trying to invalidate the session...");
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) currentRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
logoutHandler.logout(request, null, null);
log.debug("[d] Session has been invalidated");
}
}
And the option without any aspects:
#Slf4j
class LogoutHandlerInterceptor implements HandlerInterceptor {
#Override
public void postHandle(HttpServletRequest req, HttpServletResponse resp, Object h, ModelAndView view) {
HttpSession session = req.getSession(false);
if (session != null) {
log.debug("[d] Trying to invalidate the session...");
session.invalidate();
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(null);
SecurityContextHolder.clearContext();
log.debug("[d] Session has been invalidated");
}
}
}
#Configuration
#EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
//...
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.addInterceptor(new LogoutHandlerInterceptor())
// ...
;
}
}

Resources