Customization of TokenEndpoint in Sprin OAuth2 - spring

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

Related

Spring Security CAS - After Receiving the ticket unable to land to login screen

After receiving ticket unable to login to home screen, how I can debug the spring security part in my application?
How can I debug the entry point of the application once ticket received?
#Configuration
#ComponentScan
public class CasSecurityConfiguration
{
private final String casServerLoginUrl;
private final String casServerUrl;
private final String casClientUrl;
#Autowired
public CasSecurityConfiguration(#Value("#{environment.casServerLoginUrl}") String casServerLoginUrl,
#Value("#{environment.casServerUrl}") String casServerUrl,
#Value("#{environment.casClientUrl}") String casClientUrl)
{
this.casServerLoginUrl = casServerLoginUrl;
this.casServerUrl = casServerUrl;
this.casClientUrl = casClientUrl;
}
#Bean
#SuppressWarnings("unchecked")
public CasAuthenticationProvider casAuthenticationProvider(TicketValidator ticketValidator,
ServiceProperties serviceProperties, CodesLdapUserDetailsService userDetailsService)
{
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties);
provider.setTicketValidator(ticketValidator);
provider.setAuthenticationUserDetailsService(userDetailsService);
provider.setKey("cae");
return provider;
}
#Bean
public TicketValidator ticketValidator()
{
System.out.println("In ticketValidator ");
return new Cas20ServiceTicketValidator(casServerUrl);
}
#Bean
public AuthenticationManager authenticationManager(CasAuthenticationProvider casAuthenticationProvider)
{
System.out.println("In authenticationManager ");
return new ProviderManager(List.of(casAuthenticationProvider));
}
#Bean
public CasAuthenticationFilter casAuthenticationFilter(AuthenticationManager authenticationManager,
ServiceProperties serviceProperties)
{
System.out.println("In casAuthenticationFilter ");
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager);
filter.setServiceProperties(serviceProperties);
return filter;
}
#Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint(ServiceProperties serviceProperties)
{
System.out.println("In casAuthenticationEntryPoint ");
CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
casAuthenticationEntryPoint.setServiceProperties(serviceProperties);
casAuthenticationEntryPoint.setLoginUrl(casServerLoginUrl);
return casAuthenticationEntryPoint;
}
#Bean
public ServiceProperties serviceProperties()
{
System.out.println("In serviceProperties ");
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(casClientUrl);
serviceProperties.setSendRenew(false);
return serviceProperties;
}
After receiving ticket unable to login to home screen, How I can debug the spring security part in my application?
How can I debug the entry point of the application once ticket received?

JwtAuthenticationToken is not in the allowlist, Jackson issue

