I have a fairly simple case where I am trying to add HTTP headers (not SOAP headers) to a request I am making using Spring's WebServiceTemplate.
I have defined a ClientInterceptor where I am doing:
#Override
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {
try {
TransportContext context = TransportContextHolder.getTransportContext();
HttpComponentsConnection connection = (HttpComponentsConnection) context.getConnection();
connection.addRequestHeader("Authorization", String.format("Bearer %s", someAccessToken));
} catch (IOException exception) {
// Do nothing
}
return true;
}
This is how I configure my SomeClient which extends WebServiceConfigurationSupport:
#Bean
public SomeClient someClient() {
...
SomeClientImpl service = new SomeClientImpl();
service.setObjectFactory(new com.path.ObjectFactory());
service.setDefaultUri(someUri);
service.setMarshaller(marshaller);
service.setUnmarshaller(marshaller);
service.setxStreamMarshaller(xStreamMarshaller);
service.setInterceptors(new ClientInterceptor[]{wss4jSecurityInterceptor()});
service.setMessageSender(new HttpComponentsMessageSender());
service.setInterceptors(new ClientInterceptor[]{wss4jSecurityInterceptor(), addHttpHeaderInterceptor()});
return service;
}
#Bean
public ClientInterceptor addHttpHeaderInterceptor() {
return new AddHttpHeaderInterceptor(someAccessToken);
}
#Bean
public Wss4jSecurityInterceptor wss4jSecurityInterceptor() {
Wss4jSecurityInterceptor interceptor = new Wss4jSecurityInterceptor();
interceptor.setSecurementActions(securementAction);
interceptor.setSecurementUsername(securementUsername);
interceptor.setSecurementPassword(securementPassword);
interceptor.setSecurementPasswordType(WSConstants.PW_TEXT);
interceptor.setSecurementMustUnderstand(false);
return interceptor;
}
But the Authorization header is not being added. I have also tried with a CustomMessageCallback:
public class CustomMessageCallback implements WebServiceMessageCallback {
private String headerKey;
private String headerValue;
public CustomMessageCallback(String headerKey, String headerValue) {
this.headerKey = headerKey;
this.headerValue = headerValue;
}
#Override
public void doWithMessage(WebServiceMessage webServiceMessage) throws IOException, TransformerException {
TransportContext context = TransportContextHolder.getTransportContext();
HttpComponentsConnection conn = (HttpComponentsConnection) context.getConnection();
HttpPost post = conn.getHttpPost();
post.addHeader(headerKey, headerValue);
}
}
But it does not seem to work as well. What am I doing wrong, why the Authorization header is not being added? Thanks!
Use the HeadersAwareSenderWebServiceConnection interface instead of the actual underlying connection.
TransportContext context = TransportContextHolder.getTransportContext();
HeadersAwareSenderWebServiceConnection connection = (HeadersAwareSenderWebServiceConnection) context.getConnection();
connection.addRequestHeader("Authorization", String.format("Bearer %s", "********"));
Now if you upgrade/switch HTTP library you don't need to change this code.
To answer your question about what you are doing wrong is that you are casting to the wrong class. Yes the class you are using is deprecated but it is part of the library you are using, you cannot just cast to a different class without changing the underlying HTTP library.
What I did in past is to use a WebServiceMessageCallback like this one:
public class WsHttpHeaderCallback implements WebServiceMessageCallback
{
private String headerKey;
private String headerValue;
private String soapAction;
public WsHttpHeaderCallback(String headerKey, String headerValue, String soapAction)
{
super();
this.headerKey = headerKey;
this.headerValue = headerValue;
this.soapAction = soapAction;
validateRequiredFields();
}
public WsHttpHeaderCallback()
{
super();
}
#Override
public void doWithMessage(WebServiceMessage message) throws IOException, TransformerException
{
validateRequiredFields();
addRequestHeader(headerKey, headerValue);
if (StringUtils.hasText(this.soapAction))
{
AxiomSoapMessage axiomMessage = (AxiomSoapMessage) message;
axiomMessage.setSoapAction(this.soapAction);
}
}
private void validateRequiredFields()
{
if( !StringUtils.hasText(headerKey) )
{
throw new IllegalArgumentException("Impossibile proseguire. Passato HEADER HTTP con chiave non valida: ["+headerKey+"]");
}
if( !StringUtils.hasText(headerValue) )
{
throw new IllegalArgumentException("Impossibile proseguire. Passato HEADER HTTP con valore non valido: ["+headerValue+"]");
}
}
private void addRequestHeader(String headerKey, String headerValue)
{
TransportContext context = TransportContextHolder.getTransportContext();
WebServiceConnection connection = context.getConnection();
if (connection instanceof HttpComponentsConnection)
{
HttpComponentsConnection conn = (HttpComponentsConnection) connection;
HttpPost post = conn.getHttpPost();
post.addHeader(headerKey, headerValue);
}
else if( connection instanceof ClientHttpRequestConnection )
{
ClientHttpRequestConnection conn = (ClientHttpRequestConnection)connection;
conn.getClientHttpRequest().getHeaders().add(headerKey, headerValue);
}
}
}
Then I used it in this way:
wsTemplate.marshalSendAndReceive(wsUrl, request, new WsHttpHeaderCallback(headerKey, headerValue, soapAction) );
In this way I successfully set all the needed HttpHeaders (in my case just one :) )
I hope it is usefull
Angelo
TL;DR
Your messageSender should be an instance of HttpComponentsMessageSender instead of HttpUrlConnectionMessageSender. Also you need to provide proper credentials.
getConnection() function of TransportContext returns an implementation of WebServiceConnection. Both HttpUrlConnection and HttpComponentsConnection are implementations of the same. So basically you are getting the wrong type of connection,hence the ClassCastException.
The ClientInterceptor will work for custom headers but not for Authorization header. For that, your HttpComponentsMessageSender needs to be configured with your credentials.
The proper configuration should be like this
#Value("${username}")
private String username;
#Value("${password}")
private String password;
#Bean
public SomeClient someClient() {
SomeClientImpl service = new SomeClientImpl();
service.setMessageSender();
//other configs
return service;
}
public HttpComponentsMessageSender getMessageSender(){
HttpComponentsMessageSender httpComponentsMessageSender = new HttpComponentsMessageSender();
httpComponentsMessageSender.setCredentials(getCredentials);
}
public UsernamePasswordCredentials getCredentials(){
return new UsernamePasswordCredentials(username, password);
}
I went through a similar exercise, for an endpointInterceptor the connection returns a HttpServletConnection. Therefore used the following and managed to get the HTTP headers added.
HttpServletConnection connection = (HttpServletConnection)context.getConnection();
HttpServletResponse response = connection.getHttpServletResponse();
HttpServletRequest request = connection.getHttpServletRequest();
response.addHeader("myheader", "myvalue");
Some additional tips:
If you want to send back the same header you received in the request, use following in the handleResponse method of the endpointInterceptor
response.addHeader("myheader", request.getHeader("myheader"));
If you are trying to add custom headers in an clientInterceptor to send to a downstream use below in the handleRequest method,
HttpUrlConnection connection = (HttpUrlConnection)context.getConnection();
connection.addRequestHeader("myheader", "myvalue");
Related
I want to implement a feature that user connects his account with external applications (similar feature is in Facebook). User has to log in to external application and grant permission to access data by my application.
Once user connected an external app, data will be exchanged in background using access and refresh tokens.
Application architecture is:
SPA front-end (Angular)
REST API (Spring), multiple nodes
ScyllaDB
Envoy proxy (with JWT verification)
The first idea is to use Spring OAuth2 Client. However, some changes need to be made:
there is no Principal because JWT is verified by Envoy proxy and X-USER-ID header is added
REST API is stateless and we shouldn't store authorization code in session
even with sessions, there are multiple nodes and we need to share authorization code between nodes
custom URL, e.g. /app_name/connect instead of /oauth2/authorization/app_name
redirect URL may be invalid (but it's verified by Spring's filter)
How this could work:
user click "Connect with app" in SPA
SPA redirects user to /oauth2/authorization/app_name (or custom URL)
Spring redirects user to external app's authentication server
user authenticates and grants permissions
external app redirects user back to Spring (or straight to SPA?)
Spring redirects user back to SPA (or SPA sends access token to REST API?)
Despite Spring Security components can be replaced, many of them are coupled and you need to rewrite OAuth2 Client flow almost from scratch. Maybe I'm doing something wrong and it can be achieved easier.
What I already did:
http
.cors().and()
.csrf().disable()
.authorizeRequests().anyRequest().permitAll().and()
.oauth2Client(); // get rid of these two filters?
#Configuration
#RequiredArgsConstructor
public class OAuth2ClientConfig {
private final CassandraTemplate cassandraTemplate;
// overriding original client service - we need to store tokens in database
#Bean
public OAuth2AuthorizedClientService authorizedClientService(
CassandraTemplate cassandraTemplate,
ClientRegistrationRepository clientRegistrationRepository) {
return new ScyllaOAuth2AuthorizedClientService(cassandraTemplate, clientRegistrationRepository);
}
// configure client provider to use authorization code with refresh token
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
var authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.build();
var authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
// the specs recommend to use WebClient for exchanging data instead of RestTemplate
#Bean
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
// override request repository - and I'm stuck there
#Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {
return new ScyllaOAuth2AuthorizationRequestRepository(cassandraTemplate);
}
}
Because there are multiple nodes of REST API, we can't use sessions. We need to store request somewhere, e.g. ScyllaDB, Redis, Hazelcast, etc. I decided to store it as JSON in ScyllaDB but I ran into trouble.
#Slf4j
#RequiredArgsConstructor
public final class ScyllaOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
private final CassandraTemplate cassandraTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
#Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
Assert.notNull(request, "request cannot be null");
var stateParameter = this.getStateParameter(request);
if (stateParameter == null) {
return null;
}
return this.getAuthorizationRequest(request, stateParameter);
}
#Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request,
HttpServletResponse response) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(response, "response cannot be null");
if (authorizationRequest == null) {
this.removeAuthorizationRequest(request, response);
return;
}
var state = authorizationRequest.getState();
var userId = UUID.fromString(request.getHeader(Constants.USER_ID));
Assert.hasText(state, "authorizationRequest.state cannot be empty");
try {
// serialization of Auth2AuthorizationRequest to JSON works
cassandraTemplate.getCqlOperations().execute("insert into oauth2_requests (user_id,state,data) values (?,?,?)",
userId, state, objectMapper.writeValueAsString(authorizationRequest));
} catch (JsonProcessingException e) {
log.warn("Unable to save authorization request", e);
}
}
#Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
Assert.notNull(request, "request cannot be null");
var stateParameter = this.getStateParameter(request);
if (stateParameter == null) {
return null;
}
var userId = UUID.fromString(request.getHeader(Constants.USER_ID));
var originalRequest = this.getAuthorizationRequest(request, stateParameter);
cassandraTemplate.getCqlOperations().execute("delete from oauth2_requests where user_id=? and state=?",
userId, stateParameter);
return originalRequest;
}
private String getStateParameter(HttpServletRequest request) {
return request.getParameter(OAuth2ParameterNames.STATE);
}
private UUID getUserId(HttpServletRequest request) {
return UUID.fromString(request.getHeader(Constants.USER_ID));
}
private OAuth2AuthorizationRequest getAuthorizationRequest(HttpServletRequest request, String state) {
var userId = getUserId(request);
var jsonRequest = cassandraTemplate.getCqlOperations().queryForObject(
"select data from oauth2_requests where user_id=? and state=?", String.class, userId, state);
if (StringUtils.isNotBlank(jsonRequest)) {
try {
// trying to mess with OAuth2ClientJackson2Module
var objectMapper = new Jackson2ObjectMapperBuilder().autoDetectFields(true)
.autoDetectGettersSetters(true)
.modules(new OAuth2ClientJackson2Module())
.visibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
.build();
return objectMapper.readValue(jsonRequest, OAuth2AuthorizationRequest.class);
} catch (JsonProcessingException e) {
log.warn("Error decoding authentication request", e);
}
}
return null;
}
}
I get error when trying to deserialize JSON to OAuth2AuthorizationRequest:
Missing type id when trying to resolve subtype of [simple type, class org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest]: missing type id property '#class'
Without adding OAuth2ClientJackson2Module there is another error:
Cannot construct instance of `org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
By the way, OAuth2ClientJackson2Module seems never used in original filters.
Maybe it's better to serialize this object Java way and store it as BLOB or do not store request in database but somewhere other.
Another part is the controller action:
// it had to be /apps/app_name/connect but in Spring OAuth2 Client it's hardcoded to append provider name at the end
#GetMapping("/apps/connect/app_name")
public void connect(HttpServletRequest request, HttpServletResponse response) throws IOException {
userAppService.authorize(request, response, "app_name");
}
To get rid of filters which verify redirect URL and have many things hardcoded:
#Service
#RequiredArgsConstructor
public class UserAppService {
private final HttpSecurity httpSecurity;
private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private final AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;
private final ClientRegistrationRepository clientRegistrationRepository;
private final OAuth2AuthorizedClientManager authorizedClientManager;
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public void authorize(HttpServletRequest request, HttpServletResponse response, String appName) throws IOException {
var userId = UUID.fromString(request.getHeader(Constants.USER_ID));
var authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(appName)
.principal(UUIDPrincipal.fromUserId(userId))
.build();
if (isAuthorizationResponse(request)) {
var authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request);
if (authorizationRequest != null) {
processAuthorizationRequest(request, response);
}
} else {
try {
OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest);
if (authorizedClient != null) {
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
System.out.println(accessToken);
}
} catch (ClientAuthorizationException e) {
// in this URL provider name is appended at the end and no way to change this behavior
var authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository,
"/apps/connect");
var authorizationRequest = authorizationRequestResolver.resolve(request);
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
this.redirectStrategy.sendRedirect(request, response, authorizationRequest.getAuthorizationRequestUri());
}
}
}
private void processAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
var authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
var registrationId = (String) authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);
var clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
var params = toMultiMap(request.getParameterMap());
var redirectUri = UrlUtils.buildFullRequestUrl(request);
var authorizationResponse = convert(params, redirectUri);
var authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken(
clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
OAuth2AuthorizationCodeAuthenticationToken authenticationResult;
try {
var authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
authenticationResult = (OAuth2AuthorizationCodeAuthenticationToken) authenticationManager
.authenticate(authenticationRequest);
} catch (OAuth2AuthorizationException ex) {
OAuth2Error error = ex.getError();
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(authorizationRequest.getRedirectUri())
.queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode());
if (!StringUtils.hasText(error.getDescription())) {
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription());
}
if (!StringUtils.hasText(error.getUri())) {
uriBuilder.queryParam(OAuth2ParameterNames.ERROR_URI, error.getUri());
}
this.redirectStrategy.sendRedirect(request, response, uriBuilder.build().encode().toString());
return;
}
// just copy-paste of original filter - trying to understand what's happening there
Authentication currentAuthentication = SecurityContextHolder.getContext().getAuthentication();
String principalName = (currentAuthentication != null) ? currentAuthentication.getName() : "anonymousUser";
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
authenticationResult.getClientRegistration(), principalName, authenticationResult.getAccessToken(),
authenticationResult.getRefreshToken());
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, currentAuthentication, request,
response);
String redirectUrl = authorizationRequest.getRedirectUri();
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
}
private static boolean isAuthorizationResponse(HttpServletRequest request) {
return isAuthorizationResponseSuccess(request) || isAuthorizationResponseError(request);
}
private static boolean isAuthorizationResponseSuccess(HttpServletRequest request) {
return StringUtils.hasText(request.getParameter(OAuth2ParameterNames.CODE))
&& StringUtils.hasText(request.getParameter(OAuth2ParameterNames.STATE));
}
private static boolean isAuthorizationResponseError(HttpServletRequest request) {
return StringUtils.hasText(request.getParameter(OAuth2ParameterNames.ERROR))
&& StringUtils.hasText(request.getParameter(OAuth2ParameterNames.STATE));
}
// copy paste - not tested this code yet
static MultiValueMap<String, String> toMultiMap(Map<String, String[]> map) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>(map.size());
map.forEach((key, values) -> {
if (values.length > 0) {
for (String value : values) {
params.add(key, value);
}
}
});
return params;
}
static OAuth2AuthorizationResponse convert(MultiValueMap<String, String> request, String redirectUri) {
String code = request.getFirst(OAuth2ParameterNames.CODE);
String errorCode = request.getFirst(OAuth2ParameterNames.ERROR);
String state = request.getFirst(OAuth2ParameterNames.STATE);
if (StringUtils.hasText(code)) {
return OAuth2AuthorizationResponse.success(code).redirectUri(redirectUri).state(state).build();
}
String errorDescription = request.getFirst(OAuth2ParameterNames.ERROR_DESCRIPTION);
String errorUri = request.getFirst(OAuth2ParameterNames.ERROR_URI);
return OAuth2AuthorizationResponse.error(errorCode)
.redirectUri(redirectUri)
.errorDescription(errorDescription)
.errorUri(errorUri)
.state(state)
.build();
}
}
Client service to stored authorized clients in database:
#RequiredArgsConstructor
public class ScyllaOAuth2AuthorizedClientService implements OAuth2AuthorizedClientService {
private final CassandraTemplate cassandraTemplate;
private final ClientRegistrationRepository clientRegistrationRepository;
#Override
#SuppressWarnings("unchecked")
public OAuth2AuthorizedClient loadAuthorizedClient(String clientRegistrationId, String principal) {
var id = BasicMapId.id("userId", principal).with("appCode", clientRegistrationId);
var userApp = cassandraTemplate.selectOneById(id, UserApp.class);
if (userApp != null) {
var clientRegistration = getClientRegistration(clientRegistrationId);
var accessToken = getAccessToken(userApp);
var refreshToken = getRefreshToken(userApp);
return new OAuth2AuthorizedClient(clientRegistration, principal, accessToken, refreshToken);
} else {
return null;
}
}
#Override
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
Assert.notNull(authorizedClient, "authorizedClient cannot be null");
Assert.notNull(principal, "principal cannot be null");
var userApp = new UserApp();
userApp.setUserId((UUID) principal.getPrincipal());
userApp.setAppCode(authorizedClient.getClientRegistration().getClientId());
if (authorizedClient.getAccessToken() != null) {
userApp.setAccessToken(authorizedClient.getAccessToken().getTokenValue());
userApp.setAccessTokenType(OAuth2AccessToken.TokenType.BEARER.getValue());
userApp.setAccessTokenScopes(authorizedClient.getAccessToken().getScopes());
userApp.setAccessTokenIssuedAt(authorizedClient.getAccessToken().getIssuedAt());
userApp.setAccessTokenExpiresAt(authorizedClient.getAccessToken().getExpiresAt());
}
if (authorizedClient.getRefreshToken() != null) {
userApp.setRefreshToken(authorizedClient.getRefreshToken().getTokenValue());
userApp.setRefreshTokenIssuedAt(authorizedClient.getRefreshToken().getIssuedAt());
userApp.setRefreshTokenExpiresAt(authorizedClient.getRefreshToken().getExpiresAt());
}
cassandraTemplate.insert(userApp);
}
#Override
public void removeAuthorizedClient(String clientRegistrationId, String principal) {
var id = BasicMapId.id("userId", principal).with("appCode", clientRegistrationId);
cassandraTemplate.deleteById(id, UserApp.class);
}
private ClientRegistration getClientRegistration(String clientRegistrationId) {
var clientRegistration = this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId);
if (clientRegistration == null) {
throw new DataRetrievalFailureException(
"The ClientRegistration with id '" + clientRegistrationId + "' exists in the data source, "
+ "however, it was not found in the ClientRegistrationRepository.");
}
return clientRegistration;
}
private OAuth2AccessToken getAccessToken(UserApp userApp) {
return new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER,
userApp.getAccessToken(),
userApp.getAccessTokenIssuedAt(),
userApp.getAccessTokenExpiresAt(),
userApp.getAccessTokenScopes());
}
private OAuth2RefreshToken getRefreshToken(UserApp userApp) {
return new OAuth2RefreshToken(userApp.getRefreshToken(), userApp.getRefreshTokenIssuedAt());
}
}
Too much code overwrite. I need to make it as simple as possible.
Currently I'm struggling with storing authorize request in database.
How to do it Spring way but to keep the app architecture given at the beginning of this question?
Any way to configure OAuth2 Client without hardcoded URL like /oauth2/authorization/provider_name?
Maybe it's better to do the whole OAuth2 flow client-side (within SPA) and the SPA should send access and request token to REST API (to store the tokens in order to be able to exchange data with external app)?
In OAuth2 wording, REST APIs are resource-servers, not clients.
What you can do is have
your proxy be transparent to OAuth2 (forward requests with their JWT access-token authorization header and responses status code)
configure each REST API as resource-server. Tutorials there: https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials.
add an OAuth2 client library to your Angular app to handle tokens and authorize requests. My favorite is angular-auth-oidc-client
probably use an intermediate authorization-server for identity federation (Google, Facebook, etc., but also internal DB, LDAP, or whatever is needed), roles management, MFA,... Keycloak is a famous "on premise" solution, but you can search for "OIDC authorization-server" in your favorite search engine and have plenty of alternate choices, including SaaS like Auth0 or Amazon Cognito.
This is fully compatible with distributed architectures and micro-services (session-less is the default configuration for resource-servers in the tutorials I linked).
Two cases for a micro-service delegating some of its processing to another resource-server:
the "child" request is made on behalf of the user who initiated the request => retrieve original access-token from Authentication instance in security-context and forward it (set it as Bearer authorization header for the sub-request)
the "child" request is not made on behalf of a user => client-credentials must be used (the micro-services acquires a new access-token in its own name to authorize the sub request). Refer to spring-boot-oauth2-client and your preferred REST client docs for details (WebClient, #FeignClient, RestTemplate).
I have an endpoint
#GetMapping(value = "/accounts/{accountId}/placementInfo", headers = "version=1")
#ResponseStatus(HttpStatus.OK)
public List<PlacementDetail> findPlacementDetailByPlacementInfoAtTime(#PathVariable("accountId") Long accountId,
#RequestParam(value = "searchDate", required = false)
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate searchDate) {}
And I am sending the request by using rest template
placementResponseEntity = restTemplate.exchange(placementUriBuilder(accountId, searchDate), HttpMethod.GET,
apiRequestEntity,new ParameterizedTypeReference<List<PlacementDetail>>() {});
with a helper method
private String placementUriBuilder(long accountId, LocalDate searchDate) throws IOException {
String resourceUri = ACCOUNT_RESOURCE_URI_START + accountId + PLACEMENT_DETAIL_RESOURCE_URI_END;
String url;
if(searchDate != null) {
url = UriComponentsBuilder.fromUri(serverUri.getUri()).path(resourceUri).queryParam("searchDate", searchDate.format(DateTimeFormatter.ISO_DATE)).build().toUriString();
} else {
url = UriComponentsBuilder.fromUri(serverUri.getUri()).path(resourceUri).build().toUriString();
}
return url;
}
When I look at the SO people talk about sending the object and failing as the created JSON is in wrong format but here this is a get api and I do not understand the source of the problem.
This is commonly caused by a missing error handler on your RestTemplate. Your server responds with an error and your client tries to deserialize it to a List<PlacementDetail>. In order to address this, you should properly handle HTTP error codes.
See the below snippet.
#Configuration
public class ClientConfig {
#Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.errorHandler(new ClientErrorHandler())
.build();
}
public class ClientErrorHandler implements ResponseErrorHandler {
#Override
public boolean hasError(ClientHttpResponse httpResponse) throws IOException {
// check if HTTP status signals an error response
return !HttpStatus.OK.equals(httpResponse.getStatusCode());
}
#Override
public void handleError(ClientHttpResponse httpResponse) throws IOException {
// handle exception case as you see fit
throw new RuntimeException("Error while making request");
}
}
}
This approach always worked when updating a token. That is, with each request if I received an error 401, the operator retryWhen() triggered it updated the token.
Here is the code:
private Observable<TokenModel> refreshAccessToken() {
Map<String, String> requestBody = new HashMap<>();
requestBody.put(Constants.EMAIL_KEY, Constants.API_EMAIL);
requestBody.put(Constants.PASSWORD_KEY, Constants.API_PASSWORD);
return RetrofitHelper.getApiService().getAccessToken(requestBody)
.subscribeOn(Schedulers.io())
.doOnNext((AccessToken refreshedToken) -> {
PreferencesHelper.putAccessToken(mContext, refreshedToken);
});
}
public Function<Observable<Throwable>, ObservableSource<?>> isUnauthorized (){
return throwableObservable -> throwableObservable.flatMap((Function<Throwable, ObservableSource<?>>) (Throwable throwable) -> {
if (throwable instanceof HttpException) {
HttpException httpException = (HttpException) throwable;
if (httpException.code() == 401) {
return refreshAccessToken();
}
}
return Observable.error(throwable);
});
}
I call isUnauthorized() at the retryWhen() operator where I make a request to the server
class RetrofitHelper {
static ApiService getApiService() {
return initApi();
}
private static OkHttpClient createOkHttpClient() {
final OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
httpClient.addInterceptor(chain -> {
Request originalRequest = chain.request();
AccessToken accessToken= PreferencesHelper.getAccessToken(BaseApplication.getInstance());
String accessTokenStr = accessToken.getAccessToken();
Request.Builder builder =
originalRequest.newBuilder().header("Authorization", "Bearer " + accessTokenStr);
Request newRequest = builder.build();
return chain.proceed(newRequest);
});
return httpClient.build();
}
private static ApiService initApi(){
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(Constants._api_url)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(ScalarsConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(createOkHttpClient())
.build();
return retrofit.create(ApiService.class);
}
}
But we recently added Basic Auth, and now at the first request I get 401 and retryWhen() tries to update the Token, but still gets 401. That is, the doOnNext() does not work, but immediately the onError() works
private static Observable<AccessToken> refreshAccessToken() {
return RetrofitHelper.getApiService()
.getAccessToken(
Credentials.basic(
Constants._API_USERNAME, Constants._API_PASSWORD
),
Constants._API_BODY_USERNAME,
Constants._API_BODY_PASSWORD,
Constants._API_BODY_GRANT_TYPE
)
.doOnNext((AccessToken refreshedToken) -> {
PreferencesHelper.putObject(BaseApplication.getInstance(), PreferenceKey.ACCESS_TOKEN_KEY, refreshedToken);
}
});
}
// Api Service
public interface ApiService {
// Get Bearer Token
#FormUrlEncoded
#POST("oauth/token")
Observable<AccessToken> getAccessToken(#Header("Authorization") String basicAuth,
#Field("username") String username,
#Field("password") String password,
#Field("grant_type") String grantType);
}
Here, tell me why this is a mistake? Why at the first request I get 401, and from the second request everything works?
I want to suggest a better solution.
public class RefreshTokenTransformer<T extends Response<?>> implements ObservableTransformer<T, T> {
private class HttpCode {
private static final int UNAUTHORIZED_HTTP_CODE = 401;
}
private ApiService mApiService;
private UserRepository mUserRepository;
public RefreshTokenTransformer(ApiService service, UserRepository userRepository) {
mApiService = service;
mUserRepository = userRepository;
}
#Override
public ObservableSource<T> apply(final Observable<T> stream) {
return stream.flatMap(new Function<T, ObservableSource<T>>() {
#Override
public ObservableSource<T> apply(T response) throws Exception {
if (response.code() == HttpCode.UNAUTHORIZED_HTTP_CODE) {
return mApiService.refreshToken(mUserRepository.getRefreshTokenHeaders())
.filter(new UnauthorizedPredicate<>(mUserRepository))
.flatMap(new Function<Response<TokenInfo>, ObservableSource<T>>() {
#Override
public ObservableSource<T> apply(Response<TokenInfo> tokenResponse) throws Exception {
return stream.filter(new UnauthorizedPredicate<T>(mUserRepository));
}
});
}
return stream;
}
});
}
private class UnauthorizedPredicate<R extends Response<?>> implements Predicate<R> {
private UserRepository mUserRepository;
private UnauthorizedPredicate(UserRepository userRepository) {
mUserRepository = userRepository;
}
#Override
public boolean test(R response) throws Exception {
if (response.code() == HttpCode.UNAUTHORIZED_HTTP_CODE) {
throw new SessionExpiredException();
}
if (response.body() == null) {
throw new HttpException(response);
}
Class<?> responseBodyClass = response.body().getClass();
if (responseBodyClass.isAssignableFrom(TokenInfo.class)) {
try {
mUserRepository.validateUserAccess((TokenInfo) response.body());
} catch (UnverifiedAccessException error) {
throw new SessionExpiredException(error);
}
}
return true;
}
}
}
I`ve written the custom operator, which makes next actions:
first request started, and we get 401 response code;
then we execute /refresh_token request to update the token;
after that if the token is refreshed successfully, we repeat the
first request. if /refresh_token token is failed, we throw exception
Then, you can easy implement it in the any request like that:
Observable
.compose(new RefreshTokenResponseTransformer<Response<{$your_expected_result}>>
(mApiService, mUserRepository()));
One more important thing:
Most likely, that your initial observable for retrofit has params, like that:
mApiService.someRequest(token)
if the param is expected to change during the performing RefreshTokenTransformer(e.g. /refresh_token request will get new access token and you save it somewhere, then you want to use a fresh access token to repeat the request) you will need to wrap your observable with defer operator to force the creating of new observable like that:
Observable.defer(new Callable<ObservableSource<Response<? extends $your_expected_result>>>() {
#Override
public Response<? extends $your_expected_result> call() throws Exception {
return mApiService.someRequest(token);
}
})
I think it does not need to use interceptor instead you implement Authenticator by which you can access refreshed token and okhttp automatically will handle that. if you get 401 it updates header with refreshed token and make new request.
public class TokenAuthenticator implements Authenticator {
#Override
public Request authenticate(Proxy proxy, Response response) throws IOException {
// Refresh your access_token using a synchronous api request
newAccessToken = service.refreshToken();
// Add new header to rejected request and retry it
return response.request().newBuilder()
.header(AUTHORIZATION, newAccessToken)
.build();
}
In my app I am using netflix zuul to route a request from a microservice (gateway) to another. The requests are being routed fine but I also want to introduce some parameters in the request body before it is routed to the appropriate microservice. For this I am using Zuul pre filter like this.
public class SimpleFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(SimpleFilter.class);
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 1;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
try {
RequestContext context = RequestContext.getCurrentContext();
InputStream in = (InputStream) context.get("requestEntity");
if (in == null) {
in = context.getRequest().getInputStream();
}
String body = StreamUtils.copyToString(in, Charset.forName("UTF-8"));
// body = "request body modified via set('requestEntity'): "+ body;
body = body.toUpperCase();
context.set("requestEntity", new ByteArrayInputStream(body.getBytes("UTF-8")));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return null;
}
}
For now I am just trying to change the body to upper case but the microservice to which this request is routed doesn't receive the modified body (upper case). Instead it receives the original one. Am I doing something wrong. Any help would be appreciated. Thanks !!
Was able to do the following - transform a GET request to a POST request, and add body content to the (proxied) POST request.
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
context.addZuulRequestHeader("Content-Type", "application/x-www-form-urlencoded");
String body = String.format("a=%s&b=%s", a, b);
final byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
context.setRequest(new HttpServletRequestWrapper(context.getRequest()) {
#Override
public ServletInputStream getInputStream() {
return new ServletInputStreamWrapper(bytes);
}
#Override
public int getContentLength() {
return bytes.length;
}
#Override
public long getContentLengthLong() {
return bytes.length;
}
#Override
public String getMethod() {
return "POST";
}
});
return null;
}
try this one It's may be work in your case .
requestContext.getCurrentContext().put("requestEntity", new ByteArrayInputStream(body.getBytes("UTF-8")));
Turned out this method cannot change the request body within the requestContext. Truly in the requestContext, a new field "requestEntity" is added, however, the request body from context.getRequest().getInputStream() remains the same after this operation.
You can modify the request body, see this answer for an example. You just need to wrap the new request data and make sure you correctly report it's new content length.
I have used following code to read activemq message. I need to implement activemq listener using REST.
This is message read demo class
public class ActiveMQConsumer {
public static void main(String args[]) {
String url = "http://localhost:8161/api/message?destination=queue://test.queue&readTimeout=10000&type=queue&clientId=test";
RestClient client = new RestClient(userName, password);
String activeMQResponse = client.get(url);
System.out.println(activeMQResponse);
}
}
This is spring resttemplate HTTP connecting class
public class RestClient {
private String server = "";
private RestTemplate rest;
private HttpHeaders headers;
private HttpStatus status;
public RestClient(String userName, String password) {
this.rest = new RestTemplate();
this.headers = createHeaders(userName, password);
}
public HttpHeaders createHeaders(String username, String password) {
return new HttpHeaders() {
{
String auth = username + ":" + password;
byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(Charset.forName("US-ASCII")));
String authHeader = "Basic " + new String(encodedAuth);
set("Authorization", authHeader);
set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
set("Accept", "*/*");
}
};
}
public String get(String uri) {
HttpEntity<String> requestEntity = new HttpEntity<String>("", headers);
ResponseEntity<String> responseEntity = rest.exchange(server + uri, HttpMethod.GET, requestEntity, String.class);
this.setStatus(responseEntity.getStatusCode());
return responseEntity.getBody();
}
public HttpStatus getStatus() {
return status;
}
public void setStatus(HttpStatus status) {
this.status = status;
}
}
First of all, I wouldn't use RestClient for contacting some broker, since it's not just simple http communication. It's mqtt or amqt protocol not http. Ok I see that active mq provides http api, but still, I suggest to go with different approach.
Use apache-camel activemq component. Then
you will simply create a route like:
from("activemq:queue:your_queue")
.process(e -> {...}) // your processing of message goes here.
More about how to create routes can be found here. Good thing is, that camel has great support and also great documentation.
If it is an requirement, that you must consume messages from rest, then i would probably use unirest.