Microsoft Graph API auth error: "Access token validation failure. Invalid audience" - reactjs

Okay, so I've spent the last two days with this error, and just found a solution. In my search, I didn't find an single answer solving the issue that I had (rather I found multiple that eventually pointed me to the solution). So here's my attempt at explaining you the solution to the "Access token validation failure. Invalid audience" error:
TLDR:
Check that "https://graph.microsoft.com" is listed as AUD (audience) in the access token you receive when authenticating with MSAL on https://jwt.ms/ (Microsoft is behind the site jwt.ms source: https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens). In my case, the back-end API scope was listed, not "https://graph.microsoft.com". That's why the "audience" was invalid when Microsoft graph api checks the access token.
The solution is to request two different access tokens, one for the backend scope, and one for the https://graph.microsoft.com/User.Read scope:
/**
* Retrieve token for backend
*/
export const getToken = async (account): Promise<AuthenticationResult> => {
return await msalInstance.acquireTokenSilent({
scopes: [process.env.REACT_APP_API_SCOPE as string],
redirectUri: current_url,
account,
});
};
/**
* Retrieve token for Microsoft Graph API:
*/
export const getTokenForGraphApi = async (
account
): Promise<AuthenticationResult> => {
return await msalInstance.acquireTokenSilent({
scopes: ["https://graph.microsoft.com/User.Read"],
redirectUri: current_url,
account,
});
};
Here's the long story of how I found out:
I wanted to be able to query the Microsoft Graph API from a React application.
I've had the admin of my organization set up the Azure Portal, so that our App registration has API Permissions:
Backend API permission
Microsoft Graph
"User.Read"
"User.ReadBasic.All".
In React when I authenticate, I've used scopes:
scopes: [
process.env.REACT_APP_API_SCOPE as string,
"User.Read",
],
The authentication goes well, and I get an access token.
The access token works with our backend API, however when I try to use the access token with the Microsoft Graph API, I get the error:
"Access token validation failure. Invalid audience".
I read and searched forums, and I tried using jwt.ms.
Only our API is listed as "aud", and hence I suspect I need a token where both our API and "https://graph.microsoft.com" is placed.
I then tried preceding my User.Read scope with "https://graph.microsoft.com" so it would be:
scopes: [
process.env.REACT_APP_API_SCOPE as string,
"https://graph.microsoft.com/User.Read"
],
But it failed to authenticate with the error message:
"AADSTS28000: Provided value for the input parameter scope is not valid because it contains more than one resource. Scope api://{API-application-id}/a-scope https://graph.microsoft.com/User.Read openid profile is not valid."
Here, our backend is one resource, which has a-scope, and "https://graph.microsoft.com" is another resource with scope "User.Read".
The solution is hence to require two seperate access tokens: One with scope "https://graph.microsoft.com/User.Read", that you can use with the graph api, and another access token for your backend:
/**
* Retrieve token for backend
*/
export const getToken = async (account): Promise<AuthenticationResult> => {
return await msalInstance.acquireTokenSilent({
scopes: [process.env.REACT_APP_API_SCOPE as string],
redirectUri: current_url,
account,
});
};
/**
* Retrieve token for Microsoft Graph API:
*/
export const getTokenForGraphApi = async (
account
): Promise<AuthenticationResult> => {
return await msalInstance.acquireTokenSilent({
scopes: ["https://graph.microsoft.com/User.Read"],
redirectUri: current_url,
account,
});
};

The error message: "Access token validation failure. Invalid audience" tells you that the "AUD" (audience) is incorrectly set on the access token you are using with the Microsoft Graph API.
You can check your token with https://jwt.ms/ by pasting in the access token.
(Microsoft is behind the site jwt.ms site: https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens)
The solution is hence to require two separate access tokens: One with scope "https://graph.microsoft.com/User.Read", that you can use with the Microsoft graph api, and another access token for your backend:
/**
* Retrieve token for backend
*/
export const getToken = async (account): Promise<AuthenticationResult> => {
return await msalInstance.acquireTokenSilent({
scopes: [process.env.REACT_APP_API_SCOPE as string],
redirectUri: current_url,
account,
});
};
/**
* Retrieve token for Microsoft Graph API:
*/
export const getTokenForGraphApi = async (
account
): Promise<AuthenticationResult> => {
return await msalInstance.acquireTokenSilent({
scopes: ["https://graph.microsoft.com/User.Read"],
redirectUri: current_url,
account,
});
};
Then the AUD (audience) will be set correctly on both access tokens.

