Spring reactive OAuth2 resource-server: include both access and ID tokens claims in the `Authentication` - spring

I have a AWS Cognito user pool issuing tokens to my frontend application. The frontend application then uses the tokens to talk to my backend service.
This flow is working as intended. I am validating the tokens that hit my backend service using org.springframework.security:spring-security-oauth2-resource-server:6.0.1 which is configured to point back to Cognito
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://cognito-idp.us-east-1.amazonaws.com/my_pool_endpoint
I have a simple SecurityConfig
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity(useAuthorizationManager = true)
public class SecurityConfig {
#Bean
SecurityWebFilterChain securityWebFilterChain(final ServerHttpSecurity http) {
return http.authorizeExchange()
.pathMatchers("/v3/api-docs/**")
.permitAll()
.anyExchange()
.authenticated()
.and()
.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt)
.build();
}
So far everything is looking good.
But how do I gather additional information from incoming tokens, for example things such as email and username are not included in the token response from Cognito. An example of a decoded token looks like:
{
"sub": "00000000000000000000",
"cognito:groups": [
"00000000000000000000"
],
"iss": "https://cognito-idp.us-east-1.amazonaws.com/00000000000000000000",
"version": 2,
"client_id": "00000000000000000000",
"origin_jti": "00000000000000000000",
"token_use": "access",
"scope": "openid profile email",
"auth_time": 1676066347,
"exp": 1676186814,
"iat": 1676143614,
"jti": "00000000000000000000",
"username": "google_00000000000000000000"
}
When I need extra information from the token, I'm calling https://my-congito-pool.auth.us-east-1.amazoncognito.com/oauth2/userInfo and passing the JWT as the Bearer token, which works and returns the information I'm looking for such as email, picture, username etc.
My question is I don't think doing this manually every time I want additional information is the 'correct' way of handling it.
Should I be using something like a UserDetailsService to perform this once and transforming the incoming JWT into my own User which holds this information?
If so, how do I do this using ReactiveSpringSecurity?

