Cannot throw custom exception message for JWT CustomClaimVerifier - spring

I'm trying to verify the claim inside the JWT token using JwtClaimsSetVerifier given by Spring Boot 2.1. The problem is that Spring always throws an exception with the default exception message:
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
Even if I create a custom exception which extends the ClientAuthenticationException, I get the same exception message.
When the JWT claim verification fails, I want to modify the exception message. Here is my configuration class:
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceserverConfig extends ResourceServerConfigurerAdapter{
#Autowired
private DataSource dataSource;
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll().and()
.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler());
}
#Bean
public DataSource getDataSource() {
return dataSource;
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("qwerty123");
converter.setJwtClaimsSetVerifier(jwtClaimsSetVerifier());
return converter;
}
#Bean
public AuthenticationFailureHandler authenticationFailureHandler()
{
return new RestAuthenticationFailureHandler();
}
#Bean
public JwtClaimsSetVerifier jwtClaimsSetVerifier() {
return new DelegatingJwtClaimsSetVerifier(Arrays.asList(customJwtClaimVerifier()));
}
#Bean
public JwtClaimsSetVerifier customJwtClaimVerifier() {
return new CustomClaimVerifier();
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
TokenStore tokenStoreRes = new JdbcTokenStore(dataSource);
resources.resourceId("RESOURCE").tokenStore(tokenStoreRes);
}
#Bean
#Primary
public DefaultTokenServices tokenJWTServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
TokenStore tokenStoreRes = new JwtTokenStore(accessTokenConverter());
defaultTokenServices.setTokenStore(tokenStoreRes);
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
}
Here is my JWTClaimVerifier class:
public class CustomClaimVerifier implements JwtClaimsSetVerifier{
#Autowired
HttpServletRequest request;
#Override
public void verify(Map<String, Object> claims) throws InvalidTokenException {
try {
JsonParser parser = new JsonParser();
String json = new Gson().toJson(claims.get("userdetails"));
JsonElement menu = parser.parse(json);
String menuList = menu.getAsJsonObject().get("menu").getAsString();
boolean isMenuAccessible = validateAccessForMenu(request.getHeader("menuClicked"), menuList);
if(!isMenuAccessible) {
throw new InvalidTokenException("Invalid Permissions");
}
} catch (Exception e) {
throw new InvalidTokenException(e.getMessage());
}
}
}
I want an exception with my custom exception message when JWT claim verification fails, but all I get is the standard exception message thrown by Spring Security.

When the JWT claim verification fails, I want to modify the exception message.
But you do get your custom exception message.
The message you're seeing is because something else, something before the claims could be verified, failed.
If you check out the decode(String) method in JwtAccessTokenConverter, you'll see that your implementation of JwtClaimsSetVerifier is invoked after the token string has been decoded into a Jwt and the claims have been parsed. If the exception is thrown while decoding the token, your CustomClaimVerifier won't have a chance to override the exception.
Do you want to override the default exception message thrown when decoding fails?
Unfortunately, there doesn't seem to be a straightforward way of providing your own message. Perhaps you could subclass the JwtAccessTokenConverter and replace every InvalidTokenException with your own:
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
#Override
protected Map<String, Object> decode(String token) {
Map<String, Object> pieces = null;
try {
pieces = super.decode(token);
} catch(InvalidTokenException ex) {
throw new InvalidTokenException("MY CUSTOM MESSAGE");
}
return pieces;
}
}

Related

I can't update my webapp to Spring Boot 2.6.0 (2.5.7 works but 2.6.0 doesn't)

