Reuse existing token rather than requesting it on every request in spring boot + Retrofit app - spring

I have a spring boot application that uses Retrofit to make requests to a secured server.
My endpoints:
public interface ServiceAPI {
#GET("/v1/isrcResource/{isrc}/summary")
Call<ResourceSummary> getResourceSummaryByIsrc(#Path("isrc") String isrc);
}
public interface TokenServiceAPI {
#FormUrlEncoded
#POST("/bbcb6b2f-8c7c-4e24-86e4-6c36fed00b78/oauth2/v2.0/token")
Call<Token> obtainToken(#Field("client_id") String clientId,
#Field("scope") String scope,
#Field("client_secret") String clientSecret,
#Field("grant_type") String grantType);
}
Configuration class:
#Bean
Retrofit tokenAPIFactory(#Value("${some.token.url}") String tokenUrl) {
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(tokenUrl)
.addConverterFactory(JacksonConverterFactory.create());
return builder.build();
}
#Bean
Retrofit serviceAPIFactory(#Value("${some.service.url}") String serviceUrl, TokenServiceAPI tokenAPI) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new ServiceInterceptor(clientId, scope, clientSecret, grantType, apiKey, tokenAPI))
.build();
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(repertoireUrl)
.client(okHttpClient)
.addConverterFactory(JacksonConverterFactory.create());
return builder.build();
}
Interceptor to add the Authorization header to every request
public class ServiceInterceptor implements Interceptor {
public ServiceInterceptor(String clientId,
String scope,
String clientSecret,
String grantType,
String apiKey,
TokenServiceAPI tokenAPI) {
this.clientId = clientId;
this.scope = scope;
this.clientSecret = clientSecret;
this.grantType = grantType;
this.apiKey = apiKey;
this.tokenAPI = tokenAPI;
}
#Override
public Response intercept(Chain chain) throws IOException {
Request newRequest = chain.request().newBuilder()
.addHeader(AUTHORIZATION_HEADER, getToken())
.addHeader(API_KEY_HEADER, this.apiKey)
.build();
return chain.proceed(newRequest);
}
private String getToken() throws IOException {
retrofit2.Response<Token> tokenResponse = repertoireTokenAPI.obtainToken(clientId, scope, clientSecret, grantType).execute();
String accessToken = "Bearer " + tokenAPI.body().getAccessToken();
return accessToken;
}
}
This is working as expected, the problem is that the token is being requested for every request rather than using the existing valid one. How can one store the token somewhere and re-use it? I was wondering if Retrofit had a built-in solution.

a possible option with caching:
add caffeiene
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
add #Cacheable("your-token-cache-name") on the method returning the token, looks like getToken above
add max cache size and expiration configuration in application.yml
e.g. 500 entries and 10 minutes for configuration below
spring.cache.cache-names=your-token-cache-name
spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s
example from: https://www.javadevjournal.com/spring-boot/spring-boot-with-caffeine-cache/

Related

Not able to call SOAP API in WebServiceGatewaySupport by Spring WebServiceTemplate - Need help to fix this issue

I am trying to call SOAP API in Java Spring Boot using WebServiceGatewaySupport by Spring WebServiceTemplate
Config java class
public WebServiceTemplate createWebServiceTemplate(Jaxb2Marshaller marshaller, ClientInterceptor clientInterceptor) {
WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
//SOAP URL
webServiceTemplate.setDefaultUri("http://host/Services.asmx");
//Auth ---It seems issue is here only????? need to check
webServiceTemplate.setMessageSender(new Authentication());
webServiceTemplate.setMarshaller(marshaller);
webServiceTemplate.setUnmarshaller(marshaller);
webServiceTemplate.afterPropertiesSet();
webServiceTemplate.setCheckConnectionForFault(true);
webServiceTemplate.setInterceptors((ClientInterceptor[]) Arrays.asList(createLoggingInterceptor()).toArray());
return webServiceTemplate;
}
SOAP Client Call
public class TicketClient extends WebServiceGatewaySupport {
public String getTicket(Ticket req) {
System.out.println("test inside webservice support1");
response = (AcquireTicketResponse) getWebServiceTemplate().marshalSendAndReceive(req);
Authentication Class
public class Authentication extends HttpUrlConnectionMessageSender {
#Override protected void prepareConnection(HttpURLConnection connection) {
String userpassword = username+":"+password+":"+domain;
String encoded =
Base64.getEncoder().withoutPadding().encodeToString(userpassword.getBytes(StandardCharsets.UTF_8));
connection.setRequestProperty("Authorization", "Basic "+encoded); connection.setRequestProperty("Content-Type", "application/xml"); super.prepareConnection(connection);
}
Not using Authetication class and add the above into
ClientInterceptor
public class SoapLoggingInterceptor implements ClientInterceptor {
#Override
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {
String username="test";
String password="test";
String domain = "#test";
String userpassword = username+":"+password+domain;
String encoded = Base64.getEncoder().withoutPadding().encodeToString(userpassword.getBytes(StandardCharsets.UTF_8));
messageContext.setProperty("Authorization", "Basic "+encoded);
messageContext.setProperty("Content-type", "XML");
Case -1 --->When I passed (user, pwd, domain and content-type) through messagesender, content type is taking but throwed "BAD REQUEST ERROR 400"....When i comment contenttype property, then it throwed "INTERNAL SERVER ERROR 500".
Case-2...when I passed (user, pwd, domain and content-type) through ClientInterceptor , always it throwed "INTERNAL SERVER ERROR 500"......It seems Authentication properties for the service are not going to API call.............................Please suggest some options
Both the cases, Authentication is not passing to service, if i comment,Authentication code (userid/pwd/domain) in both cases also...no efforts in output
After setting the user ID/pwd
#Override
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {
String username="test";
String password="test";
String domain = "#test";
String userpassword = username+":"+password+domain;
byte[] userpassword = (username+":"+password).getBytes(StandardCharsets.UTF_8);
String encoded = Base64.getEncoder().encodeToString(userpassword);
ByteArrayTransportOutputStream os = new
ByteArrayTransportOutputStream();
try {
TransportContext context = TransportContextHolder.getTransportContext();
WebServiceConnection conn = context.getConnection();
((HeadersAwareSenderWebServiceConnection) conn).addRequestHeader("Authorization", "Basic " + encoded);
} catch (IOException e) {
throw new WebServiceIOException(e.getMessage(), e);
}
First of all don't set the content type Spring WebServices will do that for you, messing around with that will only make things worse.
You should get the WebServiceConnection and cast that to a HeadersAwareSenderWebServiceConnection to add a header.
public class BasicAuthenticationInterceptor implements ClientInterceptor {
#Override
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {
String username="test#test";
String password="test";
byte[] userpassword = (username+":"+password).getBytes(UTF_8);
String encoded = Base64.getEncoder().encodeToString(userpassword);
WebServiceConnection conn = TransportContext.getConnection();
((HeadersAwareSenderWebServiceConnection) conn).addHeader("Authorization", "Basic " + encoded);
}
}
You also need to configure it. Assuming it is a bean don't call afterPropertiesSet (and ofcourse you are now using the ClientInterceptor remove the new Authentication() for your customized message sender.
The List<ClientInterceptor> will automatically create a list with all the interceptors so you can easily inject them.
#Bean
public WebServiceTemplate createWebServiceTemplate(Jaxb2Marshaller marshaller, List<ClientInterceptor> clientInterceptors) {
WebServiceTemplate webServiceTemplate = new WebServiceTemplate(marshaller);
//SOAP URL
webServiceTemplate.setDefaultUri("http://host/Services.asmx");
webServiceTemplate.setCheckConnectionForFault(true);
webServiceTemplate.setInterceptors(clientInterceptors);
return webServiceTemplate;
}
If this doesn't work there is something else you are doing wrong and you will need to get in touch with the server developers and get more information on the error.
Update:
Apparently you also need to provide a SOAP Action in your request, which you currently don't. For this you can specify the SoapActionCallback in the marshalSendAndReceive method. Which action to specify you can find in the WSDL you are using.
SoapActionCallback soapAction = new SoapActionCallback("SoapActionToUse");
response = (AcquireTicketResponse) getWebServiceTemplate().marshalSendAndReceive(req, soapAction);

How to implement multi-tenancy in new Spring Authorization server

Link for Authorization server: https://github.com/spring-projects/spring-authorization-server
This project pretty much has everything in terms of OAuth and Identity provider.
My question is, How to achieve multi-tenancy at the Identity provider level.
I know there are multiple ways to achieve multi-tenancy in general.
The scenario I am interested in is this:
An organization provides services to multiple tenants.
Each tenant is associated with a separate database (Data isolation including user data)
When a user visits dedicated Front-end app(per tenant) and negotiate access tokens from Identity provider
Identity provider then identifies tenant (Based on header/ Domain name) and generates access token with tenant_id
This access token then is passed on to down-stream services, which intern can extract tenant_id and decide the data source
I have a general idea about all the above steps, but I am not sure about point 4.
I am not sure How to configure different data sources for different tenants on the Identity Provider? How to add tenant_id in Token?
Link to the issue: https://github.com/spring-projects/spring-authorization-server/issues/663#issue-1182431313
This is not related to Spring auth Server, but related to approaches that we can think for point # 4
I remember the last time we implemented a similar approach, where we had below options
To have unique email addresses for the users thereby using the global database to authenticate the users and post authentication, set up the tenant context.
In case of users operating in more than 1 tenant, post authentication, we can show the list of tenant's that the user has access to, which enables setting the tenant context and then proceeding with the application usage.
More details can be read from here
This is really a good question and I really want to know how to do it in new Authorization Server in a proper way. In Spring Resource Server there is a section about Multitenancy. I did it successfully.
As far as new Spring Authorization Server multitenancy concerns. I have also done it for the password and the Client Credentials grant type.
But please note that although it is working but how perfect is this. I don't know because I just did it for learning purpose. It's just a sample. I will also post it on my github when I would do it for the authorization code grant type.
I am assuming that the master and tenant database configuration has been done. I can not provide the whole code here because it's lot of code. I will just provide the relevant snippets. But here is just the sample
#Configuration
#Import({MasterDatabaseConfiguration.class, TenantDatabaseConfiguration.class})
public class DatabaseConfiguration {
}
I used the separate database. What I did I used something like the following in the AuthorizationServerConfiguration.
#Import({OAuth2RegisteredClientConfiguration.class})
public class AuthorizationServerConfiguration {
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>();
....
http.addFilterBefore(new TenantFilter(), OAuth2AuthorizationRequestRedirectFilter.class);
SecurityFilterChain securityFilterChain = http.formLogin(Customizer.withDefaults()).build();
addCustomOAuth2ResourceOwnerPasswordAuthenticationProvider(http);
return securityFilterChain;
}
}
Here is my TenantFilter code
public class TenantFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LogManager.getLogger();
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestUrl = request.getRequestURL().toString();
if (!requestUrl.endsWith("/oauth2/jwks")) {
String tenantDatabaseName = request.getParameter("tenantDatabaseName");
if(StringUtils.hasText(tenantDatabaseName)) {
LOGGER.info("tenantDatabaseName request parameter is found");
TenantDBContextHolder.setCurrentDb(tenantDatabaseName);
} else {
LOGGER.info("No tenantDatabaseName request parameter is found");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write("{'error': 'No tenant request parameter supplied'}");
response.getWriter().flush();
return;
}
}
filterChain.doFilter(request, response);
}
public static String getFullURL(HttpServletRequest request) {
StringBuilder requestURL = new StringBuilder(request.getRequestURL().toString());
String queryString = request.getQueryString();
if (queryString == null) {
return requestURL.toString();
} else {
return requestURL.append('?').append(queryString).toString();
}
}
}
Here is the TenantDBContextHolder class
public class TenantDBContextHolder {
private static final ThreadLocal<String> TENANT_DB_CONTEXT_HOLDER = new ThreadLocal<>();
public static void setCurrentDb(String dbType) {
TENANT_DB_CONTEXT_HOLDER.set(dbType);
}
public static String getCurrentDb() {
return TENANT_DB_CONTEXT_HOLDER.get();
}
public static void clear() {
TENANT_DB_CONTEXT_HOLDER.remove();
}
}
Now as there is already configuration for master and tenant database. In these configurations we also check for the TenantDBContextHolder
class that it contains the value or not. Because when request comes for token then we check the request and set it in TenantDBContextHolder. So base on this thread local variable right database is connected and the token issue to the right database. Then in the token customizer. You can use something like the following
public class UsernamePasswordAuthenticationTokenJwtCustomizerHandler extends AbstractJwtCustomizerHandler {
....
#Override
protected void customizeJwt(JwtEncodingContext jwtEncodingContext) {
....
String tenantDatabaseName = TenantDBContextHolder.getCurrentDb();
if (StringUtils.hasText(tenantDatabaseName)) {
URL issuerURL = jwtClaimSetBuilder.build().getIssuer();
String issuer = issuerURL + "/" + tenantDatabaseName;
jwtClaimSetBuilder.claim(JwtClaimNames.ISS, issuer);
}
jwtClaimSetBuilder.claims(claims ->
userAttributes.entrySet().stream()
.forEach(entry -> claims.put(entry.getKey(), entry.getValue()))
);
}
}
Now I am assuming that the Resource Server is also configure for multitenancy. Here is the link Spring Security Resource Server Multitenancy. Basically You have to configure two beans for multitenancy like the following
public class OAuth2ResourceServerConfiguration {
....
#Bean
public JWTProcessor<SecurityContext> jwtProcessor(JWTClaimsSetAwareJWSKeySelector<SecurityContext> keySelector) {
ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWTClaimsSetAwareJWSKeySelector(keySelector);
return jwtProcessor;
}
#Bean
public JwtDecoder jwtDecoder(JWTProcessor<SecurityContext> jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
NimbusJwtDecoder decoder = new NimbusJwtDecoder(jwtProcessor);
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(JwtValidators.createDefault(), jwtValidator);
decoder.setJwtValidator(validator);
return decoder;
}
}
Now two classes for spring. From which you can get the tenant Identifier from your token.
#Component
public class TenantJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
private final TenantDataSourceRepository tenantDataSourceRepository;
private final Map<String, JwtIssuerValidator> validators = new ConcurrentHashMap<>();
....
#Override
public OAuth2TokenValidatorResult validate(Jwt token) {
String issuerURL = toTenant(token);
JwtIssuerValidator jwtIssuerValidator = validators.computeIfAbsent(issuerURL, this::fromTenant);
OAuth2TokenValidatorResult oauth2TokenValidatorResult = jwtIssuerValidator.validate(token);
String tenantDatabaseName = JwtService.getTenantDatabaseName(token);
TenantDBContextHolder.setCurrentDb(tenantDatabaseName);
return oauth2TokenValidatorResult;
}
private String toTenant(Jwt jwt) {
return jwt.getIssuer().toString();
}
private JwtIssuerValidator fromTenant(String tenant) {
String issuerURL = tenant;
String tenantDatabaseName = JwtService.getTenantDatabaseName(issuerURL);
TenantDataSource tenantDataSource = tenantDataSourceRepository.findByDatabaseName(tenantDatabaseName);
if (tenantDataSource == null) {
throw new IllegalArgumentException("unknown tenant");
}
JwtIssuerValidator jwtIssuerValidator = new JwtIssuerValidator(issuerURL);
return jwtIssuerValidator;
}
}
Similarly
#Component
public class TenantJWSKeySelector implements JWTClaimsSetAwareJWSKeySelector<SecurityContext> {
....
#Override
public List<? extends Key> selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext) throws KeySourceException {
String tenant = toTenantDatabaseName(jwtClaimsSet);
JWSKeySelector<SecurityContext> jwtKeySelector = selectors.computeIfAbsent(tenant, this::fromTenant);
List<? extends Key> jwsKeys = jwtKeySelector.selectJWSKeys(jwsHeader, securityContext);
return jwsKeys;
}
private String toTenantDatabaseName(JWTClaimsSet claimSet) {
String issuerURL = (String) claimSet.getClaim("iss");
String tenantDatabaseName = JwtService.getTenantDatabaseName(issuerURL);
return tenantDatabaseName;
}
private JWSKeySelector<SecurityContext> fromTenant(String tenant) {
TenantDataSource tenantDataSource = tenantDataSourceRepository.findByDatabaseName(tenant);
if (tenantDataSource == null) {
throw new IllegalArgumentException("unknown tenant");
}
JWSKeySelector<SecurityContext> jwtKeySelector = fromUri(jwkSetUri);
return jwtKeySelector;
}
private JWSKeySelector<SecurityContext> fromUri(String uri) {
try {
return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(uri));
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
}
}
Now what about authorization code grant type grant type flow. I get the tenant identifier in this case too. But when it redirects me to login page then I lost the tenant identifier because I think it creates a new request for the login page from the authorization code request. Anyways I am not sure about it because I have to look into the code of authorization code flow that what it is actually doing. So my tenant identifier is losing when it redirects me to login page.
But in case of password grant type and client credentials grant type there is no redirection so I get the tenant identifier in later stages and I can successfully use it to put into my token claims.
Then on the resource server I get the issuer url. Get the tenant identifier from the issuer url. Verify it. And it connects to the tenant database on resource server.
How I tested it. I used the spring client. You can customize the request for authorization code flow. Password and client credentials to include the custom parameters.
Thanks.
------------------ Solve the Authorization Code login problem for multitenancy -------------
I solved this issue too. Actually what I did in my security configuration. I used the following configuration
public class SecurityConfiguration {
.....
#Bean(name = "authenticationManager")
public AuthenticationManager authenticationManager(AuthenticationManagerBuilder builder) throws Exception {
AuthenticationManager authenticationManager = builder.getObject();
return authenticationManager;
}
#Bean
#DependsOn(value = {"authenticationManager"})
public TenantUsernamePasswordAuthenticationFilter tenantAuthenticationFilter(AuthenticationManagerBuilder builder) throws Exception {
TenantUsernamePasswordAuthenticationFilter filter = new TenantUsernamePasswordAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager(builder));
filter.setAuthenticationDetailsSource(new TenantWebAuthenticationDetailsSource());
//filter.setAuthenticationFailureHandler(failureHandler());
return filter;
}
#Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
FederatedIdentityConfigurer federatedIdentityConfigurer = new FederatedIdentityConfigurer().oauth2UserHandler(new UserRepositoryOAuth2UserHandler());
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
http.addFilterBefore(tenantAuthenticationFilter(authenticationManagerBuilder), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests(authorizeRequests -> authorizeRequests.requestMatchers(new AntPathRequestMatcher("/h2-console/**")).permitAll()
.antMatchers("/resources/**", "/static/**", "/webjars/**").permitAll()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
)
......
.apply(federatedIdentityConfigurer);
return http.build();
}
Actually the problem was in case of Authorization Code is that you first redirect to login page. After successfully login you see the consent page. But when you comes to consent page then you lost the tenant parameter.
The reason is the spring internal class OAuth2AuthorizationEndpointFilter intercepts the request for Authorization Code. It checks user is authenticated or not. If user is not authenticated then it shows the login page. After successfully login it checks if consent is required. And if required then it makes a redirect uri with just three parameters. Here is the spring internal code
private void sendAuthorizationConsent(HttpServletRequest request, HttpServletResponse response,
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication) throws IOException {
....
if (hasConsentUri()) {
String redirectUri = UriComponentsBuilder.fromUriString(resolveConsentUri(request))
.queryParam(OAuth2ParameterNames.SCOPE, String.join(" ", requestedScopes))
.queryParam(OAuth2ParameterNames.CLIENT_ID, clientId)
.queryParam(OAuth2ParameterNames.STATE, state)
.toUriString();
this.redirectStrategy.sendRedirect(request, response, redirectUri);
} else {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Displaying generated consent screen");
}
DefaultConsentPage.displayConsent(request, response, clientId, principal, requestedScopes, authorizedScopes, state);
}
}
See the above method is private and I found no way that I can customize it. May be there is but I didn't find it. Anyways now your consent controller is call. But there is no tenant Identifier. You can't get it. And after consent there is no way that it connects to tenant database base in identifier.
So the first step is to add tenant identifier to login page. And then after login you should have this tenant identifier so you can set it on your consent page. And after that when you submit your consent form then this parameter will be there.
Btw I did it some time ago and may be I miss something but this is what I did.
Now how you get your parameter at login page. I solved it using the following. First I created a constant as I have to access the name from multiple times
public interface Constant {
String TENANT_DATABASE_NAME = "tenantDatabaseName";
}
Create the following class
public class RedirectModel {
#NotBlank
private String tenantDatabaseName;
public void setTenantDatabaseName(String tenantDatabaseName) {
this.tenantDatabaseName = tenantDatabaseName;
}
public String getTenantDatabaseName() {
return tenantDatabaseName;
}
}
Then on my Login controller I get it using the following code
#Controller
public class LoginController {
#GetMapping("/login")
public String login(#Valid #ModelAttribute RedirectModel redirectModel, Model model, BindingResult result) {
if (!result.hasErrors()) {
String tenantDatabaseName = redirectModel.getTenantDatabaseName();
String currentDb = TenantDBContextHolder.getCurrentDb();
LOGGER.info("Current database is {}", currentDb);
LOGGER.info("Putting {} as tenant database name in model. So it can be set as a hidden form element ", tenantDatabaseName);
model.addAttribute(Constant.TENANT_DATABASE_NAME, tenantDatabaseName);
}
return "login";
}
}
So this is the first step that I have my tenant identifier in my login page that is send to me by request.
Now the configuration that I used in my Security configuration. You can see that I am using TenantUsernamePasswordAuthenticationFilter. Here is the filer
public class TenantUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final Logger LOGGER = LogManager.getLogger();
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
String tenantDatabaseName = obtainTenantDatabaseName(request);
LOGGER.info("tenantDatabaseName is {}", tenantDatabaseName);
LOGGER.info("Setting {} as tenant database name in thread local context.", tenantDatabaseName);
TenantDBContextHolder.setCurrentDb(tenantDatabaseName);
return super.attemptAuthentication(request, response);
}
private String obtainTenantDatabaseName(HttpServletRequest request) {
return request.getParameter(Constant.TENANT_DATABASE_NAME);
}
}
And in the configuration I am setting TenantWebAuthenticationDetailsSource on this filter which is here
public class TenantWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
#Override
public TenantWebAuthenicationDetails buildDetails(HttpServletRequest context) {
return new TenantWebAuthenicationDetails(context);
}
}
Here is the class
public class TenantWebAuthenicationDetails extends WebAuthenticationDetails {
private static final long serialVersionUID = 1L;
private String tenantDatabaseName;
public TenantWebAuthenicationDetails(HttpServletRequest request) {
super(request);
this.tenantDatabaseName = request.getParameter(Constant.TENANT_DATABASE_NAME);
}
public TenantWebAuthenicationDetails(String remoteAddress, String sessionId, String tenantDatabaseName) {
super(remoteAddress, sessionId);
this.tenantDatabaseName = tenantDatabaseName;
}
public String getTenantDatabaseName() {
return tenantDatabaseName;
}
}
Now after spring authenticates the user then I have the tenant name in details. Then in the consent controller I use
#Controller
public class AuthorizationConsentController {
....
#GetMapping(value = "/oauth2/consent")
public String consent(Authentication authentication, Principal principal, Model model,
#RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
#RequestParam(OAuth2ParameterNames.SCOPE) String scope,
#RequestParam(OAuth2ParameterNames.STATE) String state) {
......
String registeredClientName = registeredClient.getClientName();
Object webAuthenticationDetails = authentication.getDetails();
if (webAuthenticationDetails instanceof TenantWebAuthenicationDetails) {
TenantWebAuthenicationDetails tenantAuthenticationDetails = (TenantWebAuthenicationDetails)webAuthenticationDetails;
String tenantDatabaseName = tenantAuthenticationDetails.getTenantDatabaseName();
model.addAttribute(Constant.TENANT_DATABASE_NAME, tenantDatabaseName);
}
model.addAttribute("clientId", clientId);
.....
return "consent-customized";
}
}
Now I have my tenant identifier on my consent page. After submitting it it's in the request parameter.
There is another class that I used and it was
public class TenantLoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
public TenantLoginUrlAuthenticationEntryPoint(String loginFormUrl) {
super(loginFormUrl);
}
#Override
protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) {
String tenantDatabaseNameParamValue = request.getParameter(Constant.TENANT_DATABASE_NAME);
String redirect = super.determineUrlToUseForThisRequest(request, response, exception);
String url = UriComponentsBuilder.fromPath(redirect).queryParam(Constant.TENANT_DATABASE_NAME, tenantDatabaseNameParamValue).toUriString();
return url;
}
}
Anyways this is how I solved it. I don't have any such requirement in any of my project but I want to do it using this new server so I just solved it in this way.
Anyways there is lot of code. I tested it using the Spring oauth2 client and it was working. Hopefully I will create some project and upload it on my Github. Once I will run it again then I will put more explanation here of the flow. Specially for the last part that after submitting the consent how it set in the Thread Local variable.
After that everything is straight forward.
Hopefully it will help.
Thanks

