I have to use oAuth2 "modern authentication" in Java Spring Context and found here. Java code I could use.
The getAccessToken() method has however a parameter of type ManagedExecutorService and I don't have any idea, what kind of value it should be.
Here is the code:
#Override
public String getAccessToken(final String clientId, final String clientSecret, final ManagedExecutorService service)
throws MalformedURLException, ExecutionException, InterruptedException, TimeoutException {
final long now = System.currentTimeMillis();
if (accessToken != null && now < expiryTimeMs - REFRESH_BEFORE_EXPIRY_MS) {
final AuthenticationContext context = new AuthenticationContext(AUTHORITY, false, service);
final AuthenticationCallback<AuthenticationResult> callback = new AuthenticationCallback<>() {
#Override
public void onSuccess(final AuthenticationResult result) {
log.info("received token");
}
#Override
public void onFailure(final Throwable exc) {
throw new RuntimeException(exc);
}
};
log.info("requesting token");
final Future<AuthenticationResult> future = context.acquireToken(RESOUCE,
new ClientCredential(clientId, clientSecret), callback);
// wait for access token
final AuthenticationResult result = future.get(30, TimeUnit.SECONDS);
// cache token and expiration
accessToken = result.getAccessToken();
expiryTimeMs = result.getExpiresAfter();
}
return accessToken;
}
I put already all necessary dependencies into the project but can't test the method until I know what ManagedExecutorServiceis and where do I get the parameter from.
Why not using one of Spring REST clients which can handle OAuth2 out of the box?
All of #FeignClient, RestTemplate and WebClient expose configuration properties for acquiring access-token from OAuth2 authorization-server using client-credentials (or directly set authorization header if you are processing an authorized request and want to forward incoming access-token).
Choose one of existing clients and read its manual.
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'm building a 3rd party app to authenticate with Contact Center Express. The documentation is necessary, but insufficient to accomplish this. For example,
https://developer.cisco.com/docs/contact-center-express/#!cisco-identity-service-client-sdk-guide/during-agent-login
// Get Access Token for the received Authorization Code
String redirectURI = config.getRedirectUri();
AccessToken token = client.getAccessToken(authCode, redirectURI);
When and where do you redirect the user to Contact Center to authenticate? I observed that Finesse will redirect the user to
https://contactcenter.example.com:8553/ids/v1/oauth/authorize?redirect_uri=https%3A%2F%2Ffinesse.example.com%3A443%2Fdesktop%2Fsso%2Fauthcode&client_id=8a75xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&state=aHR0cHM6Ly92bS1mLWZpbi1hLmRldi5pbi5zcGluc2NpLmNvbS9kZXNrdG9wL2pfaWRlbnRpdHlfY2hlY2s%2FZXJyb3I9dHJ1ZQlhcHBsb2dpbg%3D%3D&response_type=code
But where is it specified to use the identity service (IDS) path /ids/v1/oauth/authorize? And is state a required parameter? And does the IDS SDK handle the callback path /desktop/sso/authcode? I imagine that it doesn't but what are the parameters that will be sent to it? I'm using Spring framework.
Am I to reverse engineer the whole process, or is there additional documentation that I am missing?
Even after I receive an OAuth token, how would I use it to make other REST calls to other Cisco products? The Finesse REST APIs only mention HTTP basic authentication. There is no mention of headers for "Authorization: Bearer" tokens.
https://developer.cisco.com/docs/finesse/#!sign-in-to-finesse/sign-in-to-finesse
I had to reverse engineer it following all the redirects.
#Controller
public class SSOController {
#Autowired
private IdSClientConfigurationImpl config;
#Autowired
private IdSClient client;
#PostMapping("/login")
public String login(#RequestParam(name="user", required=true) String user) {
// redirect the user to the Cisco Contact Center Express Identity Service
String redirectURI = config.getRedirectUri();
String clientId = config.getClientId();
URI uri = UriComponentsBuilder
.fromUriString("https://contact-center-express:8553/ids/v1/oauth/authorize")
.queryParam("redirect_uri", "{redirect_uri}")
.queryParam("client_id", "{client_id}")
// .queryParam("state", "{state}") // base64 encoded
.queryParam("response_type", "code")
.build(redirectURI, clientId);
return "redirect:"+uri.toString();
}
#GetMapping("/idscallback")
public String idscallback(
#RequestParam(name="code", required=true) String code,
#RequestParam(name="state", required=false) String state,
HttpSession session) throws IdSClientException {
// Get Access Token for the received Authorization Code
String redirectURI = config.getRedirectUri();
AccessToken token = client.getAccessToken(code, redirectURI); // why do I need redirectURI when it's already redirected?
String accessTokenString = token.getAccess_token();
session.setAttribute("token", accessTokenString);
// model.addAttribute("token", accessTokenString);
return "redirect:/";
}
And in a bean far, far away...
#Bean
public IdSClientConfigurationImpl config() throws IOException, IdSClientException {
ClassPathResource idsclientResource = new ClassPathResource("idsclient.properties");
IdSClientConfigurationImpl config = new IdSClientConfigurationImpl(idsclientResource.getFile().getPath());
// IdSClientConfigurationImpl config = new IdSClientConfigurationImpl("src/main/resources/idsclient.properties");
config.load();
return config;
}
#Bean
public IdSClient setupIdsClient() throws IOException, IdSClientException {
IdSClient client = IdSClientFactory.getIdSClient();
client.setTLSContext(createSSLTrustManager(), createHostnameVerifier());
// client.setTLSContext(arg0, arg1) // use secure trust manager and hostname verifier in production
client.init(config);
return client;
}
private X509TrustManager createSSLTrustManager() {
X509TrustManager tm = new TrustAllX509TrustManager();
return tm;
}
private HostnameVerifier createHostnameVerifier() {
HostnameVerifier hv = new SkipAllHostNameVerifier();
return hv;
}
I'm building an application that needs access to our clients' Office 365 Management Activities. I've followed the steps outlined in this Azure Active Directory overview, and am able to use the OAuth code to acquire an initial Access Token, as well as use this token to set up O365 subscriptions.
However, when I use the refresh_token provided with my initial token to acquire a new Access Token, I get the following error:
{"error_description":"AADSTS65001: The user or administrator has not consented to use the application with ID '8f72f805-dfd2-428d-8b0e-771a98d26c16'. Send an interactive authorization request for this user and resource.\r\nTrace ID: df229c3f-8f28-420b-9ac3-321ab1b2ad09\r\nCorrelation ID: 0e0f2bcb-4b19-458a-8556-2a6d4e51379f\r\nTimestamp: 2016-10-03 17:33:20Z","error":"invalid_grant"}
Since I'm able to acquire and use the initial Access Token, I'm pretty sure that the user is granting my applications some permissions. Is there a specific permission that I need in order to acquire a new Access Token using the Refresh Token?
Edit:
Specifically, I'm using the com.microsoft.azure::adal4j java package, AuthenticationContext class, acquireTokenByAuthorizationCode and acquireTokenByRefreshToken methods:
public class AzureProvisioner {
private final AuthenticationContext authService = new AuthenticationContext(
"https://login.windows.net/common/oauth2/token", true, Executors.newSingleThreadExecutor());
private final ClientCredential clientCredential = new ClientCredential("azureAppId", "azureAppSecret");
public static final String resource = "https://manage.office.com";
// Internal implementation of REST interface; Microsoft didn't provide a Java Library
final Office365ManagementApi managementApi;
public void acquireToken(final String authCode, final URI redirectUri) {
final AuthenticationResult authResult = authService.acquireTokenByAuthorizationCode(
authCode, redirectUri, clientCredential, resource, null).get()
// internal library code, gets the "tid" field from parsing the JWT token
final String tenantId = JwtAccessToken.fromToken(authResult.getAccessToken()).getTid();
// works
createInitialSubscription(customerId, authResult.getAccessToken(), tenantId);
// throws an error
final AuthenticationResult refreshResult = authService.acquireTokenByRefreshToken(
authResult.getRefreshToken(), clientCredential, null).get();
}
private void createInitialSubscription(final String accessToken, final String tenantId) {
final String authHeader = "Authorization: Bearer " + accessToken;
final String contentType = "Audit.AzureActiveDirectory";
// internal implementation
final CreateWebhookRequest requestBody = new CreateWebhookRequest();
managementApi.createSubscription(authHeader, tenantId, contentType, requestBody);
}
}
The same code, without any external dependencies, also does not work for me:
public class AzureProvisioner {
private final AuthenticationContext authService = new AuthenticationContext(
"https://login.windows.net/common/oauth2/token", true, Executors.newSingleThreadExecutor());
private final ClientCredential clientCredential = new ClientCredential("8f72f805-dfd2-428d-8b0e-771a98d26c16", "secret");
public final String resource = "https://manage.office.com";
private URI redirectUri = new URI("https://localhost");
private static final String oAuthUrl = "https://login.windows.net/common/oauth2/authorize?response_type=code&client_id=8f72f805-dfd2-428d-8b0e-771a98d26c16&resource=https%3A%2F%2Fmanage.office.com&redirect_uri=https%3A%2F%2Flocalhost";
public AzureProvisioner() throws Exception {
// do nothing
}
public static void main(String... args) throws Exception {
final String authCode = "AQABAAAAAADRNYRQ3dhRSrm...";
new AzureProvisioner().acquireToken(authCode);
}
public void acquireToken(final String authCode) throws Exception {
final AuthenticationResult authResult = authService.acquireTokenByAuthorizationCode(
authCode, redirectUri, clientCredential, resource, null).get();
System.out.println(authResult.getAccessToken());
// throws an error
final AuthenticationResult refreshResult = authService.acquireTokenByRefreshToken(
authResult.getRefreshToken(), clientCredential, resource, null).get();
System.out.println(refreshResult.getAccessToken());
}
}
Using a proxy, I took a trace of the https refresh request:
Method: POST
Protocol-Version: HTTP/1.1
Protocol: https
Host: login.windows.net
File: /common/oauth2/token
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-Length: 876
refresh_token={token}
&resource=https%3A%2F%2Fmanage.office.com
&grant_type=refresh_token
&scope=openid
&client_secret={secret}
&client_id=8f72f805-dfd2-428d-8b0e-771a98d26c16
It turns out that the root issue was with my application permissions. Under My Application > Settings > Required Permissions > Office 365 Management APIs, I had selected the "Application Permissions", where I needed to select the "Delegated Permissions". Swapping those over, my code immediately started working as expected.
ADAL uses the stored refresh tokens automatically and transparently, you aren't required to perform any explicit action. AcquireTOkenByRefreshToken is in the ADAL surface for legacy reasons, and has been removed from version 3.x. More background at http://www.cloudidentity.com/blog/2015/08/13/adal-3-didnt-return-refresh-tokens-for-5-months-and-nobody-noticed/
I'm trying to obtain a list of a user's tweets and I've run into some trouble when trying to authenticate my call to the API. I currently get a 401 when executing the code below:
public interface TwitterApi {
String API_URL = "https://api.twitter.com/1.1";
String CONSUMER_KEY = "<CONSUMER KEY GOES HERE>";
String CONSUMER_SECRET = "<CONSUMER SECRET GOES HERE>";
String ACCESS_TOKEN = "<ACCESS TOKEN GOES HERE>";
String ACCESS_TOKEN_SECRET = "<ACCESS TOKEN SECRET GOES HERE>";
#GET("/statuses/user_timeline.json")
List<Tweet> fetchUserTimeline(
#Query("count") final int count,
#Query("screen_name") final String screenName);
}
The following throws a 401 Authorisation error when calling fetchUserTimeline()
RetrofitHttpOAuthConsumer consumer = new RetrofitHttpOAuthConsumer(TwitterApi.CONSUMER_KEY, TwitterApi.CONSUMER_SECRET);
consumer.setTokenWithSecret(TwitterApi.ACCESS_TOKEN, TwitterApi.ACCESS_TOKEN_SECRET);
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(TwitterApi.API_URL)
.setClient(new SigningOkClient(consumer))
.build();
TwitterApi twitterApi = restAdapter.create(TwitterApi.class)
tweets = twitterApi.fetchUserTimeline(2, screenName);
I've also included the relevant code from the signpost-retrofit plugin:
public class SigningOkClient extends OkClient {
private final RetrofitHttpOAuthConsumer mOAuthConsumer;
public SigningOkClient(RetrofitHttpOAuthConsumer consumer) {
mOAuthConsumer = consumer;
}
public SigningOkClient(OkHttpClient client, RetrofitHttpOAuthConsumer consumer) {
super(client);
mOAuthConsumer = consumer;
}
#Override
public Response execute(Request request) throws IOException {
Request requestToSend = request;
try {
HttpRequestAdapter signedAdapter = (HttpRequestAdapter) mOAuthConsumer.sign(request);
requestToSend = (Request) signedAdapter.unwrap();
} catch (OAuthMessageSignerException | OAuthExpectationFailedException | OAuthCommunicationException e) {
// Fail to sign, ignore
e.printStackTrace();
}
return super.execute(requestToSend);
}
}
The signpost-retrofit plugin can be found here: https://github.com/pakerfeldt/signpost-retrofit
public class RetrofitHttpOAuthConsumer extends AbstractOAuthConsumer {
private static final long serialVersionUID = 1L;
public RetrofitHttpOAuthConsumer(String consumerKey, String consumerSecret) {
super(consumerKey, consumerSecret);
}
#Override
protected HttpRequest wrap(Object request) {
if (!(request instanceof retrofit.client.Request)) {
throw new IllegalArgumentException("This consumer expects requests of type " + retrofit.client.Request.class.getCanonicalName());
}
return new HttpRequestAdapter((Request) request);
}
}
Any help here would be great. The solution doesn't have to include the use of signpost but I do want to use Retrofit. I also do not want to show the user an 'Authenticate with Twitter' screen in a WebView - I simply want to display a handful of relevant tweets as part of a detail view.
Are you certain the signpost-retrofit project works for twitter oauth? I've used twitter4j successfully in the past - and if you don't want the full library you can use their code for reference. twitter4j
I want to develop a SOAP client using CXF to connect to SharePoint. The authentication scheme is NTLM.
I am blocked on a scenario where the logged-in user of a machine (on which the SOAP client is being run) has access to SharePoint. The CXF soap client always uses the logged-in user. I want to specify some other user credentials (not the logged-in).
As CXF uses in-JDK HttpURLConnection; and what I have read about HttpURLConnection is, it bypasses the specified credentials when the logged-in user is NTLM authenticated.
Codes were tried on CXF version 2.7.11.
Solutions that I have tried out:
1) Setting Conduit authorization
String username = "user";
String password = "password";
JaxWsProxyfactoryBean factory1 = new JaxWsProxyfactoryBean();
factory1.setServiceClass(WebsSoap.class);
factory1.setAddress(url);
factory1.setUsername(username);
factory1.setPassword(password);
WebsSoap service = (WebsSoap) factory1.create();
Client client = ClientProxy.getClient(service);
HTTPconduit conduit = (HTTPconduit) client.getconduit();
conduit.getAuthorization().setAuthorizationType("NTLM");
conduit.getAuthorization().setUserName(username);
conduit.getAuthorization().setPassword(password);
HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy();
httpClientPolicy.setConnectionTimeout(36000);
httpClientPolicy.setAllowChunking(false);
conduit.setClient(httpClientPolicy);
service.getWeb(".");
Problem:
This does not work for the scenario specified above, as it always uses the logged-in credentials. And when I specify invalid credentials, it does not fail.
2) AsyncHTTPConduit
Another solution is to use AsyncHTTPConduit that uses HttpAsyncClient instead of HttpURLConnection. This is beacuse HTTP components do not bypass specified credentials and logged-in user can be ignored (I have successfully verified this with a test client using HttpClient).
Below is the code snippet::
Bus bus = BusFactory.getDefaultBus();
bus.setProperty( "use.async.http.conduit", "true" );
Client client = ClientProxy.getClient( service );
HTTPConduit http = (HTTPConduit)client.getConduit();
if ( http instanceof AsyncHTTPConduit ) {
AsyncHTTPConduit conduit = (AsyncHTTPConduit)http;
DefaultHttpAsyncClient defaultHttpAsyncClient;
try {
defaultHttpAsyncClient = conduit.getHttpAsyncClient();
}
catch ( IOException exception ) {
throw new RuntimeException( exception );
}
defaultHttpAsyncClient.getCredentialsProvider().setCredentials( AuthScope.ANY,
new NTCredentials( "username", "password", "", "domain" ) );
conduit.getClient().setAllowChunking( false );
conduit.getClient().setAutoRedirect( true );
}
Problem:
Above code throws error:
Authorization loop detected on conduit.
The above code snapshot shows the usage of DefaultHttpAsyncClient which is deprecated now and CloseableHttpAsyncClient is to be used instead. But CloseableHttpAsyncClient does not provide a way to specify credentials to an already existing CloseableHttpAsyncClient object. Not sure how to use CloseableHttpAsyncClient in this scenario.
3) Other solutions
The other solution that I tried out is to use sun.net.www.protocol.http.ntlm.NTLMAuthenticationCallback, to bypass logged-in user authentication, as mentioned here. Use this approach along with solution #1 mentioned above. This works as expected for valid/invalid credentials, and the code bypasses the logged-in credentials :). But when I specify invalid credentials, I do not get HTTP 401 error, instead I get
Could not send message, server reached max retries 20
I am trying to avoid this solution because it uses java’s internal package and there is no way to determine HTTP 401 error directly.
What can I do to arrive at a complete solution?
Try this interceptor. This will avoid automatic authentication.
public class DisableAutomaticNTLMAuthOutInterceptor extends AbstractPhaseInterceptor<Message>
{
private boolean isFieldsAvailable;
private Field tryTransparentNTLMProxyField;
private Field tryTransparentNTLMServerField;
public DisableAutomaticNTLMAuthOutInterceptor() {
super(Phase.PRE_STREAM);
AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Void run() {
try {
DisableAutomaticNTLMAuthOutInterceptor.this.tryTransparentNTLMServerField = HttpURLConnection.class.getDeclaredField("tryTransparentNTLMServer");
DisableAutomaticNTLMAuthOutInterceptor.this.tryTransparentNTLMServerField.setAccessible(true);
DisableAutomaticNTLMAuthOutInterceptor.this.tryTransparentNTLMProxyField = HttpURLConnection.class.getDeclaredField("tryTransparentNTLMProxy");
DisableAutomaticNTLMAuthOutInterceptor.this.tryTransparentNTLMProxyField.setAccessible(true);
DisableAutomaticNTLMAuthOutInterceptor.this.isFieldsAvailable = true;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
});
}
#Override
public void handleMessage(final Message message) throws Fault {
if (this.isFieldsAvailable)
AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Void run() {
try {
Object httpConnection = message.get("http.connection");
if (httpConnection != null) {
DisableAutomaticNTLMAuthOutInterceptor.this.processHttpConnection(message.get("http.connection"));
}
} catch (Throwable t) {
t.printStackTrace();
}
return null;
}
});
}
private void processHttpConnection(Object httpConnection) throws IllegalArgumentException, IllegalAccessException {
if (HttpURLConnection.class.isAssignableFrom(httpConnection.getClass())) {
tryTransparentNTLMServerField.set(httpConnection, Boolean.FALSE);
tryTransparentNTLMProxyField.set(httpConnection, Boolean.FALSE);
} else {
Field tempField = null;
for (Field field : httpConnection.getClass().getDeclaredFields()) {
if (HttpURLConnection.class.isAssignableFrom(field.getType())) {
field.setAccessible(true);
tempField = field;
break;
}
}
if (tempField != null) {
processHttpConnection(tempField.get(httpConnection));
}
}
}
}