I understand it's a long question but I would really appreciate it if anyone could share their thoughts or experience with me as I've been around this for a few days now trying lots of things. I'm having an asp net core 3.1 web API application and an ASP.NET Core 3.1 MVC application.
Both have been registered in Azure AD. The API project is supposed to create calendar events based on the request payload it receives from the MVC project. I am following the Microsoft instructions from this link here
But once the API project makes a call against the Microsoft Graph, it fails with the following error:
"code": "InvalidAuthenticationToken",
"message": "Access token validation failure. Invalid audience.",
I'm putting in the minimum here to provide some more info but the whole sample can be downloaded from the link above.
ASP.NET Core MVC Startup.cs:
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(options =>
{
Configuration.Bind("AzureAd", options);
AzureAdOptions.Settings = options;
})
.AddCookie();
ASP.NET Core MVC project AddAzureAd function:
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, ConfigureAzureOptions>();
builder.AddOpenIdConnect();
return builder;
}
ConfigureAzureOptions:
public void Configure(string name, OpenIdConnectOptions options)
{
options.ClientId = _azureOptions.ClientId;
options.Authority = _azureOptions.Authority;
options.UseTokenLifetime = true;
options.CallbackPath = _azureOptions.CallbackPath;
options.RequireHttpsMetadata = false;
options.ClientSecret = _azureOptions.ClientSecret;
options.Resource = "https://graph.microsoft.com"; // AAD graph
// Without overriding the response type (which by default is id_token), the OnAuthorizationCodeReceived event is not called.
// but instead OnTokenValidated event is called. Here we request both so that OnTokenValidated is called first which
// ensures that context.Principal has a non-null value when OnAuthorizeationCodeReceived is called
options.ResponseType = "id_token code";
// Subscribing to the OIDC events
options.Events.OnAuthorizationCodeReceived = OnAuthorizationCodeReceived;
options.Events.OnAuthenticationFailed = OnAuthenticationFailed;
}
And here's the code from the API project to configure Azure Options:
private class ConfigureAzureOptions : IConfigureNamedOptions<JwtBearerOptions>
{
private readonly AzureAdOptions _azureOptions;
public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
{
_azureOptions = azureOptions.Value;
}
public void Configure(string name, JwtBearerOptions options)
{
// options.Audience = _azureOptions.ClientId;
options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
// The valid audiences are both the Client ID(options.Audience) and api://{ClientID}
// --->>> I've changed this to also have "https://graph.micrososft.com" but no luck
options.TokenValidationParameters.ValidAudiences = new string[] { _azureOptions.ClientId, $"api://{_azureOptions.ClientId}" }; // <<--- I've changed this to "https://graph.micrososft.com" but no luck
// If you want to debug, or just understand the JwtBearer events, uncomment the following line of code
// options.Events = JwtBearerMiddlewareDiagnostics.Subscribe(options.Events);
}
public void Configure(JwtBearerOptions options)
{
Configure(Options.DefaultName, options);
}
}
This is how I gain a token from the MVC project - the authority is the api://client_id:
string userObjectID = User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
//AuthenticationContext authContext = new AuthenticationContext(AzureAdOptions.Settings.Authority, new NaiveSessionCache(userObjectID, HttpContext.Session));
AuthenticationContext authContext = new AuthenticationContext(AzureAdOptions.Settings.Authority);
ClientCredential credential = new ClientCredential(AzureAdOptions.Settings.ClientId, AzureAdOptions.Settings.ClientSecret);
I appreciate your thoughts and experience on this - thanks again for your time.
Looks like your client app is acquiring a Microsoft Graph API token:
options.Resource = "https://graph.microsoft.com";
An access token has an audience (aud claim) that specifies what API it is meant for. Your client app needs to use your API's client id or application ID URI as the resource. This way you get an access token that is meant for your API.
The Resource option there is limited to one API. If you need tokens for multiple APIs, you'll need to setup an event listener for AuthorizationCodeReceived and use MSAL.NET to exchange the authorization code for tokens. I have a sample app that does this: https://github.com/juunas11/aspnetcore2aadauth/blob/97ef0d62297995c350f40515938f7976ab7a9de2/Core2AadAuth/Startup.cs#L58. This app uses .NET Core 2.2 and ADAL though, but the general approach with MSAL would be similar.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With