Configuring AWS Signing in Reactive Elasticsearch Configuration

In one of our service I tried to configure AWS signing in Spring data Reactive Elasticsearch configuration.
Spring provides the configuring the webclient through webclientClientConfigurer
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("localhost:9200")
.usingSsl()
.withWebClientConfigurer(
webClient -> {
return webClient.mutate().filter(new AwsSigningInterceptor()).build();
})
. // ... other options to configure if required
.build();
through which we can configure to sign the requests but however AWS signing it requires url, queryparams, headers and request body(in case of POST,POST) to generate the signed headers.
Using this I created a simple exchange filter function to sign the request but in this function I was not able to access the request body and use it.
Below is the Filter function i was trying to use
#Component
public class AwsSigningInterceptor implements ExchangeFilterFunction
{
private final AwsHeaderSigner awsHeaderSigner;
public AwsSigningInterceptor(AwsHeaderSigner awsHeaderSigner)
{
this.awsHeaderSigner = awsHeaderSigner;
}
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next)
{
Map<String, List<String>> signingHeaders = awsHeaderSigner.createSigningHeaders(request, new byte[]{}, "es", "us-west-2"); // should pass request body bytes in place of new byte[]{}
ClientRequest.Builder requestBuilder = ClientRequest.from(request);
signingHeaders.forEach((key, value) -> requestBuilder.header(key, value.toArray(new String[0])));
return next.exchange(requestBuilder.build());
}
}
I also tried to access the request body inside ExchangeFilterFunction using below approach but once i get the request body using below approach.
ClientRequest.from(newRequest.build())
.body(
(outputMessage, context) -> {
ClientHttpRequestDecorator loggingOutputMessage =
new ClientHttpRequestDecorator(outputMessage) {
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
log.info("Inside write with method");
body =
DataBufferUtils.join(body)
.map(
content -> {
// Log request body using
// 'content.toString(StandardCharsets.UTF_8)'
String requestBody =
content.toString(StandardCharsets.UTF_8);
Map<String, Object> signedHeaders =
awsSigner.getSignedHeaders(
request.url().getPath(),
request.method().name(),
multimap,
requestHeadersMap,
Optional.of(
requestBody.getBytes(StandardCharsets.UTF_8)));
log.info("Signed Headers generated:{}", signedHeaders);
signedHeaders.forEach(
(key, value) -> {
newRequest.header(key, value.toString());
});
return content;
});
log.info("Before returning the body");
return super.writeWith(body);
}
#Override
public Mono<Void>
setComplete() { // This is for requests with no body (e.g. GET).
Map<String, Object> signedHeaders =
awsSigner.getSignedHeaders(
request.url().getPath(),
request.method().name(),
multimap,
requestHeadersMap,
Optional.of("".getBytes(StandardCharsets.UTF_8)));
log.info("Signed Headers generated:{}", signedHeaders);
signedHeaders.forEach(
(key, value) -> {
newRequest.header(key, value.toString());
});
return super.setComplete();
}
};
return originalBodyInserter.insert(loggingOutputMessage, context);
})
.build();
But with above approach I was not able to change the request headers as adding headers throws UnsupportedOperationException inside writewith method.
Has anyone used the spring data reactive elastic search and configured to sign with AWS signed headers?
Any help would be highly appreciated.