I have created my authorization server using org.springframework.security:spring-security-oauth2-authorization-server:0.2.2 and my client using org.springframework.boot:spring-boot-starter-oauth2-client. The users are able to sign in and out successfully, however, while testing I noticed that if I log in successfully then restart the client (but not the server) without signing out and try to login in again the server throws the following error in an endless loop of redirects
java.lang.IllegalArgumentException: The class with org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken and name of org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken is not in the allowlist. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See https://github.com/spring-projects/spring-security/issues/4370 for details
I tried to follow this link https://github.com/spring-projects/spring-security/issues/4370 but the solution on it did not work for me. I also tried a different solution described in this link https://github.com/spring-projects/spring-authorization-server/issues/397#issuecomment-900148920 and modified my authorization server code as follows:-
Here is my Jackson Configs
#Configuration
public class JacksonConfiguration {
/**
* Support for Java date and time API.
*
* #return the corresponding Jackson module.
*/
#Bean
public JavaTimeModule javaTimeModule() {
return new JavaTimeModule();
}
#Bean
public Jdk8Module jdk8TimeModule() {
return new Jdk8Module();
}
/*
* Support for Hibernate types in Jackson.
*/
#Bean
public Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
/*
* Module for serialization/deserialization of RFC7807 Problem.
*/
#Bean
public ProblemModule problemModule() {
return new ProblemModule();
}
/*
* Module for serialization/deserialization of ConstraintViolationProblem.
*/
#Bean
public ConstraintViolationProblemModule constraintViolationProblemModule() {
return new ConstraintViolationProblemModule();
}
/**
* To (de)serialize a BadCredentialsException, use CoreJackson2Module:
*/
#Bean
public CoreJackson2Module coreJackson2Module() {
return new CoreJackson2Module();
}
#Bean
#Primary
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(coreJackson2Module());
mapper.registerModule(javaTimeModule());
mapper.registerModule(jdk8TimeModule());
mapper.registerModule(hibernate5Module());
mapper.registerModule(problemModule());
mapper.registerModule(constraintViolationProblemModule());
return mapper;
}
}
and here is my Authorization server config
#Configuration(proxyBeanMethods = false)
public class AuthServerConfig {
private final DataSource dataSource;
private final AuthProperties authProps;
private final PasswordEncoder encoder;
public AuthServerConfig(DataSource dataSource, AuthProperties authProps, PasswordEncoder encoder) {
this.dataSource = dataSource;
this.authProps = authProps;
this.encoder = encoder;
}
#Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource);
}
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
authorizationServerConfigurer.tokenRevocationEndpoint(tokenRevocationEndpoint -> tokenRevocationEndpoint
.revocationResponseHandler((request, response, authentication) -> {
Assert.notNull(request, "HttpServletRequest required");
HttpSession session = request.getSession(false);
if (!Objects.isNull(session)) {
session.removeAttribute("SPRING_SECURITY_CONTEXT");
session.invalidate();
}
SecurityContextHolder.getContext().setAuthentication(null);
SecurityContextHolder.clearContext();
response.setStatus(HttpStatus.OK.value());
})
);
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
return http.formLogin(Customizer.withDefaults()).build();
}
#Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate, TokenSettings tokenSettings) {
JdbcRegisteredClientRepository clientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
RegisteredClient webClient = RegisteredClient.withId("98a9104c-a9c7-4d7c-ad03-ec61bcfeab36")
.clientId(authProps.getClientId())
.clientName(authProps.getClientName())
.clientSecret(encoder.encode(authProps.getClientSecret()))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8000/login/oauth2/code/web-client")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.tokenSettings(tokenSettings)
.build();
clientRepository.save(webClient);
return clientRepository;
}
#Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository,
ObjectMapper objectMapper) {
JdbcOAuth2AuthorizationService authorizationService =
new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);
ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
objectMapper.registerModules(SecurityJackson2Modules.getModules(classLoader));
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
// You will need to write the Mixin for your class so Jackson can marshall it.
// objectMapper.addMixIn(UserPrincipal .class, UserPrincipalMixin.class);
rowMapper.setObjectMapper(objectMapper);
authorizationService.setAuthorizationRowMapper(rowMapper);
return authorizationService;
}
#Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
#Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
private static RSAKey generateRsa() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
#Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder()
.issuer(authProps.getIssuerUri())
.build();
}
#Bean
public TokenSettings tokenSettings() {
return TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofDays(1))
.refreshTokenTimeToLive(Duration.ofDays(1))
.build();
}
}
But am still facing the same issue.
How do I solve this? Any assistance is highly appreciated.
After trying out different solutions this was how I was able to solve it.
I changed my OAuth2AuthorizationService bean to look like this.
#Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService authorizationService =
new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper =
new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);
JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper oAuth2AuthorizationParametersMapper =
new JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper();
ObjectMapper objectMapper = new ObjectMapper();
ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
objectMapper.registerModules(securityModules);
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
objectMapper.addMixIn(JwtAuthenticationToken.class, JwtAuthenticationTokenMixin.class);
rowMapper.setObjectMapper(objectMapper);
oAuth2AuthorizationParametersMapper.setObjectMapper(objectMapper);
authorizationService.setAuthorizationRowMapper(rowMapper);
authorizationService.setAuthorizationParametersMapper(oAuth2AuthorizationParametersMapper);
return authorizationService;
}
and here is my JwtAuthenticationTokenMixin configurations
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
#JsonDeserialize(using = JwtAuthenticationTokenDeserializer.class)
#JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
#JsonIgnoreProperties(ignoreUnknown = true)
public abstract class JwtAuthenticationTokenMixin {}
class JwtAuthenticationTokenDeserializer extends JsonDeserializer<JwtAuthenticationToken> {
#Override
public JwtAuthenticationToken deserialize(JsonParser parser, DeserializationContext context) throws IOException {
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
JsonNode root = mapper.readTree(parser);
return deserialize(parser, mapper, root);
}
private JwtAuthenticationToken deserialize(JsonParser parser, ObjectMapper mapper, JsonNode root)
throws JsonParseException {
JsonNode principal = JsonNodeUtils.findObjectNode(root, "principal");
if (!Objects.isNull(principal)) {
String tokenValue = principal.get("tokenValue").textValue();
long issuedAt = principal.get("issuedAt").longValue();
long expiresAt = principal.get("expiresAt").longValue();
Map<String, Object> headers = JsonNodeUtils.findValue(
principal, "headers", JsonNodeUtils.STRING_OBJECT_MAP, mapper);
Map<String, Object> claims = new HashMap<>();
claims.put("claims", principal.get("claims"));
Jwt jwt = new Jwt(tokenValue, Instant.ofEpochMilli(issuedAt), Instant.ofEpochMilli(expiresAt), headers, claims);
return new JwtAuthenticationToken(jwt);
}
return null;
}
}
abstract class JsonNodeUtils {
static final TypeReference<Set<String>> STRING_SET = new TypeReference<Set<String>>() {
};
static final TypeReference<Map<String, Object>> STRING_OBJECT_MAP = new TypeReference<Map<String, Object>>() {
};
static String findStringValue(JsonNode jsonNode, String fieldName) {
if (jsonNode == null) {
return null;
}
JsonNode value = jsonNode.findValue(fieldName);
return (value != null && value.isTextual()) ? value.asText() : null;
}
static <T> T findValue(JsonNode jsonNode, String fieldName, TypeReference<T> valueTypeReference,
ObjectMapper mapper) {
if (jsonNode == null) {
return null;
}
JsonNode value = jsonNode.findValue(fieldName);
return (value != null && value.isContainerNode()) ? mapper.convertValue(value, valueTypeReference) : null;
}
static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) {
if (jsonNode == null) {
return null;
}
JsonNode value = jsonNode.findValue(fieldName);
return (value != null && value.isObject()) ? value : null;
}
}
you don't need to create a Mixin, because it's all ready created by authorization springboot module. juste
#Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);
ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModules(new CoreJackson2Module());
objectMapper.registerModules(SecurityJackson2Modules.getModules(classLoader));
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
rowMapper.setObjectMapper(objectMapper);
authorizationService.setAuthorizationRowMapper(rowMapper);
return authorizationService;
}
i think you miss this line and is where the token mixin is registered
objectMapper.registerModules(new CoreJackson2Module());

