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/
Related
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.
I'm trying to make a Slack App, using Bolt library.
I made a simple Hello World application according to official docs: https://api.slack.com/start/building/bolt-java
Credentials I inject from properties file, that's what my SlackApp class lookalike:
#Configuration
public class SlackApp {
private final String secret;
private final String token;
public SlackApp(#Value("${slack.signing-secret}") String secret,
#Value("${slack.bot-token}") String token) {
this.secret = secret;
this.token = token;
}
#Bean
public App initSlackApp() {
AppConfig config = AppConfig.builder()
.signingSecret(secret)
.singleTeamBotToken(token)
.build();
App app = new App(config);
app.command("/hello", (req, ctx) -> {
return ctx.ack("What's up?");
});
return app;
}
}
Token and secret, I've obtained like in this part of docs: https://slack.dev/java-slack-sdk/guides/getting-started-with-bolt
But when I try to turn on Event Subscription in Slack API I got Your URL didn't respond with the value of the challenge parameter. error.
And in application logs I've got
2021-11-30 17:27:08.364 DEBUG 81561 --- [nio-3000-exec-4] c.s.a.a.SlackSignature$Verifier : Request verification (timestamp: 1638278828, body: {"token":"VERIFICATION_TOKEN","challenge":"CHALLENGE","type":"url_verification"}, signature: v0=SIGNATURE)
2021-11-30 17:27:08.366 INFO 81561 --- [nio-3000-exec-4] c.s.a.b.m.builtin.RequestVerification : Invalid signature detected - v0=SIGNATURE
What can be reason of invalid signature error, if both signing secret and bot token is correct?
I have an application A that has an endpoint which signs the response and puts the signature in a header. The header looks like:
X-Algorithm: SHA256withRSA
X-Signature: Zca8Myv4......PkH1E25hA=
When I call the application directly I see the headers.
I build application B and it's calling A via a proxy P.
Application B has an OkHttp client which sends the request and reads the response. I have a custom interceptor:
#Slf4j
public class SignatureValidatorInterceptor implements Interceptor {
private final Signature signer;
public SignatureValidatorInterceptor(final PublicKey publicKey) {
this.signer = getSigner(publicKey);
}
private static final String ALGORITHM_HEADER = "x-algorithm";
private static final String SIGNATURE_HEADER = "x-signature";
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
/**
* This interceptor verifies the signature of the response
*/
#Override
public Response intercept(final Interceptor.Chain chain) throws IOException {
final Response response = chain.proceed(chain.request());
final Headers headers = response.headers();
log.info("Received response for url={}} with headers \n{}", response.request().url(), headers);
final byte[] responseBodyBytes = response.peekBody(1000).bytes();
final String algorithmHeader = headers.get(ALGORITHM_HEADER);
final String signatureHeader = headers.get(SIGNATURE_HEADER);
if (StringUtils.isBlank(signatureHeader) || StringUtils.isBlank(algorithmHeader)) {
throw new Exception("No signature or algorithm header on response");
}
this.verifySignature(responseBodyBytes, algorithmHeader, signatureHeader);
return response;
}
private void verifySignature(final byte[] responseBodyBytes, final String algorithmHeader, final String signatureHeader) {
//code to validate signature
}
private Signature getSigner(final PublicKey publicKey) {
//code to create signer
}
}
The debug line logs the headers it receives. I see multiple standard http headers in my logs but I'm missing my custom headers!
I have no clue why. I first thought it was a network thing. But on doing a curl from the machine of B to application A, I see the headers are there. Also I had the proxy log the headers for me, and I can see they passed.
All applications are standard spring-boot java applications. Running on linux VM's.
What am I missing?
Thanks,
Rick
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;
}
To find the connection, do we have the cookies in the response or some connection id, authentication token
In the newest Java REST API for Rally, the RallyRestApi class grabs a Rally SecurityToken via the protected attachSecurityInfo() method. You can review the source code at the link above starting at line 377 to see how attachSecurityInfo requests the token, but the crux of it is outlined here:
protected static final String SECURITY_TOKEN_PARAM_KEY = "key";
private static final String SECURITY_TOKEN_URL = "/security/authorize";
protected static final String SECURITY_TOKEN_KEY = "SecurityToken";
GetResponse getResponse = getWithForceReauth(new GetRequest(SECURITY_TOKEN_URL));
JsonObject operationResult = getResponse.getObject();
JsonPrimitive securityTokenPrimitive = operationResult.getAsJsonPrimitive(SECURITY_TOKEN_KEY);
securityToken = securityTokenPrimitive.getAsString();
public GetResponse getWithForceReauth(GetRequest request) throws IOException {
return get(request, true);
}
Note that this only works for the Java REST package version 1.07 or higher, and requires (and is required by) Rally Webservices API 1.42 or higher.