Service that checks if token hasnt expired

so I have a authentication bean which provides access tokens from client credentials.
public class AuthServiceBean {
#Value("${some.url}")
private String someUrl;
#Value("${some.clientId}")
private String someClientId;
#Value("${some.secret}")
private String someSecret;
#Value("${some.username}")
private String someUsername;
#Value("${some.password}")
private String somePassword;
public AuthInfo getPrevAuth() {
return prevAuth;
}
public void setPrevAuth(AuthInfo prevAuth) {
this.prevAuth = prevAuth;
}
private AuthInfo prevAuth;
public AuthInfo getAuthInfo() throws IOException {
if (this.prevAuth != null && this.prevAuth.isNotExpired()) {
return this.prevAuth;
}
return this.Authenticate();
}
private AuthInfo Authenticate() throws IOException {
final String url = this.someUrl + "/api/oauth/v1/token";
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
String clientIdSecret = this.someClientId +":"+ this.someSecret;
String authString = Base64.getEncoder().encodeToString(clientIdSecret.getBytes());
headers.add("Authorization", "Basic " + authString);
MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
map.add("username", this.someUsername);
map.add("password", this.somePassword);
map.add("grant_type", "password");
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers);
ResponseEntity<?> response = restTemplate.postForEntity(url, request, String.class);
String bodyString = response.getBody().toString();
ObjectMapper mapper = new ObjectMapper();
try {
AuthInfo authInfo = mapper.readValue(bodyString, AuthInfo.class);
this.prevAuth = authInfo;
return this.prevAuth;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
And now how do I need to create service which checks if that access token valid if it hasn't expired and how to use refresh token. When access token expires I could ask new token with refresh token? Would be good to get any examples.
First of all, As I see in your code, you are using password grant type, not client credentials, and because of this, you pass also user credentials (username and password) in addition to the client credentials, client id and client secret.
Anyway, the reason because all the examples you found to check expiration are using jwt tokens is because these tokens have this information coded in the token itself, so you can parse it using some kind of library like Nimbus Jose and get the "exp" claim and check directly if that date is before or after the actual date.
If the token is an opaque one (not jwt). You don't have any way to check the expiration without call the server who issued that token. Normally the server (an oauth2 server) provides and endpoint called introspect in which you pass a token and it responds indicating if this token is valid or is not, because it has expired or it is revoked etc..

Spring Boot add additional attribute to WebClient request in ServerOAuth2AuthorizedClientExchangeFilterFunction

I am trying to implement the client_credentials grant to get a token in my spring boot resource server.
I am using Auth0 as an Authorization server. They seem to require an extra parameter in the request body to be added called audience.
I have tried to do the request through postman and it works. I am now trying to reproduce it within Spring. Here is the working postman request
curl -X POST \
https://XXX.auth0.com/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials&audience=https%3A%2F%2Fxxxxx.auth0.com%2Fapi%2Fv2%2F&client_id=SOME_CLIENT_ID&client_secret=SOME_CLIENT_SECRET'
The problem I am facing is that i have no way to add the missing audience parameter to the token request.
I have a configuration defined in my application.yml
client:
provider:
auth0:
issuer-uri: https://XXXX.auth0.com//
registration:
auth0-client:
provider: auth0
client-id: Client
client-secret: Secret
authorization_grant_type: client_credentials
auth0:
client-id: Client
client-secret: Secret
I have the web client filter configured like this.
#Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
ServerOAuth2AuthorizedClientRepository authorizedClients) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrations, authorizedClients);
oauth2.setDefaultClientRegistrationId("auth0");
return WebClient.builder()
.filter(oauth2)
.build();
}
I am injecting the instance and trying to do a request to get the user by email
return this.webClient.get()
.uri(this.usersUrl + "/api/v2/users-by-email?email={email}", email)
.attributes(auth0ClientCredentials())
.retrieve()
.bodyToMono(User.class);
The way i understand it, the filter intercepts this userByEmail request and before it executes it it tries to execute the /oauth/token request to get JWT Bearer token which it can append to the first one and execute it.
Is there a way to add a parameter to the filter? It has been extremely difficult to step through it and figure out where exactly the parameters are being appended since its reactive and am quite new at this. Even some pointers to where to look would be helpful.
I was having the same problem where access token response and request for it wasn't following oAuth2 standards. Here's my code (it's in kotlin but should be understandable also for java devs) for spring boot version 2.3.6.RELEASE.
Gradle dependencies:
implementation(enforcedPlatform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}"))
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
After adding them you have to firstly create your custom token request/response client which will implement ReactiveOAuth2AccessTokenResponseClient interface:
class CustomTokenResponseClient : ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
private val webClient = WebClient.builder().build()
override fun getTokenResponse(
authorizationGrantRequest: OAuth2ClientCredentialsGrantRequest
): Mono<OAuth2AccessTokenResponse> =
webClient.post()
.uri(authorizationGrantRequest.clientRegistration.providerDetails.tokenUri)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.bodyValue(CustomTokenRequest(
clientId = authorizationGrantRequest.clientRegistration.clientId,
clientSecret = authorizationGrantRequest.clientRegistration.clientSecret
))
.exchange()
.flatMap { it.bodyToMono<NotStandardTokenResponse>() }
.map { it.toOAuth2AccessTokenResponse() }
private fun NotStandardTokenResponse.toOAuth2AccessTokenResponse() = OAuth2AccessTokenResponse
.withToken(this.accessToken)
.refreshToken(this.refreshToken)
.expiresIn(convertExpirationDateToDuration(this.data.expires).toSeconds())
.tokenType(OAuth2AccessToken.TokenType.BEARER)
.build()
}
As you can see above, in this class you can adjust token request/response handling to your specific needs.
Note: authorizationGrantRequest param inside getTokenResponse method. Spring is passing here data from you application properties, so follow the standards when defining them, e.g. they may look like this:
spring:
security:
oauth2:
client:
registration:
name-for-oauth-integration:
authorization-grant-type: client_credentials
client-id: id
client-secret: secret
provider:
name-for-oauth-integration:
token-uri: https://oauth.com/token
The last step is to use your CustomTokenResponseClient inside oAuth2 configuration, it may look like this:
#Configuration
class CustomOAuth2Configuration {
#Bean
fun customOAuth2WebWebClient(clientRegistrations: ReactiveClientRegistrationRepository): WebClient {
val clientRegistryRepo = InMemoryReactiveClientRegistrationRepository(
clientRegistrations.findByRegistrationId("name-for-oauth-integration").block()
)
val clientService = InMemoryReactiveOAuth2AuthorizedClientService(clientRegistryRepo)
val authorizedClientManager =
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistryRepo, clientService)
val authorizedClientProvider = ClientCredentialsReactiveOAuth2AuthorizedClientProvider()
authorizedClientProvider.setAccessTokenResponseClient(CustomTokenResponseClient())
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
val oauthFilter = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
oauthFilter.setDefaultClientRegistrationId("name-for-oauth-integration")
return WebClient.builder()
.filter(oauthFilter)
.build()
}
}
Right now, this is possible, but not elegant.
Note that you can provide a custom ReactiveOAuth2AccessTokenResponseClient to ServerOAuth2AuthorizedClientExchangeFilterFunction.
You can create your own implementation of this - and thereby add any other parameters you need - by copying the contents of WebClientReactiveClientCredentialsTokenResponseClient.
That said, it would be better if there were a setter to make that more convenient. You can follow the corresponding issue in Spring Security's backlog.
Here is what i found out after further investigation. The code described in my question was never going to call the client_credentials and fit my use-case. I think (not 100% sure on this) it will be very useful in the future if i am trying to propagate the user submitted token around multiple services in a micro-service architecture. A chain of actions like this comes to mind:
User calls Service A -> Service A calls Service B -> Service B responds -> Service A responds back to user request.
And using the same token to begin with through the whole process.
My solution to my use-case:
What i did was create a new Filter class largely based on the original and implement a step before executing the request where i check if i have a JWT token stored that can be used for the Auth0 Management API. If i don't i build up the client_credentials grant request and get one, then attach this token as a bearer to the initial request and execute that one. I also added a small token in-memory caching mechanism so that if the token is valid any other requests at a later date will just use it. Here is my code.
Filter
public class Auth0ClientCredentialsGrantFilterFunction implements ExchangeFilterFunction {
private ReactiveClientRegistrationRepository clientRegistrationRepository;
/**
* Required by auth0 when requesting a client credentials token
*/
private String audience;
private String clientRegistrationId;
private Auth0InMemoryAccessTokenStore auth0InMemoryAccessTokenStore;
public Auth0ClientCredentialsGrantFilterFunction(ReactiveClientRegistrationRepository clientRegistrationRepository,
String clientRegistrationId,
String audience) {
this.clientRegistrationRepository = clientRegistrationRepository;
this.audience = audience;
this.clientRegistrationId = clientRegistrationId;
this.auth0InMemoryAccessTokenStore = new Auth0InMemoryAccessTokenStore();
}
public void setAuth0InMemoryAccessTokenStore(Auth0InMemoryAccessTokenStore auth0InMemoryAccessTokenStore) {
this.auth0InMemoryAccessTokenStore = auth0InMemoryAccessTokenStore;
}
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
return auth0ClientCredentialsToken(next)
.map(token -> bearer(request, token.getTokenValue()))
.flatMap(next::exchange)
.switchIfEmpty(next.exchange(request));
}
private Mono<OAuth2AccessToken> auth0ClientCredentialsToken(ExchangeFunction next) {
return Mono.defer(this::loadClientRegistration)
.map(clientRegistration -> new ClientCredentialsRequest(clientRegistration, audience))
.flatMap(request -> this.auth0InMemoryAccessTokenStore.retrieveToken()
.switchIfEmpty(refreshAuth0Token(request, next)));
}
private Mono<OAuth2AccessToken> refreshAuth0Token(ClientCredentialsRequest clientCredentialsRequest, ExchangeFunction next) {
ClientRegistration clientRegistration = clientCredentialsRequest.getClientRegistration();
String tokenUri = clientRegistration
.getProviderDetails().getTokenUri();
ClientRequest clientCredentialsTokenRequest = ClientRequest.create(HttpMethod.POST, URI.create(tokenUri))
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.body(clientCredentialsTokenBody(clientCredentialsRequest))
.build();
return next.exchange(clientCredentialsTokenRequest)
.flatMap(response -> response.body(oauth2AccessTokenResponse()))
.map(OAuth2AccessTokenResponse::getAccessToken)
.doOnNext(token -> this.auth0InMemoryAccessTokenStore.storeToken(token));
}
private static BodyInserters.FormInserter<String> clientCredentialsTokenBody(ClientCredentialsRequest clientCredentialsRequest) {
ClientRegistration clientRegistration = clientCredentialsRequest.getClientRegistration();
return BodyInserters
.fromFormData("grant_type", AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.with("client_id", clientRegistration.getClientId())
.with("client_secret", clientRegistration.getClientSecret())
.with("audience", clientCredentialsRequest.getAudience());
}
private Mono<ClientRegistration> loadClientRegistration() {
return Mono.just(clientRegistrationId)
.flatMap(r -> clientRegistrationRepository.findByRegistrationId(r));
}
private ClientRequest bearer(ClientRequest request, String token) {
return ClientRequest.from(request)
.headers(headers -> headers.setBearerAuth(token))
.build();
}
static class ClientCredentialsRequest {
private final ClientRegistration clientRegistration;
private final String audience;
public ClientCredentialsRequest(ClientRegistration clientRegistration, String audience) {
this.clientRegistration = clientRegistration;
this.audience = audience;
}
public ClientRegistration getClientRegistration() {
return clientRegistration;
}
public String getAudience() {
return audience;
}
}
}
Token Store
public class Auth0InMemoryAccessTokenStore implements ReactiveInMemoryAccessTokenStore {
private AtomicReference<OAuth2AccessToken> token = new AtomicReference<>();
private Clock clock = Clock.systemUTC();
private Duration accessTokenExpiresSkew = Duration.ofMinutes(1);
public Auth0InMemoryAccessTokenStore() {
}
#Override
public Mono<OAuth2AccessToken> retrieveToken() {
return Mono.justOrEmpty(token.get())
.filter(Objects::nonNull)
.filter(token -> token.getExpiresAt() != null)
.filter(token -> {
Instant now = this.clock.instant();
Instant expiresAt = token.getExpiresAt();
if (now.isBefore(expiresAt.minus(this.accessTokenExpiresSkew))) {
return true;
}
return false;
});
}
#Override
public Mono<Void> storeToken(OAuth2AccessToken token) {
this.token.set(token);
return Mono.empty();
}
}
Token Store Interface
public interface ReactiveInMemoryAccessTokenStore {
Mono<OAuth2AccessToken> retrieveToken();
Mono<Void> storeToken(OAuth2AccessToken token);
}
And finally defining the beans and using it.
#Bean
public Auth0ClientCredentialsGrantFilterFunction auth0FilterFunction(ReactiveClientRegistrationRepository clientRegistrations,
#Value("${auth0.client-registration-id}") String clientRegistrationId,
#Value("${auth0.audience}") String audience) {
return new Auth0ClientCredentialsGrantFilterFunction(clientRegistrations, clientRegistrationId, audience);
}
#Bean(name = "auth0-webclient")
WebClient webClient(Auth0ClientCredentialsGrantFilterFunction filter) {
return WebClient.builder()
.filter(filter)
.build();
}
There is a slight problem with the token store at this time as the client_credentials token request will be executed multiple on parallel requests that come at the same time, but i can live with that for the foreseeable future.
Your application.yml is missing one variable:
client-authentication-method: post
it should be like this:
spring:
security:
oauth2:
client:
provider:
auth0-client:
token-uri: https://XXXX.auth0.com//
registration:
auth0-client:
client-id: Client
client-secret: Secret
authorization_grant_type: client_credentials
client-authentication-method: post
Without it I was getting "invalid_client" response all the time.
Tested in spring-boot 2.7.2

Resources