It looks like Cognito allows to enrich ID tokens, but not access tokens. That's sad, most competitors allow it and it makes spring resource-servers configurations much easier.
I can think of two solutions:
configure your resource-server with access-token introspection (with http.oauth2ResourceServer().opaqueToken()), using your /oauth2/userInfo as introspection endpoint and the JWT access-token as "opaque" token
require clients to add the ID token in a dedicated header (let's say X-ID-Token) in addition to the access token (provided in the Authorization header as usual). Then in the authentication converter, retrieve and decode this additional header and build an Authentication of your own with both access and ID tokens strings and claims
I will only develop the second solution for two reasons:
the first has the usual performance cost of token introspection (a call is made from the resource-server to the authorization-server before each request is processed)
the second permits to add any data from as many headers as we need to the Authentication instance for authentication and authorization (not only ID token as we demo here) with very little performance impact
Spoiler: here is what I got:
with valid access and ID tokens
with just the access-token
Isn't it exactly what you are looking for: an Authentication instance with the roles from the access token and the email from the ID token (or Unauthorized if authorization data is missing / invalid / incomplete)?
Detailed Security Configuration
Here is the security configuration for a reactive app. For Servlets, main lines are the same, only the tooling to statically access the request context is quite different. You can refer to this tutorial I just added to my collection for details.
#Configuration
#EnableReactiveMethodSecurity
#EnableWebFluxSecurity
public class SecurityConfig {
static final String ID_TOKEN_HEADER_NAME = "X-ID-Token";
public static Mono<ServerHttpRequest> getServerHttpRequest() {
return Mono.deferContextual(Mono::just)
.map(contextView -> contextView.get(ServerWebExchange.class).getRequest());
}
public static Mono<String> getIdTokenHeader() {
return getServerHttpRequest().map(req -> {
final var headers = req.getHeaders().getOrEmpty(ID_TOKEN_HEADER_NAME).stream()
.filter(StringUtils::hasLength).toList();
if (headers.size() == 0) {
throw new MissingIdTokenException();
}
if (headers.size() > 1) {
throw new MultiValuedIdTokenException();
}
return headers.get(0);
});
}
#Bean
SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder jwtDecoder) {
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(accessToken -> getIdTokenHeader()
.flatMap(idTokenString -> jwtDecoder.decode(idTokenString).doOnError(JwtException.class, e -> {
throw new InvalidIdTokenException();
}).map(idToken -> {
final var idClaims = idToken.getClaims();
#SuppressWarnings("unchecked")
final var authorities = ((List<String>) accessToken.getClaims().getOrDefault("cognito:groups",
List.of())).stream().map(SimpleGrantedAuthority::new).toList();
return new MyAuth(authorities, accessToken.getTokenValue(), idTokenString, accessToken.getClaims(),
idClaims);
})));
http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance()).csrf().disable();
http.authorizeExchange().anyExchange().authenticated();
return http.build();
}
public static class MyAuth extends AbstractAuthenticationToken {
private static final long serialVersionUID = 9115947200114995708L;
// Save access and ID tokens strings just in case we need to call another
// micro-service on behalf of the user who initiated the request and as so,
// position "Authorization" and "X-ID-Token" headers
private final String accessTokenString;
private final String idTokenString;
private final Map<String, Object> accessClaims;
private final Map<String, Object> idClaims;
public MyAuth(Collection<? extends GrantedAuthority> authorities, String accessTokenString,
String idTokenString, Map<String, Object> accessClaims, Map<String, Object> idClaims) {
super(authorities);
this.accessTokenString = accessTokenString;
this.accessClaims = Collections.unmodifiableMap(accessClaims);
this.idTokenString = idTokenString;
this.idClaims = Collections.unmodifiableMap(idClaims);
// Minimal security checks: assert that issuer and subject claims are the same
// in access and ID tokens.
if (!Objects.equals(accessClaims.get(IdTokenClaimNames.ISS), idClaims.get(IdTokenClaimNames.ISS))
|| !Objects.equals(accessClaims.get(StandardClaimNames.SUB), idClaims.get(IdTokenClaimNames.SUB))) {
throw new InvalidIdTokenException();
}
// You could also make assertions on ID token audience, but this will require
// adding a custom property for expected ID tokens audience.
// You can't just check for audience equality with already validated access
// token one.
this.setAuthenticated(true);
}
#Override
public String getCredentials() {
return accessTokenString;
}
#Override
public String getPrincipal() {
return (String) accessClaims.get(StandardClaimNames.SUB);
}
public String getAccessTokenString() {
return accessTokenString;
}
public String getIdTokenString() {
return idTokenString;
}
public Map<String, Object> getAccessClaims() {
return accessClaims;
}
public Map<String, Object> getIdClaims() {
return idClaims;
}
}
#ResponseStatus(code = HttpStatus.UNAUTHORIZED, reason = ID_TOKEN_HEADER_NAME + " is missing")
static class MissingIdTokenException extends RuntimeException {
private static final long serialVersionUID = -4894061353773464761L;
}
#ResponseStatus(code = HttpStatus.UNAUTHORIZED, reason = ID_TOKEN_HEADER_NAME + " is not unique")
static class MultiValuedIdTokenException extends RuntimeException {
private static final long serialVersionUID = 1654993007508549674L;
}
#ResponseStatus(code = HttpStatus.UNAUTHORIZED, reason = ID_TOKEN_HEADER_NAME + " is not valid")
static class InvalidIdTokenException extends RuntimeException {
private static final long serialVersionUID = -6233252290377524340L;
}
}
Now, each time an authorization succeeds (isAuthenticated() is true), you'll have a MyAuth instance in the security context and it contains both the access and ID tokens claims!
Sample Controller
#RestController
public class GreetingController {
#GetMapping("/greet")
#PreAuthorize("isAuthenticated()")
Mono<String> greet(MyAuth auth) {
return Mono.just("Hello %s! You are granted with %s".formatted(
auth.getIdClaims().get("email"),
auth.getAuthorities()));
}
}
You may also build your #PreAuthorize expressions based on it. Something like:
#RequiredArgsConstructor
#RestController
#RequestMapping("/something/protected")
#PreAuthorize("isAuthenticated()")
public class ProtectedResourceController {
private final SomeResourceRepository resourceRepo;
#GetMapping("/{resourceId}")
#PreAuthorize("#auth.idClaims['email'] == #resource.email")
ResourceDto getProtectedResource(MyAuth auth, #RequestParam("resourceId") SomeResource resource) {
...
}
}
EDIT: Code reusability & spring-addons starters
I maintain wrappers around spring-boot-starter-oauth2-resource-server. It is very thin and opensource. If you don't want to use it, you should have a look at how it is done to get inspiration from it:
inspect resources to find out what it takes to build your own spring-boot starters
inspect beans to pick ideas for creating your own configurable ones
browse to dependencies like OpenidClaimSet and OAuthentication which could be of inspiration
Here is what the sample above becomes with "my" starter for reactive resource-servers with JWT decoders:
#Configuration
#EnableReactiveMethodSecurity
#EnableWebFluxSecurity
public class SecurityConfig {
static final String ID_TOKEN_HEADER_NAME = "X-ID-Token";
#Bean
OAuth2AuthenticationFactory authenticationFactory(
Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter,
ReactiveJwtDecoder jwtDecoder) {
return (accessBearerString, accessClaims) -> ServerHttpRequestSupport.getUniqueHeader(ID_TOKEN_HEADER_NAME)
.flatMap(idTokenString -> jwtDecoder.decode(idTokenString).doOnError(JwtException.class, e -> {
throw new InvalidHeaderException(ID_TOKEN_HEADER_NAME);
}).map(idToken -> new MyAuth(
authoritiesConverter.convert(accessClaims),
accessBearerString,
new OpenidClaimSet(accessClaims),
idTokenString,
new OpenidClaimSet(idToken.getClaims()))));
}
#Data
#EqualsAndHashCode(callSuper = true)
public static class MyAuth extends OAuthentication<OpenidClaimSet> {
private static final long serialVersionUID = 1734079415899000362L;
private final String idTokenString;
private final OpenidClaimSet idClaims;
public MyAuth(Collection<? extends GrantedAuthority> authorities, String accessTokenString,
OpenidClaimSet accessClaims, String idTokenString, OpenidClaimSet idClaims) {
super(accessClaims, authorities, accessTokenString);
this.idTokenString = idTokenString;
this.idClaims = idClaims;
}
}
}
Update #Controller (pay attention to the direct accessor to email claim):
#RestController
public class GreetingController {
#GetMapping("/greet")
#PreAuthorize("isAuthenticated()")
Mono<String> greet(MyAuth auth) {
return Mono.just("Hello %s! You are granted with %s".formatted(
auth.getIdClaims().getEmail(),
auth.getAuthorities()));
}
}
This are the configuration properties (with different claims used as authorities source depending on the authorization-server configured in the profile):
server:
error.include-message: always
spring:
lifecycle.timeout-per-shutdown-phase: 30s
security.oauth2.resourceserver.jwt.issuer-uri: https://localhost:8443/realms/master
com:
c4-soft:
springaddons:
security:
issuers:
- location: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}
authorities:
claims:
- realm_access.roles
- resource_access.spring-addons-public.roles
- resource_access.spring-addons-confidential.roles
caze: upper
prefix: ROLE_
cors:
- path: /greet
---
spring.config.activate.on-profile: cognito
spring.security.oauth2.resourceserver.jwt.issuer-uri: https://cognito-idp.us-west-2.amazonaws.com/us-west-2_RzhmgLwjl
com.c4-soft.springaddons.security.issuers:
- location: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}
authorities:
claims:
- cognito:groups
caze: upper
prefix: ROLE_
---
spring.config.activate.on-profile: auth0
com.c4-soft.springaddons.security.issuers:
- location: https://dev-ch4mpy.eu.auth0.com/
authorities:
claims:
- roles
- permissions
caze: upper
prefix: ROLE_
Unit-tests with mocked identity for the #Controller above can be as simple as:
#WebFluxTest(controllers = GreetingController.class)
#AutoConfigureAddonsWebSecurity
#Import(SecurityConfig.class)
class GreetingControllerTest {
#Autowired
WebTestClientSupport api;
#Test
#WithMyAuth(authorities = { "AUTHOR" }, idClaims = #OpenIdClaims(email = "ch4mp#c4-soft.com"))
void givenUserIsAuthenticated_whenGreet_thenOk() throws Exception {
api.get("/greet").expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello ch4mp#c4-soft.com! You are granted with [AUTHOR]");
}
#Test
void givenRequestIsAnonymous_whenGreet_thenUnauthorized() throws Exception {
api.get("/greet").expectStatus().isUnauthorized();
}
}
With annotation definition (to build the custom Authentication implementation and set it in the security context):
#Target({ ElementType.METHOD, ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
#Inherited
#Documented
#WithSecurityContext(factory = WithMyAuth.MyAuthFactory.class)
public #interface WithMyAuth {
#AliasFor("authorities")
String[] value() default {};
#AliasFor("value")
String[] authorities() default {};
OpenIdClaims accessClaims() default #OpenIdClaims();
OpenIdClaims idClaims() default #OpenIdClaims();
String accessTokenString() default "machin.truc.chose";
String idTokenString() default "machin.bidule.chose";
#AliasFor(annotation = WithSecurityContext.class)
TestExecutionEvent setupBefore()
default TestExecutionEvent.TEST_METHOD;
#Target({ ElementType.METHOD, ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
public static #interface Proxy {
String onBehalfOf();
String[] can() default {};
}
public static final class MyAuthFactory extends AbstractAnnotatedAuthenticationBuilder<WithMyAuth, MyAuth> {
#Override
public MyAuth authentication(WithMyAuth annotation) {
final var accessClaims = new OpenidClaimSet(super.claims(annotation.accessClaims()));
final var idClaims = new OpenidClaimSet(super.claims(annotation.idClaims()));
return new MyAuth(super.authorities(annotation.authorities()), annotation.accessTokenString(), accessClaims, annotation.idTokenString(), idClaims);
}
}
}
And this is the pom body:
<properties>
<java.version>17</java.version>
<spring-addons.version>6.0.13</spring-addons.version>
</properties>
<dependencies>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webflux-jwt-resource-server</artifactId>
<version>${spring-addons.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webflux-jwt-test</artifactId>
<version>${spring-addons.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

Related

Hashing a Password and Saving to application.yaml (Spring Boot 2.7.3)

My objective is to hash a password that is passed from the command line argument and save it to the application.yaml. Then when the user authenticates using the password to match it and allow users in.
I am passing the value like this:
mvn spring-boot:run -Dspring-boot.run.arguments="--user.password=admin"
The fragment of the security section in application.yaml looks like this:
security:
user:
name: admin
password: ${user.password}
I have written some password encoder classes in my spring boot application.
class MyPasswordEncoder : PasswordEncoder {
private val logger = LoggerFactory.getLogger(javaClass)
private val argon2PasswordEncoder = Argon2PasswordEncoder(SALT_LENGTH, HASH_LENGTH, PARALLELISM, MEMORY_USE, ITERATIONS)
/**
* Encrypt the password (Algorithm Used: Argon2id)
*/
override fun encode(decryptedPassword: CharSequence?): String {
val hashedPassword : String = argon2PasswordEncoder.encode(decryptedPassword)
logger.debug("Password Encoding Successful!")
return hashedPassword
}
/**
* Matches the password with the encoded password (Algorithm Used: Argon2id)
*/
override fun matches(rawPassword: CharSequence?, encodedPassword: String?): Boolean {
return argon2PasswordEncoder.matches(rawPassword, encodedPassword)
}
}
And the Delegation class:
class MyPasswordDelegation {
fun createDelegatingPasswordEncoder(): PasswordEncoder {
val idForEncode = "myEncoder"
val encoders: MutableMap<String, PasswordEncoder> = mutableMapOf()
encoders[idForEncode] = NCaaSPasswordEncoder()
return DelegatingPasswordEncoder(idForEncode, encoders)
}
}
My Security Config class looks like this:
#Configuration
#EnableWebSecurity
class SecurityConfig {
#Value("\${spring.security.user.name}")
private val userName: String? = null
#Value("\${spring.security.user.password}")
private val password: String? = null
#Autowired
lateinit var appAuthenticationEntryPoint: AppAuthenticationEntryPoint
#Bean
fun passwordEncoder(): MyPasswordEncoder {
return MyPasswordEncoder()
}
#Bean
#Throws(Exception::class)
fun userDetailsService(): InMemoryUserDetailsManager? {
val userDetails : UserDetails = User.withUsername(userName).password(passwordEncoder().encode(password).roles("USER").build()
return InMemoryUserDetailsManager(userDetails)
}
#Throws(Exception::class)
#Bean
fun filterChain(httpSecurity : HttpSecurity): SecurityFilterChain {
httpSecurity.csrf().disable()
// Allow only HTTPS Requests
httpSecurity.requiresChannel {
channel -> channel.anyRequest().requiresSecure()
}.authorizeRequests {
authorize -> authorize.anyRequest().fullyAuthenticated().and().httpBasic().and().exceptionHandling().authenticationEntryPoint(appAuthenticationEntryPoint)
}
return httpSecurity.build()
}
}
But when I try to authenticate using admin/admin, I get the following error:
Failed to process authentication request
org.springframework.security.authentication.BadCredentialsException: Bad credentials
at org.springframework.security.authentication.dao.DaoAuthenticationProvider.additionalAuthenticationChecks(DaoAuthenticationProvider.java:79)
at org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:147)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:201)
at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilterInternal(BasicAuthenticationFilter.java:172)
I know I am going wrong somewhere. But since this is all new to me, I am not able to quite put it all together.
Any help would be appreciated. Thanks!

authenticating mock user when testing in quarkus

I'm trying to test a quarkus rest-endpoint which is secured with #RolesAllowed
...
#GET
#Path("{id}")
#Produces(MediaType.APPLICATION_OCTET_STREAM)
#RolesAllowed({ "APPLICATION_USER"})
public Response getFile(#PathParam(value = "id") String documentId, #Context UriInfo uriInfo)
...
The test case
#QuarkusTest
class DocumentResourceTest {
#Test
public void testDocumentEndpoint() {
String documentId = "someId";
given()
.when().get("/documents/" + documentId)
.then()
.statusCode(200);
}
}
How can i mock an authenticated user with role 'APPLICATION_USER' for my test case ?
You can inject a SecurityIdentity which you can then stub out with the relevant role using Mockito:
#QuarkusTest
public class DocumentResourceTest {
#InjectMock
SecurityIdentity identity;
#BeforeEach
public void setup() {
Mockito.when(identity.hasRole("APPLICATION_USER")).thenReturn(true);
}
#Test
public void testDocumentEndpoint() {
String documentId = "someId";
given()
.when().get("/documents/" + documentId)
.then()
.statusCode(200);
}
}
You can of course move the stubbing call to your individual tests if you want to test a variety of different roles.
Note that you'll need to add the quarkus-junit5-mockito dependency for this to work:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
A more convinient way to mock the security is to use Quarkus' security testing features:
https://quarkus.io/guides/security-testing#testing-security
Including
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security</artifactId>
<scope>test</scope>
</dependency>
allows you to write
#Test
#TestSecurity(authorizationEnabled = false)
void someTestMethod() {
...
}
#Test
#TestSecurity(user = "testUser", roles = {"admin", "user"})
void otherTestMethod() {
...
}
In addition to the accepted answer, there is also this guide which explains how to deal with integration tests: https://quarkus.io/guides/security-oauth2#integration-testing
The first sentence there is:
If you don’t want to use a real OAuth2 authorization server for your integration tests, you can use the Properties based security extension for your test, or mock an authorization server using Wiremock.
So I think the property based security extension could also work for you: https://quarkus.io/guides/security-properties

spring: customizing the authorizationEndpoint (OAuth2)

I am trying to customize the code of the spring oauth authorization server.
for now I have just copied the framework authorizationEndpoint code and placed it in another class. I just changed the address mapping to /custom/oauth/authorize. I have also added #Controller before the class declaration otherwise this code will not be used at all:
#Controller
//#Order(Ordered.HIGHEST_PRECEDENCE)
#SessionAttributes("authorizationRequest")
public class AuthorizationEndpointCustom extends AuthorizationEndpoint {
#Autowired
private AuthenticationManager authenticationManager;
private AuthorizationCodeServices authorizationCodeServices = new InMemoryAuthorizationCodeServices();
private RedirectResolver redirectResolver = new DefaultRedirectResolver();
private UserApprovalHandler userApprovalHandler = new DefaultUserApprovalHandler();
private SessionAttributeStore sessionAttributeStore = new DefaultSessionAttributeStore();
private OAuth2RequestValidator oauth2RequestValidator = new DefaultOAuth2RequestValidator();
private String userApprovalPage = "forward:/oauth/confirm_access";
private String errorPage = "forward:/oauth/error";
private Object implicitLock = new Object();
public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) {
this.sessionAttributeStore = sessionAttributeStore;
}
public void setErrorPage(String errorPage) {
this.errorPage = errorPage;
}
#RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, #RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
System.out.println("\n\ninside custom authorization endpoint");
// Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
// query off of the authorization request instead of referring back to the parameters map. The contents of the
// parameters map will be stored without change in the AuthorizationRequest object once it is created.
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}
try {
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
// The resolved redirect URI is either the redirect_uri from the parameters or the one from
// clientDetails. Either way we need to store it on the AuthorizationRequest.
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
if (!StringUtils.hasText(resolvedRedirect)) {
throw new RedirectMismatchException(
"A redirectUri must be either supplied or preconfigured in the ClientDetails");
}
authorizationRequest.setRedirectUri(resolvedRedirect);
// We intentionally only validate the parameters requested by the client (ignoring any data that may have
// been added to the request by the manager).
oauth2RequestValidator.validateScope(authorizationRequest, client);
// Some systems may allow for approval decisions to be remembered or approved by default. Check for
// such logic here, and set the approved flag on the authorization request accordingly.
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
(Authentication) principal);
// TODO: is this call necessary?
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// Validation is all done, so we can check for auto approval...
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
// Place auth request into the model so that it is stored in the session
// for approveOrDeny to use. That way we make sure that auth request comes from the session,
// so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session.
model.put("authorizationRequest", authorizationRequest);
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
catch (RuntimeException e) {
sessionStatus.setComplete();
throw e;
}
}
private OAuth2AccessToken getAccessTokenForImplicitGrant(TokenRequest tokenRequest,
OAuth2Request storedOAuth2Request) {
OAuth2AccessToken accessToken = null;
// These 1 method calls have to be atomic, otherwise the ImplicitGrantService can have a race condition where
// one thread removes the token request before another has a chance to redeem it.
synchronized (this.implicitLock) {
accessToken = getTokenGranter().grant("implicit",
new ImplicitTokenRequest(tokenRequest, storedOAuth2Request));
}
return accessToken;
}
.
.
.
I have also instructed the framework to change the mappring from /oauth/authorize to /custom/oauth/authorize:
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).tokenStore(tokenStore())
.accessTokenConverter(tokenEnhancer()).pathMapping("/oauth/authorize", "/custom/authorize/");
}
but when I run the code I encounter the following error:
Description:
Field tokenGranter in com.example.demo.controller.AuthorizationEndpointCustom required a bean of type 'org.springframework.security.oauth2.provider.TokenGranter' that could not be found.
The injection point has the following annotations:
- #org.springframework.beans.factory.annotation.Autowired(required=true)
Action:
Consider defining a bean of type 'org.springframework.security.oauth2.provider.TokenGranter' in your configuration.
the parent class of AuthorizationEndpoint (AbstractEndpoint) declares tokenGranter but it is not instantiated. there is no #autowired for this and other attributes of this class. who does genereate and inject these variable into this class?
how can I get hold of tokenGranter obj and inject it?
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.provider.endpoint;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.util.Assert;
/**
* #author Dave Syer
*
*/
public class AbstractEndpoint implements InitializingBean {
protected final Log logger = LogFactory.getLog(getClass());
private WebResponseExceptionTranslator providerExceptionHandler = new DefaultWebResponseExceptionTranslator();
private TokenGranter tokenGranter;
private ClientDetailsService clientDetailsService;
private OAuth2RequestFactory oAuth2RequestFactory;
private OAuth2RequestFactory defaultOAuth2RequestFactory;
public void afterPropertiesSet() throws Exception {
Assert.state(tokenGranter != null, "TokenGranter must be provided");
Assert.state(clientDetailsService != null, "ClientDetailsService must be provided");
defaultOAuth2RequestFactory = new DefaultOAuth2RequestFactory(getClientDetailsService());
if (oAuth2RequestFactory == null) {
oAuth2RequestFactory = defaultOAuth2RequestFactory;
}
}
public void setProviderExceptionHandler(WebResponseExceptionTranslator providerExceptionHandler) {
this.providerExceptionHandler = providerExceptionHandler;
}
public void setTokenGranter(TokenGranter tokenGranter) {
this.tokenGranter = tokenGranter;
}
protected TokenGranter getTokenGranter() {
return tokenGranter;
}
protected WebResponseExceptionTranslator getExceptionTranslator() {
return providerExceptionHandler;
}
protected OAuth2RequestFactory getOAuth2RequestFactory() {
return oAuth2RequestFactory;
}
protected OAuth2RequestFactory getDefaultOAuth2RequestFactory() {
return defaultOAuth2RequestFactory;
}
public void setOAuth2RequestFactory(OAuth2RequestFactory oAuth2RequestFactory) {
this.oAuth2RequestFactory = oAuth2RequestFactory;
}
protected ClientDetailsService getClientDetailsService() {
return clientDetailsService;
}
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
this.clientDetailsService = clientDetailsService;
}
}
I am answering my own question.
I took a good look at the framework code and I found out that AuthorizationServerEndpointsConfiguration class creates an object of type AuthorizationEndpoint and populates it's attributes and then return this object as a bean.
I managed to solve above mentioned problem with TokenGranter by creating a bean of my new AuthorizationEndpointCustom the same way AuthorizationServerEndpointsConfiguration does. this is the code to do so:
#Autowired
private ClientDetailsService clientDetailsService;
#Autowired
AuthorizationServerEndpointsConfiguration asec;
#Bean
#Order(value = Ordered.HIGHEST_PRECEDENCE)
#Primary
public AuthorizationEndpoint authorizationEndpoint () throws Exception{
AuthorizationEndpointCustom authorizationEndpoint = new AuthorizationEndpointCustom();
FrameworkEndpointHandlerMapping mapping = asec.getEndpointsConfigurer().getFrameworkEndpointHandlerMapping();
authorizationEndpoint.setUserApprovalPage(extractPath(mapping, "/oauth/confirm_access"));
authorizationEndpoint.setProviderExceptionHandler(asec.getEndpointsConfigurer().getExceptionTranslator());
authorizationEndpoint.setErrorPage(extractPath(mapping, "/oauth/error"));
authorizationEndpoint.setTokenGranter(asec.getEndpointsConfigurer().getTokenGranter());
authorizationEndpoint.setClientDetailsService(clientDetailsService);
authorizationEndpoint.setAuthorizationCodeServices(asec.getEndpointsConfigurer().getAuthorizationCodeServices());
authorizationEndpoint.setOAuth2RequestFactory(asec.getEndpointsConfigurer().getOAuth2RequestFactory());
authorizationEndpoint.setOAuth2RequestValidator(asec.getEndpointsConfigurer().getOAuth2RequestValidator());
authorizationEndpoint.setUserApprovalHandler(asec.getEndpointsConfigurer().getUserApprovalHandler());
return authorizationEndpoint;
}
private String extractPath(FrameworkEndpointHandlerMapping mapping, String page) {
String path = mapping.getPath(page);
if (path.contains(":")) {
return path;
}
return "forward:" + path;
}
but this did not result in what I hoped to. the new bean does not replace the bean from framework code. this situation with overriding beans led to another question:
how replace framework beans
buttom line, this is not the way to override the framework endpoints. you can simply create a controller with mappings for these endpoints (e.g /oauth/authorize or /oauth/token). automatically these mappings will get precedence over framework endpoints. for more info refer to spring doc

