Feign client custom oauth2 response

2.9k views Asked by At

I have two micro-services.

  • auth-service (which uses spring-security-oauth2)
  • property-service

property-microservice implements a feign client in order get user information from the auth-service via the link

/auth/users/get/{USER_ID}

property-microservice uses oauth2 authentication in order to access to the auth-service end-point above (which works fine, i can get the response)

But auth-service does not return default response data and for this reason feign client interceptor cannot parse auth token from the response.

To be clear, this is the default response from auth-service which spring provides:

{
    "access_token": "6e7519de-f211-47ca-afc0-b65ede51bdfc",
    "token_type": "bearer",
    "refresh_token": "6146216f-bedd-42bf-b4e5-95131b0c6380",
    "expires_in": 7199,
    "scope": "ui"
}

But i do return response like this:

{
    "code": 0,
    "message": {
        "type": "message",
        "status": 200,
        "result": 200,
        "message": "Token aquired successfully."
    },
    "data": {
        "access_token": "6e7519de-f211-47ca-afc0-b65ede51bdfc",
        "token_type": "bearer",
        "refresh_token": "6146216f-bedd-42bf-b4e5-95131b0c6380",
        "expires_in": 7199,
        "scope": "ui"
    }
}

Thus, fiegn client looks for the standard response data and does't able to find it because of the modifications i made. If only i can override ResponseExtractor inside OAuth2AccessTokenSupport class i can parse the response correctly. How can i manage parsing custom oauth2 responses from feign clients (or is there any other solution)?

Application.java (property-service)

// For jsr310 java 8 java.time.* support for JPA
@EntityScan(basePackageClasses = {Application.class, Jsr310JpaConverters.class})
@SpringBootApplication
@EnableResourceServer
@EnableOAuth2Client
@EnableFeignClients
@EnableHystrix
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableConfigurationProperties
@Configuration
@EnableAutoConfiguration
public class Application extends ResourceServerConfigurerAdapter {
    @Autowired
    private ResourceServerProperties sso;

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);

        HystrixDummy.start();
    }

    @Bean
    @ConfigurationProperties(prefix = "security.oauth2.client")
    public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
        return new ClientCredentialsResourceDetails();
    }

    @Bean
    public RequestInterceptor oauth2FeignRequestInterceptor() {
        return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails());
    }

    @Bean
    public OAuth2RestTemplate clientCredentialsRestTemplate() {
        return new OAuth2RestTemplate(clientCredentialsResourceDetails());
    }

    @Bean
    public ResourceServerTokenServices tokenServices() {
        return new CustomUserInfoTokenServices(this.sso.getUserInfoUri(), this.sso.getClientId());
    }
}

AuthServiceClient (property-service)

@FeignClient(name = "auth-service", fallbackFactory = AuthServiceClient.AuthServiceClientFallback.class)
public interface AuthServiceClient {
    @RequestMapping(path = "/auth/users/get/{userId}", method = RequestMethod.GET)
    RestResponse get(@PathVariable(value = "userId") Long userId);

    @Component
    class AuthServiceClientFallback implements FallbackFactory<AuthServiceClient> {
        @Override
        public AuthServiceClient create(Throwable cause) {
            return userId -> new RestResponse(null, AppConstant.CODE_FAILURE, null);
        }
    }
}

Application.java (auth-service)

@SpringBootApplication
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

UserController.java (auth-service)

@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;

    @PreAuthorize("#oauth2.hasScope('server')")
    @RequestMapping(value = "/get/{userId}", method = RequestMethod.GET)
    public ResponseEntity<RestResponse> get(@Valid @PathVariable Long userId) throws UserNotFoundException {
        User user = this.userService.findOne(userId);

        RestResponse response = new RestResponse();

        RestMessage message = new RestMessage();
        message.setMessage(AppConstant.MESSAGE_USER_FETCHED_SUCCESS);
        message.setResult(AppConstant.CODE_USER_FETCHED);
        message.setStatus(HttpStatus.OK.value());

        response.setCode(AppConstant.CODE_SUCCESS);
        response.setMessage(message);
        response.setData(user);

        return new ResponseEntity<>(response, HttpStatus.OK);
    }
}
1

There are 1 answers

0
ngc4151 On BEST ANSWER

I just ended up writing custom FeignClientRequestInterceptor and FeignClientAccessTokenProvider like this:

FeignClientAccessTokenProvider.java

public class FeignClientAccessTokenProvider extends ClientCredentialsAccessTokenProvider {
    private ObjectMapper mapper = new ObjectMapper();

    @Override
    protected OAuth2AccessToken retrieveToken(AccessTokenRequest request, OAuth2ProtectedResourceDetails resource, MultiValueMap<String, String> form, HttpHeaders headers) throws OAuth2AccessDeniedException {
        OAuth2AccessToken token = super.retrieveToken(request, resource, form, headers);

        if (token != null && token.getValue() == null && token.getAdditionalInformation() != null) {
            if (token.getAdditionalInformation().containsKey("data")) {
                token = this.mapper.convertValue(token.getAdditionalInformation().get("data"), OAuth2AccessToken.class);
            }
        }

        return token;
    }
}