Related

Dotnet API requires auth both for application and React

I must be really stupid, But I have been struggling for weeks to try solve this issue, and all the digging I have done (in Stack overflow and MS Documentation) has yielded no results (or I'm too stupid to implement auth correctly)
I have a dotnet service which needs to act as an API - both for an application to post data to (an exe which logs exception data), and for a UI (react app) to get the posted exceptions
the exe can successfully send data to the dotnet app after first getting a token from login.microsoftonline.com and then sending the token (and secret) in the http request.
A sample postman pre-request script of the auth used (I've set all the secret stuff as environment variables):
pm.sendRequest({
url: 'https://login.microsoftonline.com/' + pm.environment.get("tenantId") + '/oauth2/v2.0/token',
method: 'POST',
header: 'Content-Type: application/x-www-form-urlencoded',
body: {
mode: 'urlencoded',
urlencoded: [
{key: "grant_type", value: "client_credentials", disabled: false},
{key: "client_id", value: pm.environment.get("clientId"), disabled: false},
{key: "client_secret", value: pm.environment.get("clientSecret"), disabled: false}, //if I don't configure a secret, and omit this, the requests fail (Azure Integration Assistant recommends that you do not configure credentials/secrets, but does not provide clear documentation as to why, or how to use a daemon api without it)
{key: "scope", value: pm.environment.get("scope"), disabled: false}
]
}
}, function (err, res) {
const token = 'Bearer ' + res.json().access_token;
pm.request.headers.add(token, "Authorization");
});
Now in React, I am using MSAL(#azure/msal-browser) in order to login a user, get their token, and pass the token to one of the dotnet endpoints using axios as my http wrapper, but no matter what I do, it returns http status 401 with WWW-Authenticate: Bearer error="invalid_token", error_description="The signature is invalid".
A simplified code flow to login user and request data from the API:
import {publicClientApplication} from "../../components/Auth/Microsoft";//a preconfigured instance of PublicClientApplication from #azure/msal-browser
const data = await publicClientApplication.loginPopup();
// ... some data validation
publicClientApplication.setActiveAccount(data.account);
// .. some time and other processes may happen here so we don't access token directly from loginPopup()
const activeAccout = publicClientApplication.getActiveAccount();
const token = publicClientApplication.acquireTokenSilent(activeAccount).accessToken;
const endpointData = await api()/*an instance of Axios.create() with some pre-configuration*/.get(
'/endpoint',
{ headers: {'Authorization': `bearer ${token}`} }); // returns status 401
The dotnet service has the following configurations
public void ConfigureServices(IServiceCollection services){
...
var authScheme = services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
authScheme.AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
...
}
namespace Controllers{
public class EndpointController : ControllerBase{
...
[Authorize]
[HttpGet]
public IActionResult GetEndpoint(){
return Ok("you finally got through");
}
}
}
I've literally tried so many things that I've lost track of what I've done...
I've even cried myself to sleep over this - but that yielded no results
i can confirm that running the request in postman, with the pre request script, it is possible to get the response from the endpoint
So....
After much digging and A-B Testing I was able to solve this issue.
I discovered that I was not sending the API scope to the OAuth token endpoint. To do this I needed to change the input for acquireTokenSilent.
The updated code flow to login user and request data from the API:
import {publicClientApplication} from "../../components/Auth/Microsoft";//a preconfigured instance of PublicClientApplication from #azure/msal-browser
const data = await publicClientApplication.loginPopup();
// ... some data validation
publicClientApplication.setActiveAccount(data.account);
// .. some time and other processes may happen here so we don't access token directly from loginPopup()
const activeAccout = publicClientApplication.getActiveAccount();
const token = publicClientApplication.acquireTokenSilent({scopes:["api://XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/.default"],account:activeAccount}).accessToken;//here scopes is an array of strings, Where I used the api URI , but you could directly use a scope name like User.Read if you had it configured
const endpointData = await api()/*an instance of Axios.create() with some pre-configuration*/.get(
'/endpoint',
{ headers: {'Authorization': `bearer ${token}`} }); // returns status 401

MS Graph API's subscription create to OnlineMessages fails

I am attempting to create a subscription to the resource /communications/onlineMeetings/?$filter=JoinWebUrl eq '{JoinWebUrl}' with the node ms graph client.
To do this, I have:
Two tenants, one with an active MS Teams license (Office 365 developer), whereas the other tenant houses my client app, which is a multi-tenant app.
Added the required scope to the client app (App level scope: OnlineMeetings.Read.All)
Given admin consent to the client app from the MS Teams tenant. The screenshot below shows the client app scope details in the MS Teams tenant.
Initialized the MSAL auth library as follows in the client app:
const authApp = new ConfidentialClientApplication({
auth: {
clientId: 'app-client-id',
clientSecret: 'app-client-secret',
authority: `https://login.microsoftonline.com/${tenantId}`,
},
});
Gotten an accessToken via the call:
const authContext = await authApp.acquireTokenByClientCredential({
authority: `https://login.microsoftonline.com/${tenantId}`,
scopes: ['https://graph.microsoft.com/.default'],
skipCache: true,
});
const accessToken = authContext.accessToken;
Initialized the MS Graph client as follows:
const client = MSClient.init({
debugLogging: true,
authProvider: (done) => {
done(null, accessToken);
},
});
Created a subscription successfully for the: CallRecords.Read.All scope (which correctly sends call record notifications to the defined webhook) with the following call:
const subscription = await client
.api('/subscriptions')
.version('beta')
.create({
changeType: 'created,updated',
notificationUrl: `https://my-ngrok-url`,
resource: '/communications/callrecords',
clientState: 'some-state',
expirationDateTime: 'date-time',
});
Attempted to create a subscription for the OnlineMeetings.Read.All scope with the following call:
const subscription = await client
.api('/subscriptions')
.version('beta')
.create({
resource: `/communications/onlineMeetings/?$filter=JoinWebUrl eq '{JoinWebUrl}'`,
changeType: 'created,updated',
notificationUrl: `https://my-ngrok-url`,
clientState: 'some-state',
expirationDateTime: 'date-time',
includeResourceData: true,
encryptionCertificate: 'serialized-cert',
encryptionCertificateId: 'cert-id',
});
This results in the error message:
GraphError: Operation: Create; Exception: [Status Code: Forbidden;
Reason: The meeting tenant does not match the token tenant.]
I am unsure what is causing this and and how to debug it further. Any help would be much appreciated.
My understanding of the joinWebUrl in the resource definition was incorrect -- the joinWebUrl is a placeholder for an actual online-meeting's URL (rather than a catch-all for all created meetings).
What I was hoping to achieve however is (currently) not possible, which is creating a subscription on a particular user's join/leave events to any online-meeting.

Wrong version of access token (expect V2 , received V1)

I am trying to register users from the Azure Active directory using #azure/msal-angular, to be more precise I tried the following tutorial
Those are the changes I have made to the project
export function MSALInstanceFactory(): IPublicClientApplication {
return new PublicClientApplication({
auth: {
clientId: 'my_real_client_id,
redirectUri: 'http://localhost:4200',
authority: 'https://login.microsoftonline.com/my_real_tenant_id',
postLogoutRedirectUri: '/'
},
cache: {
cacheLocation: BrowserCacheLocation.LocalStorage,
storeAuthStateInCookie: isIE, // set to true for IE 11
},
system: {
loggerOptions: {
loggerCallback,
logLevel: LogLevel.Info,
piiLoggingEnabled: false
}
}
});
}
export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
const protectedResourceMap = new Map<string, Array<string>>();
protectedResourceMap.set('https://graph.microsoft.com/v1.0/me', ['user.read']);
protectedResourceMap.set('http://localhost:5000/', ['profile']);
return {
interactionType: InteractionType.Redirect,
protectedResourceMap
};
}
The problem is that MsalInterceptor adds V1 token to the URL for the request to my API which expects V2.
Azure is configured to accessTokenAcceptedVersion: 2
I can provide more information if needed
Update
In my case, the problem was due to the scopes specified, both API for "user.read" and "profile" require V1 accessToken
Although you have resolved the issue, I would like to clarify more details.
In fact it's not the permissions "user.read" and "profile" require V1 accessToken. The actual reason is that:
Access Tokens versions are determined by the configuration of your application/API in the manifest. You have to change the accessTokenAcceptedVersion part of the manifest to 2 if you want a V2 access token. This is also the reason why you cannot control what version of the access token you will get from your client application when you request an access token.
We can't modify the accessTokenAcceptedVersion from Microsoft Graph side.
So the conclusion is that an access token for MS Graph and those are always V1 access token.

Getting Error while uploading file to S3 with Federated Identity using aws-amplify in React

While the user with Facebook federated Identity trying to upload Image, I'm getting an error: AWSS3Provider - error uploading Error: "Request failed with status code 403"
Status Code: 403 Forbidden
Noticed that URL in request, while user authenticated with Federated Identity (Facebook), looks:
Request URL: https://my-gallery-api-dev-photorepos3bucket-XXXX.s3.us-east-2.amazonaws.com/private/undefined/1587639369473-image.jpg?x-id=PutObject
The folder where the uploaded image will be placed is 'undefined' instead of being a valid user identity like for users authenticated with from AWS UserPool, see:
Request URL: https://my-gallery-api-dev-photorepos3bucket-XXXX.s3.us-east-2.amazonaws.com/private/us-east-2%3Aa2991437-264a-4652-a239-XXXXXXXXXXXX/1587636945392-image.jpg?x-id=PutObject
For Authentication and upload I'am using React aws dependency "aws-amplify": "^3.0.8"
Facebook Authentication (Facebook Button):
async handleResponse(data) {
console.log("FB Response data:", data);
const { userID, accessToken: token, expiresIn } = data;
const expires_at = expiresIn * 1000 + new Date().getTime();
const user = { userID };
this.setState({ isLoading: true });
console.log("User:", user);
try {
const response = await Auth.federatedSignIn(
"facebook",
{ token, expires_at },
user
);
this.setState({ isLoading: false });
console.log("federatedSignIn Response:", response);
this.props.onLogin(response);
} catch (e) {
this.setState({ isLoading: false })
console.log("federatedSignIn Exception:", e);
alert(e.message);
this.handleError(e);
}
}
Uploading:
import { Storage } from "aws-amplify";
export async function s3Upload(file) {
const filename = `${Date.now()}-${file.name}`;
const stored = await Storage.vault.put(filename, file, {
contentType: file.type
});
return stored.key;
}
const attachment = this.file
? await s3Upload(this.file)
: null;
I'm understand that rejection by S3 with 403, because of the IAM role, I have for authenticated users:
# IAM role used for authenticated users
CognitoAuthRole:
Type: AWS::IAM::Role
Properties:
Path: /
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Principal:
Federated: 'cognito-identity.amazonaws.com'
Action:
- 'sts:AssumeRoleWithWebIdentity'
Condition:
StringEquals:
'cognito-identity.amazonaws.com:aud':
Ref: CognitoIdentityPool
'ForAnyValue:StringLike':
'cognito-identity.amazonaws.com:amr': authenticated
Policies:
- PolicyName: 'CognitoAuthorizedPolicy'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- 'mobileanalytics:PutEvents'
- 'cognito-sync:*'
- 'cognito-identity:*'
Resource: '*'
# Allow users to invoke our API
- Effect: 'Allow'
Action:
- 'execute-api:Invoke'
Resource:
Fn::Join:
- ''
-
- 'arn:aws:execute-api:'
- Ref: AWS::Region
- ':'
- Ref: AWS::AccountId
- ':'
- Ref: ApiGatewayRestApi
- '/*'
# Allow users to upload attachments to their
# folder inside our S3 bucket
- Effect: 'Allow'
Action:
- 's3:*'
Resource:
- Fn::Join:
- ''
-
- Fn::GetAtt: [PhotoRepoS3Bucket, Arn]
- '/private/**${cognito-identity.amazonaws.com:sub}/***'
- Fn::Join:
- ''
-
- Fn::GetAtt: [PhotoRepoS3Bucket, Arn]
- '/private/**${cognito-identity.amazonaws.com:sub}**'
It works fine for users registered in AWS User Pool (Email, Password), but for federated users, there is no record in AWS User Pool only in Federated Identities, so there will be no cognito-identity.amazonaws.com:sub found for those users and directory 'undefined' not falling in role allowance for user identified with Federated Identity.
Please advise:
1. Where/how to fix this 'undefined' in URL?
2. Also, I would like, probably, to replace thouse Id's in upload URL to genereted user Id's from user database I'm going to add in near future. How to fix IAM Role to use custom Id's?
I stumbled with the same problem when doing Serverless Stack tutorial
This error arises when you do the Extra Credit > React > Facebook Login with Cognito using AWS Amplify, as you have notice uploading a file fails if you're authenticated with Facebook.
The error comes up when sending a PUT to:
https://<bucket>.s3.amazonaws.com/private/<identity-id>/<file>
...the <identity-id> is undefined so the PUT fails.
You can track down the source of this undefinition if you log what you get when running the login commands. For example, when you login using your email and password, if you do:
await Auth.signIn(fields.email, fields.password);
const currCreds = await Auth.currentCredentials();
console.log('currCreds', currCreds);
...you can see that identityId is set correctly.
On the other hand when you login with Facebook through Auth.federatedSignIn if you log the response you don't get identityId. Note: In the case you've previously logged in using email and password, it will remain the same, so this misconfiguration will also make uploading fail.
The workaround I've used is adding a simple lambda which returns the identityId for the logged in user, so once the user logs in with facebook, we ask for it and we can send the PUT to the correct url using AWS.S3().putObject
In the case you want to try this out, take into account that you should host your React app in https as Facebook doesn't allow http domains. You can set this adding HTTPS=true to your React .env file.
You can check my repos as example:
API
Frontend

How do I manage an access token, when storing in local storage is not an option?

I have a ReactJS app running in browser, which needs access to my backend laravel-passport API server. So, I am in control of all code on both client and server side, and can change it as I please.
In my react app, the user logs in with their username and password, and if this is successful, the app recieves a personal access token which grants access to the users data. If I store this token in local storage, the app can now access this users data by appending the token to outgoing requests.
But I do not want to save the access token in local storage, since this is not secure. How do I do this?
Here is what I have tried:
In the laravel passport documentation, there is a guide on how to automatically store the access token in a cookie. I believe this requires the app to be on the same origin, but I cannot get this to work. When testing locally, I run the app on localhost:4000, but the API is run on my-app.localhost. Could this be a reason why laravel passport does not make a cookie with the token, although they technically both have origin localhost?
OAuth has a page on where to store tokens. I tried the three options for "If backend is present", but they seem to focus on how the authorization flow rather than how to specifically store the token.
Here's the relevant parts of my code (of course, feel free to ask for more if needed):
From my react app:
const tokenData = await axios.post(this.props.backendUrl + '/api/loginToken', { email: 'myEmail', password: 'myPassword' })
console.log('token data: ', tokenData)
const personalAccessToken = tokenData.data.success.token;
var config = {
headers: {
'Authorization': "Bearer " + personalAccessToken
};
const user = await axios.get(this.props.backendUrl + '/api/user', config);
From the controller class ApiController:
public function loginToken()
{
if (Auth::attempt(['email' => request('email'), 'password' => request('password')])) {
$user = Auth::user();
$success['token'] = $user->createToken('MyApp')->accessToken;
return response()->json(['success' => $success], 200);
} else {
return response()->json(['error' => 'Unauthorised'], 401);
}
}
and the loginToken function is called from the /api/loginToken route.
Expected and actual results:
Ideally, I would love to have the token saved in a cookie like in the passport documentation, so I don't even have to attach the token to outgoing requests from the react app, but I'm not sure that this is even possible. Perhaps with third party cookies?
Else, I'd just like to find some way to store the token securely (for example in a cookie?), and then append it to outgoing calls from the react app.

Resources