Spring AOP NullPointerException after running successfully for an extended period of time

This is a problem which has stumped myself and two of my colleagues for a few days now.
We are receiving a NullPointerException after our spring-boot microservice has been running without a hitch anywhere from a few minutes to a few hours and has received a few hundred to few thousand requests. This issue started after a few beans were changed to being request-scoped due to a requirements change.
Classes (all objects are autowired/constructed at microservice boot):
// New class introduced to accommodate requirements change.
#Repository("databaseUserAccountRepo")
public class DatabaseAccountUserRepoImpl implements UserLdapRepo {
private final DatabaseAccountUserRepository databaseAccountUserRepository;
#Autowired
public DatabaseAccountUserRepoImpl(
#Qualifier("databaseAccountUserRepositoryPerRequest") final DatabaseAccountUserRepository databaseAccountUserRepository
) {
this.databaseAccountUserRepository = databaseAccountUserRepository;
}
// ...snip...
}
// ==============================================================================
// New class introduced to accommodate requirements change.
#Repository("databaseAccountUserRepository")
public interface DatabaseAccountUserRepository
extends org.springframework.data.repository.CrudRepository {
// ...snip...
}
// ==============================================================================
#Repository("ldapUserAccountRepo")
public class UserLdapRepoImpl implements UserLdapRepo {
// ...snip...
}
// ==============================================================================
#Component
public class LdapUtils {
private final UserLdapRepo userLdapRepo;
#Autowired
public LdapUtils(
#Qualifier("userLdapRepoPerRequest") final UserLdapRepo userLdapRepo
) {
this.userLdapRepo = userLdapRepo;
}
// ...snip...
public Object myMethod(/* whatever */) {
// ...snip...
return userLdapRepo.someMethod(/* whatever */);
}
}
// ==============================================================================
// I have no idea why the original developer decided to do it this way.
// It's worked fine up until now so I see no reason to change it unless
// I really need to.
public class AuthenticationContext {
private static final ThreadLocal<String> organizationNameThreadLocal = new ThreadLocal<>();
// ...snip...
public static void setOrganizationName(String organizationName) {
organizationNameThreadLocal.set(organizationName);
}
public static String getOrganizationName() {
return organizationNameThreadLocal.get();
}
public static void clear() {
organizationNameThreadLocal.remove();
}
// ...snip...
}
// ==============================================================================
public class AuthenticationContextInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
AuthenticationContext.setOrganizationName(request.getHeader("customer-id"));
return true;
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
AuthenticationContext.clear();
}
}
Code to request-scope:
#Configuration
// We have some aspects in our codebase, so this might be relevant.
#EnableAspectJAutoProxy(proxyTargetClass = true)
public class ServiceConfiguration {
// ...snip...
#Bean
#Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public UserLdapRepo userLdapRepoPerRequest(
final Map<String, String> customerIdToUserLdapRepoBeanName
) {
final String customerId = AuthenticationContext.getOrganizationName();
final String beanName = customerIdToUserLdapRepoBeanName.containsKey(customerId)
? customerIdToUserLdapRepoBeanName.get(customerId)
: customerIdToUserLdapRepoBeanName.get(null); // default
return (UserLdapRepo) applicationContext.getBean(beanName);
}
#Bean
public Map<String, String> customerIdToUserLdapRepoBeanName(
#Value("${customers.user-accounts.datastore.use-database}") final String[] customersUsingDatabaseForAccounts
) {
final Map<String, String> customerIdToUserLdapRepoBeanName = new HashMap<>();
customerIdToUserLdapRepoBeanName.put(null, "ldapUserAccountRepo"); // default option
if (customersUsingDatabaseForAccounts != null && customersUsingDatabaseForAccounts.length > 0) {
Arrays.stream(customersUsingDatabaseForAccounts)
.forEach(customerId ->
customerIdToUserLdapRepoBeanName.put(customerId, "databaseUserAccountRepo")
);
}
return customerIdToUserLdapRepoBeanName;
}
// Given a customer ID (taken from request header), returns the
// DatabaseAccountUserRepository instance for that particular customer.
// The DatabaseAccountUserRepositoryProvider is NOT request-scoped.
// The DatabaseAccountUserRepositoryProvider is basically just a utility
// wrapper around a map of String -> DatabaseAccountUserRepository.
#Bean
#Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public DatabaseAccountUserRepository databaseAccountUserRepositoryPerRequest(
final DatabaseAccountUserRepositoryProvider databaseAccountUserRepositoryProvider
) {
final String customerId = AuthenticationContext.getOrganizationName();
return databaseAccountUserRepositoryProvider.getRepositoryFor(customerId);
}
// ...snip...
}
The stack trace:
java.lang.NullPointerException: null
at org.springframework.aop.framework.adapter.DefaultAdvisorAdapterRegistry.getInterceptors(DefaultAdvisorAdapterRegistry.java:81)
at org.springframework.aop.framework.DefaultAdvisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(DefaultAdvisorChainFactory.java:89)
at org.springframework.aop.framework.AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice(AdvisedSupport.java:489)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:659)
at com.mycompany.project.persistence.useraccount.ldap.UserLdapRepoImpl$$EnhancerBySpringCGLIB$$b6378f51.someMethod(<generated>)
at sun.reflect.GeneratedMethodAccessor304.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:133)
at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:121)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
at com.sun.proxy.$Proxy209.findByFederatedInfo(Unknown Source)
at com.mycompany.project.util.LdapUtils.myMethod(LdapUtils.java:141)
The method in which the NPE is thrown is this guy:
//////////////////////////////////////////
// This is a method in Spring framework //
//////////////////////////////////////////
#Override
public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
List<MethodInterceptor> interceptors = new ArrayList<MethodInterceptor>(3);
Advice advice = advisor.getAdvice(); // <<<<<<<<<< line 81
if (advice instanceof MethodInterceptor) {
interceptors.add((MethodInterceptor) advice);
}
for (AdvisorAdapter adapter : this.adapters) {
if (adapter.supportsAdvice(advice)) {
interceptors.add(adapter.getInterceptor(advisor));
}
}
if (interceptors.isEmpty()) {
throw new UnknownAdviceTypeException(advisor.getAdvice());
}
return interceptors.toArray(new MethodInterceptor[interceptors.size()]);
}
Most relevant dependencies:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!-- this results in spring-aop:4.3.14.RELEASE -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
The request header customer-id is set by our proxy, so it must be available on the request (we added logging to verify that this statement is true; it is).
We do not know the exact traffic pattern which can cause the NPE to start being triggered. Once triggered, all subsequent requests also result in an NPE.
We have several other request-scoped beans in this project; they are also selected using the customer-id. Several of said objects have existed in this project for months prior to this change. They do not exhibit this problem.
We believe that the userLdapRepoPerRequest() and databaseAccountUserRepositoryPerRequest() methods are working correctly - receiving the correct customer-id, is returning the correct object, etc...at least when the methods are hit. This was determined by adding logging to the body of those methods - a log message immediately upon entering the method which records the parameter, one log message verifying value of the customer-id, and one log message immediately before returning which records the value which is to be returned. Note: Our logging setup has a correlation ID present on each message, so we can keep track of what messages corresponds the same request.
It's almost as if Spring is losing track of a few of its proxied beans.
Anyone have any ideas on what's happening or anything you would like us to try? Any leads are much appreciated.

