MS Graph API's subscription create to OnlineMessages fails - azure-active-directory

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.

Related

How do I avoid React Native GoogleSignIn by sending to google a password violation

I used #react-native-google-signin/google-signin": "^8.0.0" to create a google sign in button into my app.
When I used it, google recognised it as not trusted app, so send my an email to advice of a violation, and now every time I use a password saved on my google account to login on any site or application, he gives me a message telling me to change all of my passwords. I solved to remove the message, by ignoring for every passwords, almost 200 :/. But it's just temporary solution, cause if I do login again to my app it will happen again. How can I say to google that it is an app in developing, is there any mode to activate? Here's my code:
GoogleSignin.configure({
scopes: ['https://www.googleapis.com/auth/drive.readonly'], // what API you want to access on behalf of the user, default is email and profile
webClientId: '15299853035-njb79hdij6h1svo22drigurca1qb4djb.apps.googleusercontent.com', // client ID of type WEB for your server (needed to verify user ID and offline access)
offlineAccess: true, // if you want to access Google API on behalf of the user FROM YOUR SERVER
// hostedDomain: '', // specifies a hosted domain restriction
// forceCodeForRefreshToken: true, // [Android] related to `serverAuthCode`, read the docs link below *.
// accountName: '', // [Android] specifies an account name on the device that should be used
iosClientId: '15299853035-siujgcjtol0lfja83n7p6fk55cq6jinn.apps.googleusercontent.com', // [iOS] if you want to specify the client ID of type iOS (otherwise, it is taken from GoogleService-Info.plist)
// googleServicePlistPath: '', // [iOS] if you renamed your GoogleService-Info file, new name here, e.g. GoogleService-Info-Staging
// openIdRealm: '', // [iOS] The OpenID2 realm of the home web server. This allows Google to include the user's OpenID Identifier in the OpenID Connect ID token.
// profileImageSize: 120, // [iOS] The desired height (and width) of the profile image. Defaults to 120px
});
try {
await GoogleSignin.hasPlayServices();
const { idToken } = await (await GoogleSignin.signIn());
const googleCredential = await GoogleAuthProvider.credential(idToken);
await signInWithCredential(authApp, googleCredential)
.then(async(userCredential) => {
if(!authApp.currentUser.emailVerified)
{
sendEmailVerification(authApp.currentUser)
.then(() => {
// Email verification sent!
// ...
})
.catch((error)=>{
setLoading(false)
console.log(error)
})
}
......

Unable to configure ASP.NET for Azure Access Token

I was hoping that someone might be able to shed some light on issues that I'm having with authentication. I've mostly used this guidance: https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-spa-overview
I've got a react app that is successfully (I think...) retrieving access tokens for my API:
const account = msalInstance.getActiveAccount();
if (account) {
msalInstance.acquireTokenSilent({
...apiToken,
account: account
}).then((response) => {
setToken(response.accessToken);
});
}
My requests place the token in the authorization header:
Headers
My token looks looks like: Token
API Registration
My API returns 401 whenever I use the Authorize attribute. Because my client is retrieving a token that looks correct, I'm assuming the issue is on my API. This is what I have in my startup auth:
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
AccessTokenFormat = new JwtFormat(
new TokenValidationParameters
{
// Check if the audience is intended to be this application
ValidAudiences = new[] { [MY_API_CLIENT_ID (SAME AS AUDIENCE IN TOKEN)], [MY API REGISTRATION URI] },
// Change below to 'true' if you want this Web API to accept tokens issued to one Azure AD tenant only (single-tenant)
// Note that this is a simplification for the quickstart here. You should validate the issuer. For details,
// see https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore
ValidateIssuer = false,
ValidateAudience = false,
ValidateTokenReplay = false,
ValidateIssuerSigningKey = false,
ValidateLifetime = false,
ValidateActor = false, //all false for testing
},
new OpenIdConnectSecurityKeyProvider("https://login.microsoftonline.com/[MY_TENANT_ID]/v2.0/.well-known/openid-configuration")
),
});
Tough silent error here. I noticed that none of the validation callbacks were being invoked. I was missing the package Microsoft.Owin.Host.SystemWeb

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

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.

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

Custom endpoint for authorized clients on Identity Server 4

I want my Identity Server 4 server to offer an additional service (e.g., "MyAdditionalService") for SOME of the registered clients. That service will be consumed by them through a custom endpoint to be defined on the server.
I am thinking of defining an API for my that service (e.g., named "myAdditionalService") so that the access to such service can be granted to clients according to their configuration. However I am not sure how to restrict the access to the Endpoint (MVC - Action method) allowing only the clients (potentially on behalf of a user) that are allowed to consume the API.
I found out that I can do:
services.AddAuthorization(options =>
{
options.AddPolicy("MyAdditionalServicePolicy",
policy => policy.RequireClaim("scope",
"myAdditionalService"));
});
and use the attribute [Authorize("MyAdditionalServicePolicy")] to decorate the action method that is used to access such service. However, I don't know can the server be the API at the same time or even if it is possible.
How can I implement this? It is confusing that the token service plays the role of the API as well, since it protects access to an action method or endpoint.
Thanks.
UPDATE:
My web app is an IdentityServerWithAspNetIdentity which already use the Authentication mechanism of Asp.net core Identity. For the sake of the example, the additional service my web app if offering to some registered clients is the list of Twitter friends of a user (Modeled on a controller called Twitter, action called ImportFriends) the api is consequently called "TwitterFriends"
As per suggestion in response below, I modified my Configure() method to have app.UseJwtBearerAuthentication(). I already had app.UseIdentity() and app.UseIdentityServer() as shown below:
app.UseIdentity();
app.UseIdentityServer();
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AuthenticationScheme = "Bearer",
Authority = Configuration["BaseUrl"],
Audience = "TwitterFriends",
RequireHttpsMetadata = false //TODO: make true, it is false for development only
});
// Add external authentication middleware below. To configure them please see http://go.microsoft.com/fwlink/?LinkID=532715
app.UseGoogleAuthentication(new GoogleOptions
{
AuthenticationScheme = "Google",
SignInScheme = "Identity.External", // this is the name of the cookie middleware registered by UseIdentity()
And on a dedicated controller:
[Authorize(ActiveAuthenticationSchemes = "Identity.Application,Bearer")]
//[Authorize(ActiveAuthenticationSchemes = "Identity.Application")]
//[Authorize(ActiveAuthenticationSchemes = "Bearer")]
[SecurityHeaders]
public class TwitterController : Controller
{...
but I am getting this in the log:
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware
[7]
Identity.Application was not authenticated. Failure message: Unprotect tic
ket failed
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed for user: (null).
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.A
uthorization.AuthorizeFilter'.
info: Microsoft.AspNetCore.Mvc.ChallengeResult[1]
Executing ChallengeResult with authentication schemes (Identity.Applicatio
n, Bearer).
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware
[12]
AuthenticationScheme: Identity.Application was challenged.
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware[12]
AuthenticationScheme: Bearer was challenged.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action IdentityServerWithAspNetIdentity.Controllers.TwitterContro
ller.ImportFriends (IdentityServerWithAspNetIdentity) in 86.255ms
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 105.2844ms 401
I have tried different combinations of the attribute but it seems that Identity.Application and Bearer don't get along in this scenario: getting 401.
any help is appreciated.
Thanks..
See this example on how to host an API in the same web app as IdentityServer.
https://github.com/brockallen/IdentityServerAndApi
In essence you need to add the JWT token validation handler:
services.AddAuthentication()
.AddJwtBearer(jwt =>
{
jwt.Authority = "base_address_of_identityserver";
jwt.Audience = "name of api";
});
On the API itself you must select the JWT authentication scheme:
public class TestController : ControllerBase
{
[Route("test")]
[Authorize(AuthenticationSchemes = "Bearer")]
public IActionResult Get()
{
var claims = User.Claims.Select(c => new { c.Type, c.Value }).ToArray();
return Ok(new { message = "Hello API", claims });
}
}
If you want to enforce an additional authorization policy, you can either pass that into the [Authorize] attribute or call it imperatively.
To achieve this, first you have to write some policy. Policy will define the boundry of accessibility of that specific api.
So you will assign the some scope to registered clients. let's say scope name is "ApiOnlyForRegisteredClients".
So we will create the policy as below:
services.AddAuthorization(options =>
{
options.SetRegisteredClientsPolicy();
}
and
private static void RequireScope(this AuthorizationPolicyBuilder authorizationPolicyBuilder, string[] values)
{
authorizationPolicyBuilder.RequireClaim("scope", values);
}
private static void SetRegisteredClientsPolicy(this AuthorizationOptions options)
{
options.AddPolicy(
OpenIdPolicies.Clients.RegisteredClients,
policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser();
policyBuilder.RequireScope(new string[] { "ApiOnlyForRegisteredClients" });
});
}
Once it done, you are done with policy creation.
Make sure while creating the access token, you are put the same value "ApiOnlyForRegisteredClients" in scope claim.
Now we have to add one api and label it with [Authorize] attribute.
[Authorize(AuthenticationSchemes = "Bearer", Policy = OpenIdPolicies.Clients.RegisteredClients)]
public async Task<ActionResult<T>> Post(int userId, [FromBody] List<int> simRoleIds)
{
}
Now we have to add jwt authentication middleware.
.AddJwtBearer("Bearer", options =>
{
options.Authority = configuration["AuthorityAddresses"];
options.RequireHttpsMetadata = Convert.ToBoolean(configuration["RequireHttpsMetadata"]);
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
TokenDecryptionKey = new X509SecurityKey()
ValidAudiences = apiResources.Select(x => x.ResourceName).ToList(),
ValidIssuers = new List<string> { authorityAddressWithHttps.Uri.OriginalString, authorityAddressWithBasePathHttps.Uri.OriginalString, configuration["AuthorityAddresses"] }
};
})

Resources