As mentioned in the title I can't update my webapp to Spring Boot 2.6.0. I wrote my webapp using Spring Boot 2.5.5 and everything works perfectly. If I update the pom.xml file with this new tag:
<version>2.5.7</version>
My webapp works perfectly. All tests work.
If I perform this update the webapp does not start:
<version>2.6.0</version>
Starting the DEBUG mode the IDE shows me an error and 2 links to 2 classes of my webapp.
2021-11-23 00:31:45.419 ERROR 21884 --- [ restartedMain] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'configurazioneSpringSecurity': Requested bean is currently in creation: Is there an unresolvable circular reference?
It seems the problem is in this class:
#Configuration
#EnableWebSecurity
public class ConfigurazioneSpringSecurity extends WebSecurityConfigurerAdapter {
#Autowired
LivelliDeiRuoli livelliDeiRuoli;
#Autowired
GestioneUtentiSpringSecurity gestioneUtentiSpringSecurity;
#Bean
public BCryptPasswordEncoder metodoCrittografia() {
return new BCryptPasswordEncoder();
}
#Autowired
public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(gestioneUtentiSpringSecurity).passwordEncoder(metodoCrittografia());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().antMatchers(
"/",
"/login",
"/benvenuto",
"/registrazione",
"/registrazione-eseguita",
"/pagine-applicazione"
).permitAll();
http.authorizeRequests().antMatchers("/area-riservata")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(1L) + "')");
http.authorizeRequests().antMatchers("/cambio-password")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(1L) + "')");
http.authorizeRequests().antMatchers("/cambio-nome")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(1L) + "')");
http.authorizeRequests().antMatchers("/cancella-utente")
.access("isAuthenticated()");
http.authorizeRequests().antMatchers("/gestione-utenti")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(2L) + "')");
http.authorizeRequests().antMatchers("/gestione-ruoli")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(3L) + "')");
http.authorizeRequests().antMatchers("/pannello-di-controllo")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(3L) + "')");
http.authorizeRequests().and().exceptionHandling().accessDeniedPage("/errore-403");
http.authorizeRequests().and().formLogin()
.loginProcessingUrl("/pagina-login")
.loginPage("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?errore=true")
.usernameParameter("username")
.passwordParameter("password")
.and().logout().logoutUrl("/pagina-logout")
.logoutSuccessUrl("/login?logout=true");
http.authorizeRequests().and() //
.rememberMe().tokenRepository(this.persistentTokenRepository()) //
.tokenValiditySeconds(365 * 24 * 60 * 60);
http.authorizeRequests().antMatchers("/gestione-eventi")
.access("hasAnyRole('" + livelliDeiRuoli.elencoRuoli(2L) + "')");
http.authorizeRequests().antMatchers(
"/cerca-eventi",
"/ultimi-eventi"
).permitAll();
}
#Autowired
private DataSource dataSource;
#Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setDataSource(dataSource);
return db;
}
#Bean(name = BeanIds.AUTHENTICATION_MANAGER)
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
or in this:
#SpringBootApplication
#Profile("sviluppo")
public class GestioneUtentiApplication extends SpringBootServletInitializer {
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(GestioneUtentiApplication.class);
}
public static void main(String[] args) {
System.setProperty("server.servlet.context-path", "/gestioneutenti");
SpringApplication.run(GestioneUtentiApplication.class, args);
}
}
What's wrong with these classes?
What changes with Spring Boot 2.6.0?
GestioneUtentiSpringSecurity implements UserDetailsService:
#Service
public class GestioneUtentiSpringSecurity implements UserDetailsService {
#Autowired
private UtenteRepository utenteRepository;
#Autowired
private RuoloRepository ruoloRepository;
#Autowired
EseguiVariabiliDiSistema eseguiVariabiliDiSistema;
#Autowired
LivelliDeiRuoli livelliDeiRuoli;
#Override
public UserDetails loadUserByUsername(String nomeUtente) throws UsernameNotFoundException {
Utente utente = trovaUtenteConPrivilegiDiAutenticazione(nomeUtente);
if (utente == null) {
throw new UsernameNotFoundException("L'utente " + nomeUtente + " non รจ stato trovato nel database.");
}
List<String> ruoliUtente = null;
try {
ruoliUtente = this.ruoloRepository.trovaRuoliUtente(utente.getId());
}catch (Exception b){
ruoliUtente = null;
}
List<GrantedAuthority> grantList = null;
try{
grantList = new ArrayList<GrantedAuthority>();
if (ruoliUtente != null) {
for (String ruolo : ruoliUtente) {
GrantedAuthority authority = new SimpleGrantedAuthority(ruolo);
grantList.add(authority);
}
}
}catch (Exception c){
grantList = null;
}
UserDetails userDetails = null;
if((utente != null) && (ruoliUtente != null) && (grantList != null)){
userDetails = (UserDetails) new User(utente.getNome(), utente.getPassword(), grantList);
}
return userDetails;
}
public Utente trovaUtenteConPrivilegiDiAutenticazione(String nomeUtente){
try{
Utente utente = utenteRepository.trovaUtente(nomeUtente);
if(livelliDeiRuoli.requisitiUtenteConRuoloMassimo(utente)){
return utente;
} else{
eseguiVariabiliDiSistema.trovaVariabileSenzaVerificaUtente(
new VariabileSistema(0L, "login", "")
);
if(eseguiVariabiliDiSistema.getVariabileDiSistema().getValore().equals("true")){
return utente;
}else if(eseguiVariabiliDiSistema.getVariabileDiSistema().getValore().equals("false")){
return null;
}else{
return null;
}
}
}catch (Exception e){
return null;
}
}
}
Starting on Spring Boot 2.6, circular dependencies are prohibited by default. you can allow circular references again by setting the following property:
spring.main.allow-circular-references = true
You can read some more details about this in the Spring Boot 2.6 Release Notes.
The problem that Spring faces here and causes to not able to move forward starting from spring boot 2.6 with the default configuration of spring.main.allow-circular-references = false is located in the following part
#Bean
public BCryptPasswordEncoder metodoCrittografia() {
return new BCryptPasswordEncoder();
}
#Autowired
public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(gestioneUtentiSpringSecurity).passwordEncoder(metodoCrittografia());
}
I believe this is happening because the WebSecurityConfig extends WebSecurityConfigurerAdapter has some circular references in combination with BCryptPasswordEncoder inside this class.
The solution is to create another configuration class, where you can split the configurations so that spring is able to correctly create the beans avoiding circular references.
So you can create the following extra class
#Configuration
public class CustomSecurityConfig {
#Bean
public BCryptPasswordEncoder metodoCrittografia() {
return new BCryptPasswordEncoder();
}
}
Then in your original ConfigurazioneSpringSecurity.class you can replace the failing
#Bean
public BCryptPasswordEncoder metodoCrittografia() {
return new BCryptPasswordEncoder();
}
#Autowired
public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(gestioneUtentiSpringSecurity).passwordEncoder(metodoCrittografia());
}
with the
#Autowired
private PasswordEncoder passwordEncoder;
#Autowired
public void crittografiaPassword(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(gestioneUtentiSpringSecurity)
.passwordEncoder(passwordEncoder);
}
Although setting the application.properties works, it is likely using a feature that is going to be deprecated. I was able to work around this by using setter based injection. It's a bit more verbose but might be a good starting point for those looking to stay current and not use features that might be deprecated down the line.
It's certainly an answer that can be improved upon and I hope others can contribute perhaps more concise answers. I'll update this if I find anything cleaner.
Before
#Component
public class CustomFilter extends OncePerRequestFilter {
#Autowired
private MyUserDetailsService myUserDetailsService;
#Autowired
private JWTUtils jwtUtils;
//When any api will be called this method will be called first and this will extract
// Token from header pass to JWT Util calls for token details extraction
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, FilterChain filterChain)
throws ServletException, IOException {
//implementation
}
}
After
#Component
public class CustomFilter extends OncePerRequestFilter {
private MyUserDetailsService myUserDetailsService;
public void setMyUserDetailsService(MyUserDetailsService myUserDetailsService) {
this.myUserDetailsService = myUserDetailsService;
}
public void setJwtUtils(JWTUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
private JWTUtils jwtUtils;
//When any api will be called this method will be called first and this will extract
// Token from header pass to JWT Util calls for token details extraction
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, FilterChain filterChain)
throws ServletException, IOException {
//implementation
}
}
reference:
https://theintuitiveprogrammer.com/post-eight.html
I've this problem during migrate to spring boot 2.6.x with WebSecurityConfig code:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public UserDetailsService userDetailsService() {
return email -> {
....
};
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
...;
}
fix according WebSecurityConfigurerAdapter#userDetailsServiceBean javadoc:
Override this method to expose a UserDetailsService created from configure(AuthenticationManagerBuilder) as a bean ...
To change the instance returned, developers should change userDetailsService() instead
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
#Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
#Override
public UserDetailsService userDetailsService() {
return email -> {
....
};
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
...;
}

