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 set on my properties like this
gcp.storage.json-path=${GOOGLE_APPLICATION_CREDENTIALS}
and have code like this
public CloudStorageService(
#Value("${gcp.storage.bucket-name-file}") String fileBucketName,
#Value("${gcp.storage.bucket-name}") String bucketName,
#Value("${gcp.storage.json-path}") String jsonPath
) {
this.fileBucketName = fileBucketName;
this.bucketName = bucketName;
this.jsonPath = jsonPath;
log.info("fileBuckerName: {}", fileBucketName);
log.info("bucketName: {}", bucketName);
log.info("jsonPath: {}", jsonPath);
}
#PostConstruct
void init() {
try {
GoogleCredentials credentials = GoogleCredentials.fromStream(new FileInputStream(jsonPath))
.createScoped(Lists.newArrayList("https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.read_write"));
// storage = StorageOptions.getDefaultInstance().getService();
but when i deploy my code on kubernetes i cannot upload my file to GCS. my question is how to read data from my kubernetes workload identity using java ?
I'm using RestTemplate to call my webservice's health actuator endpoint from another webservice of mine to see if the webservice is up. If the webservice is up, all works fine, but when it's down, I get an error 500, "Internal Server Error". If my webservice is down, I'm trying to catch that error to be able to handle it, but the problem I'm having is that I can't seem to be able to catch the error.
I've tried the following and it never enters either of my catch sections
#Service
public class DepositService {
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofMillis(3000))
.setReadTimeout(Duration.ofMillis(3000))
.build();
}
private static void getBankAccountConnectorHealth() {
final String uri = "http://localhost:9996/health";
RestTemplate restTemplate = new RestTemplate();
String result = null;
try {
result = restTemplate.getForObject(uri, String.class);
} catch (HttpClientErrorException exception) {
System.out.println("callToRestService Error :" + exception.getResponseBodyAsString());
} catch (HttpStatusCodeException exception) {
System.out.println( "callToRestService Error :" + exception.getResponseBodyAsString());
}
System.out.println(result);
}
}
I've also tried doing it this way, but same results. It never seems to enter my error handler class.
public class NotFoundException extends RuntimeException {
}
public class RestTemplateResponseErrorHandler implements ResponseErrorHandler {
#Override
public boolean hasError(ClientHttpResponse httpResponse) throws IOException {
return (httpResponse.getStatusCode().series() == CLIENT_ERROR || httpResponse.getStatusCode().series() == SERVER_ERROR);
}
#Override
public void handleError(ClientHttpResponse httpResponse) throws IOException {
if (httpResponse.getStatusCode().series() == HttpStatus.Series.SERVER_ERROR) {
// handle SERVER_ERROR
System.out.println("Server error!");
} else if (httpResponse.getStatusCode().series() == HttpStatus.Series.CLIENT_ERROR) {
// handle CLIENT_ERROR
System.out.println("Client error!");
if (httpResponse.getStatusCode() == HttpStatus.NOT_FOUND) {
throw new NotFoundException();
}
}
}
}
#Service
public class DepositService {
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofMillis(3000))
.setReadTimeout(Duration.ofMillis(3000))
.build();
}
private static void getAccountHealth() {
final String uri = "http://localhost:9996/health";
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new RestTemplateResponseErrorHandler());
String result = null;
result = restTemplate.getForObject(uri, String.class);
System.out.println(result);
}
}
Any suggestions as to how I can call my webservice's health actuator from another webservice and catch if that webservice is down?
It looks like the getForObject doesn't throw either of the exceptions you are catching. From the documentation, it throws RestClientException. The best method I have found for identifying thrown exceptions is to catch Exception in the debugger and inspect it to find out if it's useful.
With the second method, I'm not sure why you would create a bean method for the RestTemplate and then create one with new. You probably should inject your RestTemplate and initialise the ResponseErrorHandler with the RestTemplateBuilder::errorHandler method.
Internal serve error throw HttpServerErrorException You should catch this exception if you want to handle it However the better way to do that is using error handler you can see the following posts to see how to do that:
spring-rest-template-error-handling
spring-boot-resttemplate-error-handling
I'm trying to create a test where I have to mock a method inside the class that I want to test. But it keeps calling the real method, but I want mock it.
The method that I want to mock is
extractSecretValue(String path)
I know it's not mocking the method because there is a "println", and it's printing.
What am I doing wrong?
I'm using JUnit 5
The class that I want to test:
#Configuration
public class RestTemplateConfig {
#Value("${******}")
private String keystore;
#Value("${******}")
private String identificador;
#Value("${******}")
private String token;
#Bean
public RestTemplate restTemplate() throws NoSuchAlgorithmException, KeyManagementException {
SSLContext context = null;
context = SSLContext.getInstance("TLSv1.2");
context.init(null, null, null);
List<Header> headers = new ArrayList<>();
headers.add(new BasicHeader("Authorization", "Bearer " + extractSecretValue(token)));
CloseableHttpClient httpClient = HttpClientBuilder.create().setSSLContext(context).setDefaultHeaders(headers)
.build();
HttpComponentsClientHttpRequestFactory hcchr = new HttpComponentsClientHttpRequestFactory(httpClient);
hcchr.setConnectionRequestTimeout(10000);
return new RestTemplate(hcchr);
}
public String extractSecretValue(String path) {
System.out.println("Test1");
Path secretPath = Paths.get(path);
String value = "";
try (Stream<String> lines = Files.lines(secretPath)) {
value = lines.collect(Collectors.joining());
} catch (IOException ignored) {
throw new ApplicationException(ignored);
}
return value.isEmpty() ? path : value;
}
}
The Test class:
#ExtendWith(MockitoExtension.class)
public class RestTemplateConfigTest {
#Test
public void return_restTemplateConfig() {
RestTemplateConfig restTemplateConfig = new RestTemplateConfig();
RestTemplateConfig restTemplateMock;
RestTemplate restTemplate;
restTemplateMock = Mockito.spy(restTemplateConfig);
try {
when(restTemplateMock.extractSecretValue(anyString())).thenReturn("423424");
restTemplate = restTemplateMock.restTemplate();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new ApplicationException(e);
}
}
}
I've already tried this too:
doReturn("2332").when(restTemplateMock).extractSecretValue(anyString());
If you use when(...).thenReturn(...) the real method will still be invoked
(from Mockito, which is not relevant for your test),
but that should not happen when you use the doReturn(...).when(...) notation instead.
The problem in your test is that token is null and your anyString() does not match that as it only matches non-null strings.
Use any() instead, which matches anything, including nulls.
Combine that with the doReturn(...).when(...) and your test should succeed.
If you do not want the actual methods to be called, then you should be using Mockito.mock() and not Mockito.spy().
you should update your test class to use :
restTemplateMock = Mockito.mock(RestTemplateConfig.class);
You haven't mocked your RestTemplateConfig, you've instantiated it.
What you want is:
restTemplateMock = Mockito.spy(new RestTemplateConfig());
I am currently working on an client program to access the soap webservice. The webservice is hit via WSO2 Identity Server (IS) Security Token Service (STS). I have coded using org.apache.rahas.client.STSClient. I am looking for a solution to add trusted certificates for STSClient rather than updating the trusted entries at JVM level, because post this webservice call, I have functionality which will need access to default cacerts. Kindly help me with the solution.
Basically I am trying to replace the below two lines
System.setProperty("javax.net.ssl.trustStore", keystorePath);
System.setProperty("javax.net.ssl.trustStorePassword", keystorePwd);
I explored setting up stsClient.setCryptoInfo() with the truststore details, but I didn't get enough help from internet to understand what it does.
I tried exploring how set custom ssl for the stsclient, but couldn't figure out that also.
public class Client {
public void initialize() {
try {
loadConfigurations();
// set the trust store as a system property for communication over
// TLS.
System.setProperty("javax.net.ssl.trustStore", keystorePath);
System.setProperty("javax.net.ssl.trustStorePassword", keystorePwd);
// create configuration context
ConfigurationContext configCtx = ConfigurationContextFactory
.createConfigurationContextFromFileSystem(repoPath);
// create STS client
STSClient stsClient = new STSClient(configCtx);
stsClient.setRstTemplate(getRSTTemplate());
String action = null;
String responseTokenID = null;
action = TrustUtil.getActionValue(RahasConstants.VERSION_05_02,
RahasConstants.RST_ACTION_ISSUE);
stsClient.setAction(action);
// request the security token from STS.
Token responseToken;
Policy stsPolicy = loadPolicy(stsPolicyPath);
// add rampart config assertion to the ws-sec policies
RampartConfig rampartConfig = buildRampartConfig();
stsPolicy.addAssertion(rampartConfig);
responseToken = stsClient.requestSecurityToken(null, stsEPR, stsPolicy, relyingPartyEPR);
// store the obtained token in token store to be used in future
// communication.
TokenStorage store = TrustUtil.getTokenStore(configCtx);
responseTokenID = responseToken.getId();
store.add(responseToken);
// print token
System.out.println(responseToken.getToken().toString());
...
//Send the token to relying party
if (enableRelyingParty) {
/* Invoke secured service using the obtained token */
OMElement responseElem = null;
// create service client
ServiceClient serClient = new ServiceClient(configCtx, null);
// engage modules
serClient.engageModule("addressing");
serClient.engageModule("rampart");
// load policy of secured service
Policy sec_policy = loadPolicy(relyingPartyPolicyPath);
// add rampart config to the ws-sec policies
sec_policy.addAssertion(rampartConfig);
// set in/out security policies in client opts
serClient.getOptions().setProperty(RampartMessageData.KEY_RAMPART_POLICY,
sec_policy);
// Set the token id as a property in the Axis2 client scope, so that
// this will be picked up when creating the secure message to invoke
// the endpoint.
serClient.getOptions().setProperty(RampartMessageData.KEY_CUSTOM_ISSUED_TOKEN,
responseTokenID);
// set action of the Hello Service to be invoked.
serClient.getOptions().setAction("urn:echoString");
serClient.getOptions().setTo(new EndpointReference(relyingPartyEPR));
// invoke the service
responseElem = serClient.sendReceive(getPayload(echoRequestMsg));
// cleanup transports
serClient.getOptions().setCallTransportCleanup(true);
System.out.println(responseElem.toString());
System.exit(0);
}
} catch (IOException e) {
e.printStackTrace();
} catch (TrustException e) {
e.printStackTrace();
} catch (XMLStreamException e) {
e.printStackTrace();
}
}
private OMElement getRSTTemplate() throws TrustException {
OMFactory omFac = OMAbstractFactory.getOMFactory();
OMElement element = omFac.createOMElement(SP11Constants.REQUEST_SECURITY_TOKEN_TEMPLATE);
if (ClientConstants.SAML_TOKEN_TYPE_20.equals(tokenType)) {
TrustUtil.createTokenTypeElement(RahasConstants.VERSION_05_02, element).setText(
RahasConstants.TOK_TYPE_SAML_20);
} else if (ClientConstants.SAML_TOKEN_TYPE_11.equals(tokenType)) {
TrustUtil.createTokenTypeElement(RahasConstants.VERSION_05_02, element).setText(
RahasConstants.TOK_TYPE_SAML_10);
}
if (ClientConstants.SUBJECT_CONFIRMATION_BEARER.equals(subjectConfirmationMethod)) {
TrustUtil.createKeyTypeElement(RahasConstants.VERSION_05_02, element,
RahasConstants.KEY_TYPE_BEARER);
} else if (ClientConstants.SUBJECT_CONFIRMATION_HOLDER_OF_KEY
.equals(subjectConfirmationMethod)) {
TrustUtil.createKeyTypeElement(RahasConstants.VERSION_05_02, element,
RahasConstants.KEY_TYPE_SYMM_KEY);
}
// request claims in the token.
OMElement claimElement = TrustUtil.createClaims(RahasConstants.VERSION_05_02, element,claimDialect);
// Populate the <Claims/> element with the <ClaimType/> elements
addClaimType(claimElement, claimUris);
return element;
}
private void addClaimType(OMElement parent, String[] claimUris) {
OMElement element = null;
// For each and every claim uri, create an <ClaimType/> elem
for (String attr : claimUris) {
element = parent.getOMFactory()
.createOMElement(
new QName("http://schemas.xmlsoap.org/ws/2005/05/identity",
"ClaimType", "wsid"), parent);
element.addAttribute(parent.getOMFactory().createOMAttribute("Uri", null, attr));
}
}
private Policy loadPolicy(String policyPath) throws XMLStreamException, FileNotFoundException {
StAXOMBuilder omBuilder = new StAXOMBuilder(policyPath);
return PolicyEngine.getPolicy(omBuilder.getDocumentElement());
}
private RampartConfig buildRampartConfig() {
RampartConfig rampartConfig = new RampartConfig();
rampartConfig.setUser(username);
rampartConfig.setEncryptionUser(encryptionUser);
rampartConfig.setUserCertAlias(userCertAlias);
rampartConfig.setPwCbClass(pwdCallbackClass);
Properties cryptoProperties = new Properties();
cryptoProperties.put("org.apache.ws.security.crypto.merlin.keystore.type", "JKS");
cryptoProperties.put("org.apache.ws.security.crypto.merlin.file", keystorePath);
cryptoProperties
.put("org.apache.ws.security.crypto.merlin.keystore.password", keystorePwd);
CryptoConfig cryptoConfig = new CryptoConfig();
cryptoConfig.setProvider("org.apache.ws.security.components.crypto.Merlin");
cryptoConfig.setProp(cryptoProperties);
rampartConfig.setEncrCryptoConfig(cryptoConfig);
rampartConfig.setSigCryptoConfig(cryptoConfig);
return rampartConfig;
}
private OMElement getPayload(String value) {
OMFactory factory = null;
OMNamespace ns = null;
OMElement elem = null;
OMElement childElem = null;
factory = OMAbstractFactory.getOMFactory();
ns = factory.createOMNamespace("http://echo.services.core.carbon.wso2.org", "ns");
elem = factory.createOMElement("echoString", ns);
childElem = factory.createOMElement("in", null);
childElem.setText(value);
elem.addChild(childElem);
return elem;
}
...
}
PasswordCBHandler.java is used by the underlying Rampart module to get the password of the key alias which is used to sign the request.
public class PasswordCBHandler implements CallbackHandler{
...
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
readUsernamePasswordFromProperties();
WSPasswordCallback pwcb = (WSPasswordCallback) callbacks[0];
String id = pwcb.getIdentifier();
int usage = pwcb.getUsage();
if (usage == WSPasswordCallback.USERNAME_TOKEN) {
if (username.equals(id)) {
pwcb.setPassword(password);
}
} else if (usage == WSPasswordCallback.SIGNATURE || usage == WSPasswordCallback.DECRYPT) {
if (keyAlias.equals(id)) {
pwcb.setPassword(keyPassword);
}
}
}
...
}