Spring boot SAML 2 authentication object null

I've a requirement to integrate SAML authentication with rest API, so that I can make my rest services stateless, the approach which I've taken is as follows
Developed an authentication service behind zuul proxy which is running behind AWS ALB
User tries to generate token via endpoint https://my-domain/as/auth/login
Since user is not logged in, so he gets redirected to IDP where he authenticate
After authentication the IDP redirect user back to my service i.e. at URL https://my-domain/as/auth/login
I check for user authentication principal and if the user is authenticated then I generate JWT token and return it to user
SAML authentication works well, the issue is when the user is redirected back to success URL i.e. https://my-domain/as/auth/login then the authentication object is null because the SecurityContextHolder is cleared after successful authentication and 401 handler kicks in and redirect user to IDP in loop until SAML assertion is failed, please suggest where I'm mistaking
My Zuul proxy config looks like below
zuul:
ribbon:
eager-load:
enabled: true
host:
connect-timeout-millis: 5000000
socket-timeout-millis: 5000000
ignoredServices: ""
ignoredPatterns:
- /as/*
routes:
sensitiveHeaders: Cookie,Set-Cookie
import-data-service:
path: /ids/*
serviceId: IDS
stripPrefix: true
authentication-service:
path: /as/*
serviceId: AS
stripPrefix: true
customSensitiveHeaders: false
My SAML security config looks like below
#Configuration
public class SamlSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public WebSSOProfileOptions defaultWebSSOProfileOptions() {
WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
webSSOProfileOptions.setIncludeScoping(false);
webSSOProfileOptions.setBinding(SAMLConstants.SAML2_POST_BINDING_URI);
return webSSOProfileOptions;
}
#Bean
public SAMLEntryPoint samlEntryPoint() {
SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
samlEntryPoint.setDefaultProfileOptions(defaultWebSSOProfileOptions());
return samlEntryPoint;
}
#Bean
public MetadataDisplayFilter metadataDisplayFilter() {
return new MetadataDisplayFilter();
}
#Bean
public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
return new SimpleUrlAuthenticationFailureHandler();
}
#Bean
public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler =
new SavedRequestAwareAuthenticationSuccessHandler();
successRedirectHandler.setDefaultTargetUrl("https://my-domain/as/saml/SSO");
return successRedirectHandler;
}
#Bean
public SessionRepositoryFilter sessionFilter() {
HttpSessionStrategy cookieStrategy = new CookieHttpSessionStrategy();
MapSessionRepository repository = new MapSessionRepository();
((CookieHttpSessionStrategy) cookieStrategy).setCookieName("JSESSIONID");
SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter(repository);
sessionRepositoryFilter.setHttpSessionStrategy(cookieStrategy);
return sessionRepositoryFilter;
}
#Bean
public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
samlWebSSOProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
return samlWebSSOProcessingFilter;
}
#Bean
public HttpStatusReturningLogoutSuccessHandler successLogoutHandler() {
return new HttpStatusReturningLogoutSuccessHandler();
}
#Bean
public SecurityContextLogoutHandler logoutHandler() {
SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
logoutHandler.setInvalidateHttpSession(true);
logoutHandler.setClearAuthentication(true);
return logoutHandler;
}
#Bean
public SAMLLogoutFilter samlLogoutFilter() {
return new SAMLLogoutFilter(successLogoutHandler(), new LogoutHandler[] {logoutHandler()},
new LogoutHandler[] {logoutHandler()});
}
#Bean
public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() {
return new SAMLLogoutProcessingFilter(successLogoutHandler(), logoutHandler());
}
#Bean
public MetadataGeneratorFilter metadataGeneratorFilter() {
return new MetadataGeneratorFilter(metadataGenerator());
}
#Bean
public MetadataGenerator metadataGenerator() {
MetadataGenerator metadataGenerator = new MetadataGenerator();
metadataGenerator.setEntityId("entityUniqueIdenifier");
metadataGenerator.setExtendedMetadata(extendedMetadata());
metadataGenerator.setIncludeDiscoveryExtension(false);
metadataGenerator.setRequestSigned(true);
metadataGenerator.setKeyManager(keyManager());
metadataGenerator.setEntityBaseURL("https://my-domain/as");
return metadataGenerator;
}
#Bean
public KeyManager keyManager() {
ClassPathResource storeFile = new ClassPathResource("/saml-keystore.jks");
String storePass = "samlstorepass";
Map<String, String> passwords = new HashMap<>();
passwords.put("mykeyalias", "mykeypass");
return new JKSKeyManager(storeFile, storePass, passwords, "mykeyalias");
}
#Bean
public ExtendedMetadata extendedMetadata() {
ExtendedMetadata extendedMetadata = new ExtendedMetadata();
extendedMetadata.setIdpDiscoveryEnabled(false);
extendedMetadata.setSignMetadata(false);
extendedMetadata.setSigningKey("mykeyalias");
extendedMetadata.setEncryptionKey("mykeyalias");
return extendedMetadata;
}
#Bean
public FilterChainProxy samlFilter() throws Exception {
List<SecurityFilterChain> chains = new ArrayList<>();
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"),
metadataDisplayFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
samlEntryPoint()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"),
samlWebSSOProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSOHoK/**"),
samlWebSSOHoKProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"),
samlLogoutFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
samlLogoutProcessingFilter()));
return new FilterChainProxy(chains);
}
#Bean
public TLSProtocolConfigurer tlsProtocolConfigurer() {
return new TLSProtocolConfigurer();
}
#Bean
public ProtocolSocketFactory socketFactory() {
return new TLSProtocolSocketFactory(keyManager(), null, "default");
}
#Bean
public Protocol socketFactoryProtocol() {
return new Protocol("https", socketFactory(), 443);
}
#Bean
public MethodInvokingFactoryBean socketFactoryInitialization() {
MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
methodInvokingFactoryBean.setTargetClass(Protocol.class);
methodInvokingFactoryBean.setTargetMethod("registerProtocol");
Object[] args = {"https", socketFactoryProtocol()};
methodInvokingFactoryBean.setArguments(args);
return methodInvokingFactoryBean;
}
#Bean
public VelocityEngine velocityEngine() {
return VelocityFactory.getEngine();
}
#Bean(initMethod = "initialize")
public StaticBasicParserPool parserPool() {
return new StaticBasicParserPool();
}
#Bean(name = "parserPoolHolder")
public ParserPoolHolder parserPoolHolder() {
return new ParserPoolHolder();
}
#Bean
public HTTPPostBinding httpPostBinding() {
return new HTTPPostBinding(parserPool(), velocityEngine());
}
#Bean
public HTTPRedirectDeflateBinding httpRedirectDeflateBinding() {
return new HTTPRedirectDeflateBinding(parserPool());
}
#Bean
public SAMLProcessorImpl processor() {
Collection<SAMLBinding> bindings = new ArrayList<>();
bindings.add(httpRedirectDeflateBinding());
bindings.add(httpPostBinding());
return new SAMLProcessorImpl(bindings);
}
#Bean
public HttpClient httpClient() {
return new HttpClient(multiThreadedHttpConnectionManager());
}
#Bean
public MultiThreadedHttpConnectionManager multiThreadedHttpConnectionManager() {
return new MultiThreadedHttpConnectionManager();
}
#Bean
public static SAMLBootstrap sAMLBootstrap() {
return new CustomSamlBootStrap();
}
#Bean
public SAMLDefaultLogger samlLogger() {
SAMLDefaultLogger samlDefaultLogger = new SAMLDefaultLogger();
samlDefaultLogger.setLogAllMessages(true);
samlDefaultLogger.setLogErrors(true);
return samlDefaultLogger;
}
#Bean
public SAMLContextProviderImpl contextProvider() {
SAMLContextProviderLB samlContextProviderLB = new SAMLContextProviderLB();
samlContextProviderLB.setServerName("my-domain/as");
samlContextProviderLB.setScheme("https");
samlContextProviderLB.setServerPort(443);
samlContextProviderLB.setIncludeServerPortInRequestURL(false);
samlContextProviderLB.setContextPath("");
// samlContextProviderLB.setStorageFactory(new EmptyStorageFactory());
return samlContextProviderLB;
}
// SAML 2.0 WebSSO Assertion Consumer
#Bean
public WebSSOProfileConsumer webSSOprofileConsumer() {
return new WebSSOProfileConsumerImpl();
}
// SAML 2.0 Web SSO profile
#Bean
public WebSSOProfile webSSOprofile() {
return new WebSSOProfileImpl();
}
// not used but autowired...
// SAML 2.0 Holder-of-Key WebSSO Assertion Consumer
#Bean
public WebSSOProfileConsumerHoKImpl hokWebSSOprofileConsumer() {
return new WebSSOProfileConsumerHoKImpl();
}
// not used but autowired...
// SAML 2.0 Holder-of-Key Web SSO profile
#Bean
public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() {
return new WebSSOProfileConsumerHoKImpl();
}
#Bean
public SingleLogoutProfile logoutprofile() {
return new SingleLogoutProfileImpl();
}
#Bean
public ExtendedMetadataDelegate idpMetadata()
throws MetadataProviderException, ResourceException {
Timer backgroundTaskTimer = new Timer(true);
ResourceBackedMetadataProvider resourceBackedMetadataProvider =
new ResourceBackedMetadataProvider(backgroundTaskTimer,
new ClasspathResource("/IDP-metadata.xml"));
resourceBackedMetadataProvider.setParserPool(parserPool());
ExtendedMetadataDelegate extendedMetadataDelegate =
new ExtendedMetadataDelegate(resourceBackedMetadataProvider, extendedMetadata());
extendedMetadataDelegate.setMetadataTrustCheck(true);
extendedMetadataDelegate.setMetadataRequireSignature(false);
return extendedMetadataDelegate;
}
#Bean
#Qualifier("metadata")
public CachingMetadataManager metadata() throws MetadataProviderException, ResourceException {
List<MetadataProvider> providers = new ArrayList<>();
providers.add(idpMetadata());
return new CachingMetadataManager(providers);
}
#Bean
public SAMLUserDetailsService samlUserDetailsService() {
return new SamlUserDetailsServiceImpl();
}
#Bean
public SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter() throws Exception {
final SAMLWebSSOHoKProcessingFilter filter = new SAMLWebSSOHoKProcessingFilter();
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(successRedirectHandler());
filter.setAuthenticationFailureHandler(authenticationFailureHandler());
return filter;
}
#Bean
public SAMLAuthenticationProvider samlAuthenticationProvider() {
SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider();
samlAuthenticationProvider.setUserDetails(samlUserDetailsService());
samlAuthenticationProvider.setForcePrincipalAsString(false);
return samlAuthenticationProvider;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(samlAuthenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().authenticationEntryPoint(samlEntryPoint());
http.csrf().disable();
http.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
.addFilterAfter(samlFilter(), BasicAuthenticationFilter.class);
http.authorizeRequests().antMatchers("/error").permitAll().antMatchers("/saml/**").permitAll()
.anyRequest().authenticated();
http.logout().logoutSuccessUrl("/");
}
Finally found out the issue.
In Zuul, the sensitiveHeaders has got a default value of Cookie,Set-Cookie,Authorization
Now if we dont set the property itself, then these headers are going to be treated as sensitive and doesnt get flown downstream to our service.
Had to set the the sensitiveHeaders value to empty so that the cookies get passed on to the service. Cookie contains our JSESSIONID which has identifies the session.
zuul:
ribbon:
eager-load:
enabled: true
host:
connect-timeout-millis: 5000000
socket-timeout-millis: 5000000
ignoredServices: ""
sensitiveHeaders:
ignoredPatterns:
- /as/*
routes:
import-data-service:
path: /ids/*
serviceId: IDS
stripPrefix: true
authentication-service:
path: /as/*
serviceId: AS
stripPrefix: true
customSensitiveHeaders: false

Cannot throw custom exception message for JWT CustomClaimVerifier

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;
}
}

Twitter Sign in using Spring Boot

I am trying to sign in to my web application (developed using Spring Boot) using social logins. The logins for Google & facebook are okay. But the for some reason there is a token issue in the twitter login. I have created the project in the twitter developer site obtained all the credentials. Please refer to my code below.
My Property file values are mentioned below.
twitter.client.client-id=XXXXXXX
twitter.client.client-secret=XXXXXXXX
twitter.client.access-token-uri=https://api.twitter.com/oauth/access_token
twitter.client.user-authorization-uri=https://api.twitter.com/oauth/authorize
twitter.client.token-name=oauth_token
twitter.client.authentication-scheme=form
twitter.resource.user-info-uri=https://api.twitter.com/1.1/account/verify_credentials.json
The filter method
private Filter ssoTwitterFilter(String processingUrl, PrincipalExtractor principalExtractor) {
OAuth2ClientAuthenticationProcessingFilter twitterFilter = new OAuth2ClientAuthenticationProcessingFilter(
processingUrl);
LOGGER.debug("processingUrl :{} ", processingUrl);
twitterFilter.setAuthenticationSuccessHandler(authenticationSuccessHandlerAndRegistrationFilter());
OAuth2RestTemplate twitterTemplate = new OAuth2RestTemplate(twitter(), oauth2ClientContext);
twitterFilter.setRestTemplate(twitterTemplate);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(twitterResource().getUserInfoUri(),
twitter().getClientId());
tokenServices.setRestTemplate(twitterTemplate);
tokenServices.setPrincipalExtractor(principalExtractor);
return twitterFilter;
}
These are the bean configurations.
#Bean
#ConfigurationProperties("twitter.client")
public AuthorizationCodeResourceDetails twitter() {
return new AuthorizationCodeResourceDetails();
}
#Bean
#ConfigurationProperties("twitter.resource")
public ResourceServerProperties twitterResource() {
return new ResourceServerProperties();
}
This is the error that I get
enter image description here
Please can anyone shed some light on this. Because all the samples I found were related getting profile information from twitter where as i need a sample for sign in using spring Boot. Thanks in advance
You can configure Twitter login like this:
#Configuration
#EnableSocial
public class SocialConfig implements SocialConfigurer {
#Autowired
private UserAuthorizationService userAuthorizationService;
#Override
public void addConnectionFactories(ConnectionFactoryConfigurer cfConfig, Environment env) {
cfConfig.addConnectionFactory(new TwitterConnectionFactory(
env.getProperty("twitter.consumer-key"),
env.getProperty("twitter.consumer-secret")
));
}
#Override
public UserIdSource getUserIdSource() {
return new UserIdSource() {
#Override
public String getUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new IllegalStateException("Unable to get a ConnectionRepository: no user signed in");
}
return authentication.getName();
}
};
}
#Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
InMemoryUsersConnectionRepository usersConnectionRepository = new InMemoryUsersConnectionRepository(
connectionFactoryLocator
);
return usersConnectionRepository;
}
#Autowired
private TwitterConnectionSignup twitterConnectionSignup;
#Autowired
private ConnectionFactoryLocator connectionFactoryLocator;
#Autowired
private UsersConnectionRepository usersConnectionRepository;
#Bean
public ProviderSignInController providerSignInController() {
((InMemoryUsersConnectionRepository) usersConnectionRepository)
.setConnectionSignUp(twitterConnectionSignup);
return new ProviderSignInController(
connectionFactoryLocator,
usersConnectionRepository,
new TwitterSignInAdapter(userAuthorizationService));
}
}
Configure TwitterConnectionSignup:
#Service
public class TwitterConnectionSignup implements ConnectionSignUp {
#Autowired
private UserRepo userRepo;
#Override
public String execute(Connection<?> connection) {
//add your logic to save user to your db
return connection.getDisplayName();
}
}
Now configure TwitterSignInAdapter:
public class TwitterSignInAdapter implements SignInAdapter {
private UserAuthorizationService userAuthorizationService;
public TwitterSignInAdapter(UserAuthorizationService userAuthorizationService) {
this.userAuthorizationService = userAuthorizationService;
}
#Override
public String signIn(String localUserId, Connection<?> connection, NativeWebRequest webRequest) {
log.debug(" Email {}", localUserId);
UserAuthDto userAuthDto = (UserAuthDto) userAuthorizationService.loadUserByUsername(localUserId);
UsernamePasswordAuthenticationToken updatedAuth = new UsernamePasswordAuthenticationToken(userAuthDto, userAuthDto.getSocialId(),
userAuthDto.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(updatedAuth);
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
// add authentication to the session
servletRequest.getSession().setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext());
return "/";
}
}

Resources