FeignClientRequestInterceptor .java

public class FeignClientRequestInterceptor implements RequestInterceptor {

    public static final String BEARER = "Bearer";

    public static final String AUTHORIZATION = "Authorization";

    private final OAuth2ClientContext oAuth2ClientContext;

    private final OAuth2ProtectedResourceDetails resource;

    private final String tokenType;

    private final String header;

    private AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(Arrays
            .<AccessTokenProvider>asList(new AuthorizationCodeAccessTokenProvider(),
                    new ImplicitAccessTokenProvider(),
                    new ResourceOwnerPasswordAccessTokenProvider(),
                    new FeignClientAccessTokenProvider()));

    /**
     * Default constructor which uses the provided OAuth2ClientContext and Bearer tokens
     * within Authorization header
     *
     * @param oAuth2ClientContext provided context
     * @param resource            type of resource to be accessed
     */
    public FeignClientRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
                                         OAuth2ProtectedResourceDetails resource) {
        this(oAuth2ClientContext, resource, BEARER, AUTHORIZATION);
    }

    /**
     * Fully customizable constructor for changing token type and header name, in cases of
     * Bearer and Authorization is not the default such as "bearer", "authorization"
     *
     * @param oAuth2ClientContext current oAuth2 Context
     * @param resource            type of resource to be accessed
     * @param tokenType           type of token e.g. "token", "Bearer"
     * @param header              name of the header e.g. "Authorization", "authorization"
     */
    public FeignClientRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
                                         OAuth2ProtectedResourceDetails resource, String tokenType, String header) {
        this.oAuth2ClientContext = oAuth2ClientContext;
        this.resource = resource;
        this.tokenType = tokenType;
        this.header = header;
    }

    /**
     * Create a template with the header of provided name and extracted extract
     *
     * @see RequestInterceptor#apply(RequestTemplate)
     */
    @Override
    public void apply(RequestTemplate template) {
        template.header(this.header, extract(this.tokenType));
    }

    /**
     * Extracts the token extract id the access token exists or returning an empty extract
     * if there is no one on the context it may occasionally causes Unauthorized response
     * since the token extract is empty
     *
     * @param tokenType type name of token
     * @return token value from context if it exists otherwise empty String
     */
    protected String extract(String tokenType) {
        OAuth2AccessToken accessToken = getToken();
        return String.format("%s %s", tokenType, accessToken.getValue());
    }

    /**
     * Extract the access token within the request or try to acquire a new one by
     * delegating it to {@link #acquireAccessToken()}
     *
     * @return valid token
     */
    public OAuth2AccessToken getToken() {

        OAuth2AccessToken accessToken = this.oAuth2ClientContext.getAccessToken();
        if (accessToken == null || accessToken.isExpired()) {
            try {
                accessToken = acquireAccessToken();
            } catch (UserRedirectRequiredException e) {
                this.oAuth2ClientContext.setAccessToken(null);
                String stateKey = e.getStateKey();
                if (stateKey != null) {
                    Object stateToPreserve = e.getStateToPreserve();
                    if (stateToPreserve == null) {
                        stateToPreserve = "NONE";
                    }
                    this.oAuth2ClientContext.setPreservedState(stateKey, stateToPreserve);
                }
                throw e;
            }
        }
        return accessToken;
    }

    /**
     * Try to acquire the token using a access token provider
     *
     * @return valid access token
     * @throws UserRedirectRequiredException in case the user needs to be redirected to an
     *                                       approval page or login page
     */
    protected OAuth2AccessToken acquireAccessToken()
            throws UserRedirectRequiredException {
        AccessTokenRequest tokenRequest = this.oAuth2ClientContext.getAccessTokenRequest();
        if (tokenRequest == null) {
            throw new AccessTokenRequiredException(
                    "Cannot find valid context on request for resource '"
                            + this.resource.getId() + "'.",
                    this.resource);
        }
        String stateKey = tokenRequest.getStateKey();
        if (stateKey != null) {
            tokenRequest.setPreservedState(
                    this.oAuth2ClientContext.removePreservedState(stateKey));
        }
        OAuth2AccessToken existingToken = this.oAuth2ClientContext.getAccessToken();
        if (existingToken != null) {
            this.oAuth2ClientContext.setAccessToken(existingToken);
        }
        OAuth2AccessToken obtainableAccessToken;
        obtainableAccessToken = this.accessTokenProvider.obtainAccessToken(this.resource,
                tokenRequest);
        if (obtainableAccessToken == null || obtainableAccessToken.getValue() == null) {
            throw new IllegalStateException(
                    " Access token provider returned a null token, which is illegal according to the contract.");
        }
        this.oAuth2ClientContext.setAccessToken(obtainableAccessToken);
        return obtainableAccessToken;
    }
}

Hope this helps to anyone facing this problem.