How to use the on-behalf-of flow to call Microsoft Graph from Spring resource server - spring

I'm trying to complete a fairly simple process:
A web application, authenticated with Azure AD via a personal Microsoft account (i.e. an #hotmail account), makes a call to a second microservice.
The microservice is secured via a JWT, obtains a second token using the on-behlaf-of flow, and called the Microsoft Graph API to retrieve calendar events.
I can log into the web frontend OK, and call the second microservice OK. The second microservice can obtain an on-behalf-of (obo) token OK, but the problem I run into is that the obo access token provided to me fails to call the Graph API. The error I receive is this:
{"error":{"code":"NoPermissionsInAccessToken","message":"The token contains no permissions, or permissions can not be
understood.","innerError":{"oAuthEventOperationId":"7499efa7-932b-425d-8ad4-43206630f961","oAuthEventcV":"ed1BzGz2t/H/wK7JnZB6lQ.1.1.1","errorUrl":"https://aka.ms/autherrors#error-InvalidGrant","requestId":"3ca5fa79-423b-460e-9130-b7d1172ec841","date":"2021-09-13T09:24:17"}}}
My problem is similar to the one described here, where my obo JWT does not contains a roles claim. This is the decoded JWT sent to the Graph API:
{
"typ": "JWT",
"nonce": "A3IzBCOGrnE53ukPqb2jHjWYT0grwFbHo_OkzcUhSRc",
"alg": "RS256",
"x5t": "l3sQ-50cCH4xBVZLHTGwnSR7680",
"kid": "l3sQ-50cCH4xBVZLHTGwnSR7680"
}.{
"aud": "https://graph.microsoft.com",
"iss": "https://sts.windows.net/26375159-666a-4217-adfe-c06427b7798c/",
"iat": 1631522998,
"nbf": 1631522998,
"exp": 1631526898,
"acct": 1,
"acr": "1",
"aio": "AUQAu/8TAAAA1HEurZSE5YS0ADYs3oKeEEy6qhWwyXBZtoHtBPbIS/jo0OD5BTlQptuXZ3ZLtDczWZQw7b0+dUoCNdpN2mY4ew==",
"altsecid": "1:live.com:00014B90C0D373BC",
"amr": [
"pwd"
],
"app_displayname": "SpringBootMicroserviceDemo",
"appid": "5907efc0-f0b5-45db-b4cf-725f655009c3",
"appidacr": "1",
"email": "matthewcasperson#hotmail.com",
"family_name": "Casperson",
"given_name": "Matthew",
"idp": "live.com",
"idtyp": "user",
"ipaddr": "45.132.224.55",
"name": "matthewcasperson",
"oid": "e377a23b-1b88-4d14-9c99-fc6ecd4a41c7",
"platf": "3",
"puid": "1003200180F4FB20",
"rh": "0.AVAAWVE3JmpmF0Kt_sBkJ7d5jMDvB1m18NtFtM9yX2VQCcNQAKM.",
"scp": "Calendars.Read openid profile User.Read email",
"signin_state": [
"kmsi"
],
"sub": "vhbBIoJqEHoEaJLMVsG5sh0C4FjoJiAfAKOFCzrC8hQ",
"tenant_region_scope": "NA",
"tid": "26375159-666a-4217-adfe-c06427b7798c",
"unique_name": "live.com#matthewcasperson#hotmail.com",
"uti": "ufe_nSFv_UuiYCOur72mAQ",
"ver": "1.0",
"wids": [
"13bd1c72-6f4a-4dcf-985f-18d3b80f208a"
],
"xms_st": {
"sub": "PCUskXV6aCsNgHz9Yug42-WhS-iea1gy5GI5trkTZ4E"
},
"xms_tcdt": 1631479794
}.[Signature]
The JWT has Calendars.Read in the scp, but no roles, which appears to be an issue.
The JWT above will complete a call to the "me" endpoint OK (i.e. https://graph.microsoft.com/v1.0/me). Unfortunately, calling the "me" endpoint is as far as any example provided by Microsoft goes.
The sample application here provides a controller that shows calling the "me" endpoint on the Graph API, and if I modify the Graph API client to include the https://graph.microsoft.com/Calendars.Read scope, it also fails the Graph API request.
The sample application here appears to be a slightly older example in that it builds the token manually rather than injecting something like #RegisteredOAuth2AuthorizedClient("graph") OAuth2AuthorizedClient client (which is described in more detail on the Spring blog here). But again the sample only goes so far as to call the "me" endpoint.
As far as I'm aware I have enabled all the correct settings. My Azure AD Application has requested Calendars.Read both as a delegated and application permission, and the permissions have been granted consent:
My web based application can be found here, and the Calendar API microservice can be found here.
Further reading:
Azure AD v2 roles not included in Access Token: This described how to add custom roles to the JWT, but I did not see how it could be used to add Graph API roles.
Graph API access token for calendar read reports No Permissions: This talked about adding application permissions, which did not resolve the issue for me.
Microsoft Graph API outlook task folder : NoPermissionsInAccessToken: This issue talks about different OAuth flows, but this doesn't appear to apply to a resource server that accepts a JWT.
How to solve "NoPermissionsInAccessToken" returned by Microsoft Graph when reading calendar or sending mail: The answer here talks about "Adding guest users is meaningless", which makes no sense to me.
Spring Boot Starter for Azure Active Directory developer's guide: This is a reasonably detailed tutorial that even speaks about resource servers, but I could not find any tips that addressed the NoPermissionsInAccessToken error.
OAuth 2.0 Sample for Azure AD Spring Boot Starter client library for Java - This is another reasonably detailed tutorial that talks about accessing a second resource server, but again I didn't find any clues to the NoPermissionsInAccessToken error.
List events and Get calendar: This indicates that a Delegated (personal Microsoft account) can use the Calendars.Read permission.
The crux of my issue is that using the on-behalf-of flow appears to be the recommended solution for calling the Graph API from a resource server that accepts a JWT from a front end application. Defining the delegated permissions in the Azure AD Application, consenting to them, defining them in an authorization-clients: in the application.yaml file, and getting a client via #RegisteredOAuth2AuthorizedClient("graph") OAuth2AuthorizedClient client are the only consistent instructions from the Microsoft documentation and sample applications. And yet the resulting JWT can only call the "me" endpoint.
Can anyone shed some light on how to call the Graph API with on-behalf-of token in Spring boot?

Related

ACCESS_TOKEN_SCOPE_INSUFFICIENT when trying to access Google Classroom course announcements

My user account can use the Google Classroom web UI to see all the announcements for a given course. Trying to pull them programmatically using the Google Classroom API.
I've set up an app with Oauth consent screen covering (for test purposes) ALL the scopes listed under the Google Classroom API, and can run the consent flow with my user account to get an access token.
I can successfully GET course details by curl'ing https://classroom.googleapis.com/v1/courses/<my course ID> using the access token obtained from the oauth flow. However, when I GET https://classroom.googleapis.com/v1/courses/<my course ID>/announcements with the same token, I get the following:
{
"error": {
"code": 403,
"message": "Request had insufficient authentication scopes.",
"status": "PERMISSION_DENIED",
"details": [
{
"#type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "ACCESS_TOKEN_SCOPE_INSUFFICIENT",
"domain": "googleapis.com",
"metadata": {
"method": "google.classroom.v1.Work.ListAnnouncements",
"service": "classroom.googleapis.com"
}
}
]
}
}
Behaviour is same in using both client libraries as well as raw REST calls.
Am I missing an auth scope (I switched them all on), am I just not allowed to do this since I'm not the course owner, or am I doing something else wrong? Advice please!
You apear to be using the courses.get method
In order to access this method your application needs to be authorized with one of the following scopes.
You also appear to be using the courses.announcements.list method
This method requires that your application be authorized with one of the following scopes.
The error message "Request had insufficient authentication scopes." means exactly what it says. The access token you are using was not authorized with one of the scopes needed for the courses.announcements.list endpoint there for you can not use it.
You need to delete the access token you have now and request authorization of the user using the proper scope for this method. Always make sure to delete your old token. When changing scopes in code your app does not always request authorization again if you just change the scopes in the code. You need to force it to request authorization again.
All the scopes
you should not be requesting all of the scopes of the user only the scopes that you need. If you only need readonly access make sure not to request write.
I had forgotten that my code explicitly defines the scopes when configuring the client that then builds the oauth request URL:
config, err = google.ConfigFromJSON(b, classroom.ClassroomCoursesReadonlyScope)
Changed to
config, err = google.ConfigFromJSON(b, classroom.ClassroomCoursesReadonlyScope, **classroom.ClassroomAnnouncementsReadonlyScope**)
and it works fine.

Try this API for Logging API returns "PERMISSION_DENIED"

I am trying to test the Google Cloud Logging API on the "Try this API" feature that Google Cloud Platform has on their documentation, but I get this response back:
{
"error": {
"code": 403,
"message": "The caller does not have permission",
"status": "PERMISSION_DENIED"
}
}
I know that my response body is correct because it works with OAuth 2.0 but fails when I use API Key.
Auth 2.0:
Working request using OAuth 2.0
API Key:
Non-Working request using API Key
Google docs says that they generate their own API Key for this "Try this API" feature. https://developers.google.com/explorer-help/
Since Google is using their own API Key, I do not understand why I am getting a response status of PERMISSION_DENIED.
Edit:
Here is a link to the Try this API feature in Google Cloud Platform if you would like to give it a try. https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write?apix_params=%7B%22resource%22%3A%7B%22entries%22%3A%5B%7B%22logName%22%3A%22projects%2F%5BPROJECT_ID%5D%2Flogs%2Frequests%22%2C%22resource%22%3A%7B%22type%22%3A%22http_load_balancer%22%2C%22labels%22%3A%7B%7D%7D%7D%5D%7D%7D
Here is the python request that I am using in my code to create an entry:
import requests
entry = {
"entries": [
{
"logName": "projects/[PROJECT_ID]/logs/requests",
"resource": {
"type": "http_load_balancer",
"labels": {}
}
}
]
}
requests.post('https://logging.googleapis.com/v2/entries:write?key=[YOUR_API_KEY]', data=json.dumps(entry))
The API key was created from my user that has "logs writer", "logs viewer", and "logging admin" permissions. This theoretically should be all the permissions I need to make the post request. However, it is still returning a "PERMISSION_DENIED" status.
Any help would be much appreciated. Thank you in advance.
It looks like you are making a request to write data which isn't publicly writable. API Keys have no concept of user, they are only identifying you are allowed to call an API. So it looks like your API key request is working to the extent it can, but the response is telling you: I don't know who you are so I can't let you do this.
OAuth 2.0 is the solution here, as it acts on behalf of your account (you have to give consent), allowing the API to verify you have permission to take this action.
Service accounts are another option, to act on behalf of your project instead of your user, but they aren't practical from a web UI.

How to check caller is part of AD Security group in WebAPI layer?

I have a website (WebApp) from where I make Ajax calls to my WebAPI layer that accepts JWT Bearer Token auth. I have integrated Adal.js into my front end script layer and the config values look like this.
var config = {
instance: "https://login.microsoftonline.com/",
tenant: "tenant.com",
clientId: "CLIENT_ID OF THE PORTAL APP", // Web-Portal app
redirectUri: "http://localhost:8241/",
popUp: true,
cacheLocation: "localStorage" };
The WebApiConfig looks like this:
var webApiConfig = {
resourceId: "CLIENT_ID OF API APP", // Web-API app
resourceBaseAddress: "http://localhost:9020/"
};
It all works fine, I am able to authenticate/authorize and call my webapi via an access token via Implicit oauth flow.
Now I would like to be able to retrieve the User's security group membership values for the logged in user so that at the WebAPI layer, I can make sure the user belongs to a particular security group as part of the authorization logic. So I set the GroupMembershipClaims: "SecurityGroup" in the App's manifest xml in Azure AD (I did this for the WebApi App manifest first, but then also in the WebApp app manifest).
However, looks like GroupMembershipClaims are only included in the idtoken and not the accessToken. And given it is the accessToken that is sent to the WebAPI, I am unable to do this check at that layer. I guess I could make the check at the WebApp layer, but given this WebApi layer will be called from many other frontend apps (which are not owned by me, I am primarily an "API provider"), that is not a secure soln.
So how do I solve this? I guess one way is to use the "on-behalf-of" flow on the WebApi layer to make calls to Graph API to find this out? I am afraid that will require more permissions than available at User scope.
Thanks!
It's a good question.
My immediate first thought was "well you can get them from Graph API", but I see you already thought of that :)
If you want to do that, you can use On-behalf-of like you said, and use this operation on MS Graph API: https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/api/user_getmembergroups.
You would need these delegated permissions: User.Read and Group.Read.All.
So you would need the permission to read all groups in addition to the basic User.Read.
There are a couple other options.
You can define roles in your API.
I wrote an article on how to do this: Defining permission scopes and roles offered by an app in Azure AD.
So if you define a role like this in the API's manifest:
{
"appRoles": [
{
"allowedMemberTypes": [
"User"
],
"displayName": "Administrator",
"id": "179c1dc0-4801-46f3-bc0d-35f059da1415",
"isEnabled": true,
"description": "Administrators can access advanced features.",
"value": "admin"
}
]
}
You'll need to make the id a unique GUID for each role, you can use online generators or PowerShell for that [System.Guid]::NewGuid().
These roles can then be assigned to users, and if you have at least Azure AD Basic, you can assign them to groups.
Roles are included in access tokens, so you can check these quite easily in your API.
The other idea I had was to have the API and front-end use the same registered app in Azure AD.
This way you could pass the Id token to the API instead of an access token, and you'd get the groups.
But since you might have other front-ends too, I doubt this would work for you.

Using Azure Active Directory token in ASP.Net Core Web API with UseJwtBearerAuthenticiation

I have a native client application written in Ionic Framework 3. I have a Web API written in ASP.NET Core 1.1. I want to use Azure Active Directory to manage access to the Web API.
I have registered two applications with Azure Active Directory: Mobile App and Web API. The Mobile App has the required permission of granting access to the Web API. Below are screen shots of the permissions from our Azure Admin Portal:
Mobile App Permissions
This is configured as a Native App in AAD. I have an Application ID and an Object ID given by AAD. Additionally, I added an arbitrary Redirect URI, which I thought based on several tutorials did not need to resolve, that URI is http://mobileCRMApp. Looking at the Properties in AAD, the Home page URL is blank and the Logout URL is blank.
API Permissions
BOLD UPDATED 10/03/2017:
This is configured as a Web App/API in AAD. I have an Application ID and an Object ID given by AAD. Additionally, I set both the Home Page URL and the App ID URI to match the root of my Web API (https://crm.mycompany.com).
My Ionic client application successfully authenticates against AAD roughly in the following way:
authenticate(userID, authCompletedCallback) : any {
let parent = this;
//this.context = new AuthenticationContext(this.config.authority);
let context = this.msAdal.createAuthenticationContext("https://login.microsoftonline.com/myTenantId");
console.log(context);
context.acquireTokenAsync(parent.config.resourceUri, parent.config.clientId, parent.config.redirectUri, userID, "")
.then(authCompletedCallback)
.catch((e: any) => console.log('Authentication failed', e));
}
The login process goes fine in the app, and the callback receives a token, and I can translate its payload using jwt.io into roughly the following:
{
"aud": "https://crm.mycompany.com/",
"iss": "https://sts.windows.net/someID/",
"iat": 1506539211,
"nbf": 1506539211,
"exp": 1506543111,
"acr": "1",
"aio": "someOtherID",
"amr": [
"pwd"
],
"appid": "appID",
"appidacr": "0",
"e_exp": 262800,
"family_name": "Walter",
"given_name": "Philip",
"ipaddr": "someAddress",
"name": "Philip Walter",
"oid": "someOtherID",
"onprem_sid": "someOtherID",
"puid": "stuff",
"scp": "user_impersonation",
"sub": "e_X7WlAoVS2vzXm1pr3kcDOrET7czcC0f8-YRU_2DJ8",
"tenant_region_scope": "NA",
"tid": "ourTenantID",
"unique_name": "pwalter#advtis.com",
"upn": "pwalter#advtis.com",
"uti": "RLvLlibQHESwmujVBBdlAA",
"ver": "1.0"
}
So then I take the token and send it along with an http request to the API from the Ionic client app, roughly like so:
let headers = new Headers();
headers.append('Authorization', 'Bearer ' + this.authToken.accessToken);
let options = new RequestOptions({ headers : headers });
this.data.http.get(url, options).map(res => res.json()).subscribe((data) => {
console.log(data);
});
The API then has the following in Startup.cs
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
Authority = Configuration["Authentication: AzureAd:AADInstance"] + Configuration["Authentication: AzureAd:TenantId"],
Audience = Configuration["Authentication: AzureAd:Audience"]
});
So, I can log in through AAD in the client app, and I receive a token, but I still get a 401 unauthorized response from the web api when I send a request to a route with the [Authorize] tag above it.
I am obviously doing something wrong in configuring the API or the permissions. I put this together using several different tutorials, because I could not find anything that specifically addressed my use case. Any ideas as to what I'm doing wrong, or how I might troubleshoot?
Why does your audience say "aud": "https://graph.windows.net"?
It should say "aud": "https://yourWebApi.azurewebsites.net" or something similar.
The token you included above is only good for the Graph API, any other Azure AD protected resource will refuse it, including your Web API, since it expects the audience to match self.
parent.config.resourceUri is probably where you source the desired audience from:
context.acquireTokenAsync(
parent.config.resourceUri,
...
According to RFC 7519:
The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the "aud" claim when this claim is present, then the JWT MUST be rejected. In the general case, the "aud" value is an array of case- sensitive strings, each containing a StringOrURI value. In the special case when the JWT has one audience, the "aud" value MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience values is generally application specific. Use of this claim is OPTIONAL.
Sometimes it's called resource, sometimes audience, sometimes aud... it is what it is :)

IdentityServer3 Sync users with client app

I have the following setup:
IdenetityServer3 for auth (OAUth / OpenID)
ASP.NET WebApi back end
Ember-cli UI
I have the auth flow working nicely - I haven't managed to get the admin UI working yet but I can prepopulate users, scopes and clients so that's fine.
When the user auths against IdentityServer3 they are redirected back to the UI and the UI uses the oidc-client to retrieve the users info from the JWT - the client also uses the bearer token to send to the API to auth requests - all good.
My problem is that the IddentityServer is in charge of authentication / authorization - but the API doesn't yet have any notion of a user - but it needs that.
What is the best way of syncing user info between IdentityServer and my API? How can I best manage things like roles and user hierarchy? Is there a way for the API to query IdentityServer for this? It seems silly holding a copy of the user info locally to the API when we have an identity server that manages all of this.
IdentityServer exposes a UserInfo endpoint (https://identityserver.github.io/Documentation/docsv2/endpoints/userinfo.html) which you can call to retrieve additional information about a user.
However, wherever possible, try to achieve what you need to by passing a token that has the relevant amount of claims so that you can make AuthZ decisions without requiring a call to Identity Server. This reduces coupling, and means you have less outbound calls from your API.
E.g. When you sign in, a JWT token could be created that contains the roles the user is a member of plus the users unique id (sub claim)
{
"iss": "https://my.api.com/trust",
"aud": "https://my.api.com",
"exp": 1512748805,
"nbf": 1481212805,
"scope": "openid",
"sub": "83b0451a718b4d54b930d6fe9cb7b442",
"idp": "site",
"roles": [
"role1",
"role2"
]
}
Your API can now just check the claims presented to it and say 'To call this API endpoint, the token presented to me must have role2 in the roles claim'.
You can also do this with the scopes, using the scope attribute
A well designed JWT token will contain the right amount of information to make AuthZ decisions without requiring lots of additional calls, whilst keeping the overall size of the token as small as possible - remember, it's included on every request.

Resources