Use sAMAccountName instead of userPrincipalName in LDAP auth in Spring Boot

So I am using the code below to connect to our LDAP server in my spring boot app, I can authorized successfully using the userPrincipalName -> (LastName.FirstName#enterprise.com), but I want to use the assigned sAMAccountName instead (ID00001).
I messed around with the setSearchFilter by doing: provider.setSearchFilter("(sAMAccountName ={0})")
but it's not working. (I'm getting bad credentials)
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/myapplication")
.authorizeRequests()
.anyRequest().fullyAuthenticated()
.and()
.httpBasic();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}
#Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Arrays.asList(activeDirectoryLdapAuthenticationProvider()));
}
#Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider("", "ldap://test.enterprise.com","dc=ORG1, dc=ORG2");
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
provider.setSearchFilter("(sAMAccountName={0})"); // THIS DOES NOT WORK
provider.setSearchFilter("(userPrincipalName={0})"); // THIS WORKS
return provider;
}
}
EDIT: OK turns out I have been using the wrong field, there is another field: sAMAccountName that has the same value that I should be using, updated the title and question contents.
EDIT:
Ok I tried:
provider.setSearchFilter("(&(objectClass=user)(cn={0}))");
provider.setSearchFilter("(&(objectClass=user)(sAMAccountName={0}))");
provider.setSearchFilter("(&(objectClass=user)(cn={1}))");
provider.setSearchFilter("(&(objectClass=user)(sAMAccountName={1}))");
and it's still the same error,
bad credentials, AcceptSecurityContextError, 52e v2580
I managed to solve my issue but instead of using spring ldap dependencies, I created a custom authentication provider that implements AuthenticationProvider and and used the following code to connect to ldap and validate the credentials:
autowire it to WebSecurityConfig class.
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
private UserDetails userDetails;
#Override
public Authentication authenticate(Authentication auth) throws
AuthenticationException {
String user = authentication.getName();
String pass = authentication.getCredentials.ToString();
try {
if (isRegistrered(user,pass) {
return new UsernamePasswordAuthenticationToken(user,pass, new
ArrayList<>());
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
private isRegisterd(String user, String pass) {
boolean result = false;
try {
// Set up the environment for creating the initial context
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://ldap_server:389");
//
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "DOMAIN\\USER");
env.put(Context.SECURITY_CREDENTIALS, "PASSWORD");
// Create the initial context
DirContext ctx = new InitialDirContext(env);
if(ctx != null) {
ctx.close();
}
return result;
} catch (Exception e) {
return result;
}
}
private void retrieveUserDetails(DirContext ctx, String username) throws NamingException {
String userSearchBase = "dc=TEST,dc=SAMPLE";
String userSearchFilter = String.format("sAMAccountName=%s", username);
//any attributes that you want
String[] attributes = { "sAMAccountName", "department" };
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
controls.setReturningAttributes(attributes);
SearchResult result = null;
NamingEnumeration<?> users = ctx.search(userSearchBase, userSearchFilter, controls);
while(users.hasmore()) {
result = (SearchResult) users.next();
Attributes attr = result.getAttribtutes();
String sAMAccountName = attr.get("sAMAccountName").get(0).toString();
String department = attr.get("department").get(0).toString();
//assign to autowired object to be accessed anywhere
this.userDetails.setAccountName(sAMAccountName);
this.userDetails.setDepartment(department);
}
}
}
There's another approach by using LdapAuthenticationProvider. I use LdapAuthenticationProvider with a single parameter constructor.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
#Profile("integration")
public class SecurityConfigurationT extends WebSecurityConfigurerAdapter {
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final TokenProvider tokenProvider;
public SecurityConfigurationT(
AuthenticationManagerBuilder authenticationManagerBuilder,
TokenProvider tokenProvider) {
this.authenticationManagerBuilder = authenticationManagerBuilder;
this.tokenProvider = tokenProvider;
}
#PostConstruct
public void initIntegration() {
try {
authenticationManagerBuilder
.authenticationProvider(ldapAuthenticationProvider());
} catch (Exception e) {
throw new BeanInitializationException("Security configuration failed", e);
}
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public LdapAuthenticationProvider ldapAuthenticationProvider() throws Exception {
return new LdapAuthenticationProvider(ldapAuthenticator());
}
#Bean
public LdapContextSource ldapContextSource() throws Exception {
PasswordPolicyAwareContextSource contextSource = new PasswordPolicyAwareContextSource("ldaps://int.root.company.ag:636");
contextSource.setUserDn("CN=system_user,OU=companygroup svc accs,DC=int,DC=root,DC=company,DC=ag");
contextSource.setPassword("XXXXXX");
return contextSource;
}
// Use this for other filter such as "sAMAccountName".
#Bean
public LdapAuthenticator ldapAuthenticator() {
BindAuthenticator authenticator = new BindAuthenticator(ldapContextSource());
authenticator.setUserSearch(new FilterBasedLdapUserSearch("OU=company,OU=companygroup users,DC=int,DC=root,DC=company,DC=ag", "(sAMAccountName={0})", ldapContextSource()));
return authenticator;
}
}
Reference:
https://www.stevenschwenke.de/LDAPWithSpringSecurity

Handling SOAP Fault in Spring Boot WebServiceTemplate

I am new to SOAP, trying to run a sample SOAP client using Spring Boot
How the SOAP fault, Exceptions or Errors are handled while using WebServiceTemplate
public class CountryClient extends WebServiceGatewaySupport {
private static final Logger log = LoggerFactory.getLogger(CountryClient.class);
public GetCountryResponse getCountry(String country) {
GetCountryRequest request = new GetCountryRequest();
request.setName(country);
log.info("Requesting location for " + country);
GetCountryResponse response = (GetCountryResponse) getWebServiceTemplate()
.marshalSendAndReceive("http://localhost:8080/ws/countries", request,
new SoapActionCallback(
"http://spring.io/guides/gs-producing-web-service/GetCountryRequest"));
return response;
}
}
One way is writing your custom interceptor which implements Spring WS's ClientInterceptor interface. You should override handleFault method to handle SOAP faults with your custom logic.
public class MySoapClientInterceptor implements ClientInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(MySoapClientInterceptor.class);
#Override
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {
return true;
}
#Override
public boolean handleResponse(MessageContext messageContext) throws WebServiceClientException {
return true;
}
#Override
public boolean handleFault(MessageContext messageContext) throws WebServiceClientException {
LOGGER.info("intercepted a fault...");
SoapBody soapBody = getSoapBody(messageContext);
SoapFault soapFault = soapBody.getFault();
LOGGER.error(soapFault.getFaultStringOrReason());
throw new RuntimeException(String.format("Error occured while invoking SOAP service - %s ", soapFault.getFaultStringOrReason()));
}
#Override
public void afterCompletion(MessageContext messageContext, Exception ex) throws WebServiceClientException {
}
private SoapBody getSoapBody(MessageContext messageContext) {
SoapMessage soapMessage = (SoapMessage) messageContext.getResponse();
SoapEnvelope soapEnvelope = soapMessage.getEnvelope();
return soapEnvelope.getBody();
}
}
Then you need to register your custom Interceptor class as an interceptor at your SOAP client config class. At the bean definition of CountryClient at your Configuration class in your case.
#Configuration
public class SoapClientConfig {
#Value("${soap.server.url}")
public String soap_server_url;
#Bean
public Jaxb2Marshaller marshaller() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setContextPath("com.example");
return marshaller;
}
#Bean
public SoapConnector soapConnector(Jaxb2Marshaller marshaller) {
SoapConnector client = new SoapConnector();
client.setDefaultUri(soap_server_url);
client.setMarshaller(marshaller);
client.setUnmarshaller(marshaller);
ClientInterceptor[] clientInterceptors = {new MySoapClientInterceptor()};
client.setInterceptors(clientInterceptors);
return client;
}
}

Customization of TokenEndpoint in Sprin OAuth2

I would like to provide a custom implmentation of the TokenEndpoint class in Spring framework.
Ive copied over the TokenEndpoint class of spring and have made my changes to the required places. But when the applications starts, I'm always getting the error
Caused by: java.lang.IllegalStateException: TokenGranter must be provided
I have provided an implementation for TokenGranter in my OAuthConfig, but spring is not picking up that
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.pathMapping("/oauth/token", "/oauth/token/v1")
.tokenServices(tokenServices())
.tokenGranter(tokenGranter())
.authenticationManager(authenticationManager).tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancer()).accessTokenConverter(accessTokenConverter());
}
#Bean
#Primary
public TokenGranter tokenGranter() {
TokenGranter tokenGranter = null;
if (tokenGranter == null) {
tokenGranter = new TokenGranter() {
private CompositeTokenGranter delegate;
#Override
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (delegate == null) {
delegate = new CompositeTokenGranter(getDefaultTokenGranters());
}
return delegate.grant(grantType, tokenRequest);
}
};
}
return tokenGranter;
}
I even tried to provide this implementation, in my custom TokenEndpoint class.
For now, the implementation of custom TokenEndpoint is exactly the same as Spring's TokenEndpoint.
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
private List<TokenGranter> getDefaultTokenGranters() {
ClientDetailsService clientDetails = clientDetailsService();
AuthorizationServerTokenServices tokenServices = tokenServices();
AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
OAuth2RequestFactory requestFactory = requestFactory();
List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,
requestFactory));
tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
tokenGranters.add(implicit);
tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
if (authenticationManager != null) {
tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetails,
requestFactory));
}
return tokenGranters;
}
private DefaultTokenServices createDefaultTokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(true);
tokenServices.setClientDetailsService(clientDetailsService());
tokenServices.setTokenEnhancer(tokenEnhancer());
addUserDetailsService(tokenServices, new CustomDetailsService());
return tokenServices;
}
private ClientDetailsService clientDetailsService() {
ClientDetailsService clientDetailsService = null;
clientDetailsService = new InMemoryClientDetailsService();
addUserDetailsService(createDefaultTokenServices(), new CustomDetailsService());
return clientDetailsService;
}
private void addUserDetailsService(DefaultTokenServices tokenServices, UserDetailsService userDetailsService) {
if (userDetailsService != null) {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>(
userDetailsService));
tokenServices
.setAuthenticationManager(new ProviderManager(Arrays.<AuthenticationProvider> asList(provider)));
}
}
private AuthorizationCodeServices authorizationCodeServices() {
AuthorizationCodeServices authorizationCodeServices = new InMemoryAuthorizationCodeServices();
return authorizationCodeServices;
}
private OAuth2RequestFactory requestFactory() {
OAuth2RequestFactory requestFactory = new DefaultOAuth2RequestFactory(clientDetailsService());
return requestFactory;
}
#Bean
public JwtTokenStore tokenStore() {
JwtTokenStore jwtTokenStore = new JwtTokenStore(accessTokenConverter());
return jwtTokenStore;
}
#Bean
#Primary
public AuthorizationServerTokenServices tokenServices() {
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setAccessTokenValiditySeconds(-1);
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter() {
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
return accessToken;
}
};
return converter;
}
Ive been trying to figure this out for a couple of days, but without any luck. So any help would be much appreciated.
I know the question is quite old, but I encountered the same problem and didn't manage to find a complete guide on customizing TokenEndpoint. I wasn't be able to use TokenEnhancer, because I needed to change headers of the response. So, this is the version worked for me.
You define your overwritten controller as usual:
#RequestMapping(value = "/oauth/token")
public class CustomTokenEndpoint extends TokenEndpoint {
#PostMapping
public ResponseEntity<OAuth2AccessToken> postAccessToken(
Principal principal,
#RequestParam Map<String, String> parameters
) throws HttpRequestMethodNotSupportedException {
ResponseEntity<OAuth2AccessToken> defaultResponse = super.postAccessToken(principal, parameters);
// do some work
return defaultResponse;
}
}
And you need to create your own TokenEndpoint bean:
#Bean
#Primary
public TokenEndpoint tokenEndpoint(AuthorizationServerEndpointsConfiguration conf) {
TokenEndpoint tokenEndpoint = new CustomTokenEndpoint();
tokenEndpoint.setClientDetailsService(conf.getEndpointsConfigurer().getClientDetailsService());
tokenEndpoint.setProviderExceptionHandler(conf.getEndpointsConfigurer().getExceptionTranslator());
tokenEndpoint.setTokenGranter(conf.getEndpointsConfigurer().getTokenGranter());
tokenEndpoint.setOAuth2RequestFactory(conf.getEndpointsConfigurer().getOAuth2RequestFactory());
tokenEndpoint.setOAuth2RequestValidator(conf.getEndpointsConfigurer().getOAuth2RequestValidator());
tokenEndpoint.setAllowedRequestMethods(conf.getEndpointsConfigurer().getAllowedTokenEndpointRequestMethods());
return tokenEndpoint;
}
And here's the kicker. You need to allow overwriting spring beans in your application.properties:
spring.main.allow-bean-definition-overriding: true
Hope this helps someone
Why do you need to implement TokenEndpoint again?
You can create a TokenGranter bean and inject it to default endpoints.
Where is getDefaultTokenGranters() method?
It looks like you have an incomplete copy of AuthorizationServerEndpointsConfigurer source code.
Update:
If you want to customize the token response ,use TokenEnhancer.
for example:
public class CustomTokenEnhancer implements TokenEnhancer {
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
OurUser user = (OurUser) authentication.getPrincipal();
final Map<String, Object> additionalInfo = new HashMap<>();
Map<String, Object> userDetails = new HashMap<>();
userDetails.put(USERID, user.getId().getId());
userDetails.put(NAME, user.getName());
userDetails.put(MOBILE, user.getMobile());
userDetails.put(EMAIL, user.getEmail());
additionalInfo.put(USERINFO, userDetails);
// Set additional information in token for retriving in #org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
in OAuth2 Config:
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure(endpoints);
endpoints.
.....
// Include additional information to OAuth2 Access token with custom token enhancer
.tokenEnhancer(tokenEnhancer());
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
https://stackoverflow.com/a/28512607/4377110

Can i append some information in oauth/check_token endpoint and retrieve it at authorization server?

Preface
I am working on an OAuth application for security between two servers. I have an OAuth Server and a Resource Server. The Resource Server has a single .war deployed that contains 4 APIs.
Single Responsibility
The OAuth server has to validate a the access token that was passed by an API (1 of the 4) from that same .war.
The OAuth server has to keep a hit count for a particular accessToken for a particular API. If the hit count exceeds the configured hits the OAuth server would throw a 403: Forbidden.
Every API in the .war must first validate the accessToken from the OAuth server and if it's validated, then proceed to provide the response.
What I've done:
If a .war has a single API then I can simply make the two servers communicate using a webHook, below is the code that does it.
On the Resource Server Side:
My urls for different APIs are:
localhost:8080/API/API1
localhost:8080/API/API2
Below code routes any request if they have /API/anything towards the spring security filters
<http pattern="/API/**" create-session="never" authentication-manager-ref="authenticationManager" entry-point-ref="oauthAuthenticationEntryPoint" xmlns="http://www.springframework.org/schema/security">
<anonymous enabled="false" />
<intercept-url pattern="/places/**" method="GET" access="IS_AUTHENTICATED_FULLY" />
<custom-filter ref="resourceServerFilter" before="PRE_AUTH_FILTER" />
<access-denied-handler ref="oauthAccessDeniedHandler" />
</http>
I have used remote token services and defined the webHook to route the request to the OAuth server
<bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.RemoteTokenServices">
<property name="checkTokenEndpointUrl" value="http://localhost:8181/OUTPOST/oauth/check_token"/>
<property name="clientId" value="atlas"/>
<property name="clientSecret" value="atlas"/>
</bean>
Configuration for Auth server
#EnableAuthorizationServer
#Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private static String REALM="OUTPOST_API";
#Autowired
private ClientDetailsService clientService;
#Autowired
public AuthorizationServerConfig(AuthenticationManager authenticationManager,RedisConnectionFactory redisConnectionFactory) {
this.authenticationManager = authenticationManager;
this.redisTokenStore = new RedisTokenStore(redisConnectionFactory);
}
// #Autowired
// #Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
private TokenStore redisTokenStore;
#Autowired
private UserApprovalHandler userApprovalHandler;
#Autowired
private RedisConnectionFactory redisConnectionFactory;
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("isAuthenticated()").
realm(REALM+"/client");
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("cl1")
.secret("pwd")
.authorizedGrantTypes("password", "client_credentials", "refresh_token")
.authorities("ROLE_CLIENT", "ROLE_ADMIN")
.scopes("read", "write", "trust")/*
.resourceIds("sample-oauth")*/
.accessTokenValiditySeconds(1000)
.refreshTokenValiditySeconds(5000)
.and()
.withClient("atlas")
.secret("atlas");
}
#Bean
#Autowired
public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {
this.redisTokenStore = new RedisTokenStore(redisConnectionFactory);
return this.redisTokenStore;
}
#Bean
public WebResponseExceptionTranslator loggingExceptionTranslator() {
return new DefaultWebResponseExceptionTranslator() {
#Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
// This is the line that prints the stack trace to the log. You can customise this to format the trace etc if you like
e.printStackTrace();
// Carry on handling the exception
ResponseEntity<OAuth2Exception> responseEntity = super.translate(e);
HttpHeaders headers = new HttpHeaders();
headers.setAll(responseEntity.getHeaders().toSingleValueMap());
OAuth2Exception excBody = responseEntity.getBody();
return new ResponseEntity<>(excBody, headers, responseEntity.getStatusCode());
}
};
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(redisTokenStore).userApprovalHandler(userApprovalHandler)
.authenticationManager(authenticationManager)
.exceptionTranslator(loggingExceptionTranslator());
}
public void setRedisConnectionFactory(RedisConnectionFactory redisConnectionFactory) {
this.redisConnectionFactory = redisConnectionFactory;
}
#Bean
public TokenStoreUserApprovalHandler userApprovalHandler(){
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(redisTokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientService));
handler.setClientDetailsService(clientService);
return handler;
}
#Bean
#Autowired
public ApprovalStore approvalStore() throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(redisTokenStore);
return store;
}
#Bean
#Primary
#Autowired
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(redisTokenStore);
return tokenServices;
}
}
#Component
class MyOAuth2AuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint{}
What I need help with:
The issue is with the support for single .war and multiple API. The issue is the spring config is created at a package level because of which all the APIs in the .war have the same clientID and clientSecret.
How would my OAuth server know, which specific API is being accessed and of which API the hitCount needs to be deducted.
Possible Solution?
I was thinks of customizing RemoteTokenService and adding a request parameter at the webHoot URL and then using a filter at OAuth server to get the passed tag (if I may call it that)
Is this even possible? Is there any better approch than this, that doesn't involve all these work arounds?
Eureka !! I finally found a way out to resolve this problem.
All you have to do is :
Configuration at Resource server
Instead of using RemoteTokenService make a custom remote token service which appends some data (query parameter) in the generated request.
public class CustomRemoteTokenService implements ResourceServerTokenServices {
protected final Log logger = LogFactory.getLog(getClass());
private RestOperations restTemplate;
private String checkTokenEndpointUrl;
private String clientId;
private String clientSecret;
private String tokenName = "token";
private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
#Autowired
public CustomRemoteTokenService() {
restTemplate = new RestTemplate();
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
#Override
// Ignore 400
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400) {
super.handleError(response);
}
}
});
}
public void setRestTemplate(RestOperations restTemplate) {
this.restTemplate = restTemplate;
}
public void setCheckTokenEndpointUrl(String checkTokenEndpointUrl) {
this.checkTokenEndpointUrl = checkTokenEndpointUrl;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
this.tokenConverter = accessTokenConverter;
}
public void setTokenName(String tokenName) {
this.tokenName = tokenName;
}
#Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
/*
* This code needs to be more dynamic. Every time an API is added we have to add its entry in the if check for now.
* Should be changed later.
*/
HttpServletRequest request = Context.getCurrentInstance().getRequest();
MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
String uri = request.getRequestURI();
formData.add(tokenName, accessToken);
if(request != null) {
if(uri.contains("API1")) {
formData.add("api", "1");
}else if(uri.contains("API2")) {
formData.add("api", "2");
}
}
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);
if (map.containsKey("error")) {
logger.debug("check_token returned error: " + map.get("error"));
throw new InvalidTokenException(accessToken);
}
Assert.state(map.containsKey("client_id"), "Client id must be present in response from auth server");
return tokenConverter.extractAuthentication(map);
}
#Override
public OAuth2AccessToken readAccessToken(String accessToken) {
throw new UnsupportedOperationException("Not supported: read access token");
}
private String getAuthorizationHeader(String clientId, String clientSecret) {
String creds = String.format("%s:%s", clientId, clientSecret);
try {
return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
}
catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Could not convert String");
}
}
private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
if (headers.getContentType() == null) {
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
}
#SuppressWarnings("rawtypes")
Map map = restTemplate.exchange(path, HttpMethod.POST,
new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
#SuppressWarnings("unchecked")
Map<String, Object> result = map;
return result;
}
}
By implementing ResourceServerTokenServices you can modify the request that is sent by the resource server to the auth server for authentication and authorization.
configuration at Auth Server
Override the spring security controller. What i mean by overring is make a custom controller so that the request for oauth/check_token is handled by your custom controller and not the spring defined controller.
#RestController
public class CustomCheckTokenEndpoint {
private ResourceServerTokenServices resourceServerTokenServices;
private AccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
protected final Log logger = LogFactory.getLog(getClass());
private WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();
#Autowired
KeyHitManager keyHitManager;
public CustomCheckTokenEndpoint(ResourceServerTokenServices resourceServerTokenServices) {
this.resourceServerTokenServices = resourceServerTokenServices;
}
/**
* #param exceptionTranslator
* the exception translator to set
*/
public void setExceptionTranslator(WebResponseExceptionTranslator exceptionTranslator) {
this.exceptionTranslator = exceptionTranslator;
}
/**
* #param accessTokenConverter
* the accessTokenConverter to set
*/
public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
this.accessTokenConverter = accessTokenConverter;
}
#RequestMapping(value = "/oauth/check_token")
#ResponseBody
public Map<String, ?> customCheckToken(#RequestParam("token") String value, #RequestParam("api") int api) {
OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
if (token == null) {
throw new InvalidTokenException("Token was not recognised");
}
if (token.isExpired()) {
throw new InvalidTokenException("Token has expired");
}
OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
Map<String, ?> response = accessTokenConverter.convertAccessToken(token, authentication);
String clientId = (String) response.get("client_id");
if (!keyHitManager.isHitAvailble(api,clientId)) {
throw new InvalidTokenException(
"Services for this key has been suspended due to daily/hourly transactions limit");
}
return response;
}
#ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
// This isn't an oauth resource, so we don't want to send an
// unauthorized code here. The client has already authenticated
// successfully with basic auth and should just
// get back the invalid token error.
#SuppressWarnings("serial")
InvalidTokenException e400 = new InvalidTokenException(e.getMessage()) {
#Override
public int getHttpErrorCode() {
return 400;
}
};
return exceptionTranslator.translate(e400);
}
}

Resources