Feign client and Spring retry

I have a restful service calling an external service using Spring Cloud Feign client
#FeignClient(name = "external-service", configuration = FeignClientConfig.class)
public interface ServiceClient {
#RequestMapping(value = "/test/payments", method = RequestMethod.POST)
public void addPayment(#Valid #RequestBody AddPaymentRequest addPaymentRequest);
#RequestMapping(value = "/test/payments/{paymentId}", method = RequestMethod.PUT)
public ChangePaymentStatusResponse updatePaymentStatus(#PathVariable("paymentId") String paymentId,
#Valid #RequestBody PaymentStatusUpdateRequest paymentStatusUpdateRequest);
}
I noticed the following failure 3-4 times in the last 3 months in my log file:
json.ERROR_RESPONSE_BODY:Connection refused executing POST
http://external-service/external/payments json.message:Send Payment
Add Payment Failure For other reason: {ERROR_RESPONSE_BODY=Connection
refused executing POST http://external-service/external/payments,
EVENT=ADD_PAYMENT_FAILURE, TRANSACTION_ID=XXXXXXX} {}
json.EVENT:ADD_PAYMENT_FAILURE
json.stack_trace:feign.RetryableException: Connection refused
executing POST http://external-service/external/payments at
feign.FeignException.errorExecuting(FeignException.java:67) at
feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:104)
at
feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:76)
at
feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103)
Is it possible to add Spring Retry on a Feign client.
What I wanted to annotate the addPayment operation with
#Retryable(value = {feign.RetryableException.class }, maxAttempts = 3, backoff = #Backoff(delay = 2000, multiplier=2))
But this is not possible, what other options do I have?
You can add a Retryer in the FeignClientConfig
#Configuration
public class FeignClientConfig {
#Bean
public Retryer retryer() {
return new Custom();
}
}
class Custom implements Retryer {
private final int maxAttempts;
private final long backoff;
int attempt;
public Custom() {
this(2000, 3);
}
public Custom(long backoff, int maxAttempts) {
this.backoff = backoff;
this.maxAttempts = maxAttempts;
this.attempt = 1;
}
public void continueOrPropagate(RetryableException e) {
if (attempt++ >= maxAttempts) {
throw e;
}
try {
Thread.sleep(backoff);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
#Override
public Retryer clone() {
return new Custom(backoff, maxAttempts);
}
}
Updated with sample Retryer example config based on the Retryer.Default.
If you are using ribbon you can set properties, you can use below properties for retry:
myapp.ribbon.MaxAutoRetries=5
myapp.ribbon.MaxAutoRetriesNextServer=5
myapp.ribbon.OkToRetryOnAllOperations=true
Note: "myapp" is your service id.
Checkout this Github implementation for working example
Just new a contructor Default
#Configuration
public class FeignClientConfig {
#Bean
public Retryer retryer() {
return new Retryer.Default(100, 2000, 3);
}
}
Adding this if it can help someone. I was getting connection reset using feign, as some unknown process was running on that port.
Try changing the port. Refer this to find the process running on a port
I prepared a blog post about using Spring Retry with Feign Client methods. You may consider checking the Post. All steps have been explained in the post.
This is my config. Test OK in spring boot 2.2.0.RELEASE
spring cloud Hoxton.M3.
feign.hystrix.enabled=true
MY-SPRING-API.ribbon.MaxAutoRetries=2
MY-SPRING-API.ribbon.MaxAutoRetriesNextServer=2
MY-SPRING-API.ribbon.OkToRetryOnAllOperations=true
MY-SPRING-API.ribbon.retryableStatusCodes=404,500
feign.client.config.PythonPatentClient.connectTimeout=500
feign.client.config.PythonPatentClient.readTimeout=500
hystrix.command.PythonPatentClient#timeTest(String).execution.isolation.thread.timeoutInMilliseconds=5000
java code is :
#FeignClient(name = "MY-SPRING-API",configuration = {PythonPatentConfig.class},fallbackFactory = FallBack.class)
public interface PythonPatentClient
#RequestLine("GET /test?q={q}")
void timeTest(#Param("appNo") String q);
Controller is :
#RequestMapping(value = "/test",method = {RequestMethod.POST,RequestMethod.GET})
public Object test() throws InterruptedException {
log.info("========important print enter test========");
TimeUnit.SECONDS.sleep(10L);
pom.xml additon add:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
optional:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
#EnableRetry
#SpringBootApplication
public class ApiApplication
this is document :
https://docs.spring.io/spring-cloud-netflix/docs/2.2.10.RELEASE/reference/html/#retrying-failed-requests
https://github.com/spring-projects/spring-retry
https://github.com/spring-cloud/spring-cloud-netflix/
I resolved that by creating a wrapper on top of ServiceClient
#Configuration
public class ServiceClient {
#Autowired
ServiceFeignClient serviceFeignClient;
#Retryable(value = { ClientReprocessException.class }, maxAttemptsExpression = "#{${retryMaxAttempts}}", backoff = #Backoff(delayExpression = "#{${retryDelayTime}}"))
public void addPayment( AddPaymentRequest addPaymentRequest){
return serviceFeignClient.addPayment(addPaymentRequest);
}
}

Resources