I am having a problem configuring a React client accessing a .NET 5 web API using Azure AD B2C. I reviewed the following documents;
Register apps in Azure Active Directory B2C
Register a single-page application (SPA) in Azure AD B2C
Add a web API application to your Azure AD B2C tenant
Single-page application: Acquire a token to call an API
Enable authentication in your own web API by using Azure AD B2C
I ended up using the examples shown in the last document and using the configuration settings for the node.js API server in my .NET 5 Web API;
React Configuration
import { LogLevel } from "#azure/msal-browser";
export const b2cPolicies = {
names: {
signIn: "b2c_1_signin",
signInStaff: "b2c_1_signupin_staff"
},
authorities: {
signIn: {
authority: "https://<b2cTenantName>.b2clogin.com/<b2cTenantName>.onmicrosoft.com/b2c_1_signin",
},
signInStaff: {
authority: "https://<b2cTenantName>.b2clogin.com/<b2cTenantName>.onmicrosoft.com/b2c_1_signupin_staff"
}
},
authorityDomain: "<b2cTenantName>.b2clogin.com"
}
export const msalConfig = {
auth: {
clientId: "ec0441a4-89ac-1111-1111-111111111111",
authority: b2cPolicies.authorities.signIn.authority,
knownAuthorities: [b2cPolicies.authorityDomain],
redirectUri: "http://localhost:3000",
navigateToLoginRequestUrl: false,
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: false,
},
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (containsPii) return;
level = LogLevel.Verbose;
switch (level) {
case LogLevel.Error:
console.error(message);
return;
case LogLevel.Info:
console.info(message);
return;
case LogLevel.Verbose:
console.debug(message);
return;
case LogLevel.Warning:
console.warn(message);
return;
default:
console.log(message);
}
}
}
}
};
export const loginRequest = {
scopes: ["openid", "offline_access"]
};
export const loginRequestStaff = {
scopes: ["openid", "profile"]
};
export const protectedResources = {
portalApi: {
scopes: ["https://<b2cTenantName>.onmicrosoft.com/PortalClient/access_as_user"],
redirectUri: "http://localhost:3000/Dashboard",
},
portalApiStaff: {
scopes: ["https://<b2cTenantName>.onmicrosoft.com/PortalClient/access_as_staff"],
redirectUri: "http://localhost:3000/Dashboard",
}
}
.NET 5 AppSettings.json
{
...
"AzureAd": {
"Instance": "https://<b2cTenantName>.b2clogin.com",
"Domain": "<b2cTenantName>.onmicrosoft.com",
"ClientId": "ec0441a4-89ac-1111-1111-111111111111",
"SignUpSignInPolicyId": "B2C_1_SignIn"
},
...
}
Portal API Registration
Property
Value
Application (Client) ID
d1a138a9-2379-1111-1111-111111111111
Directory (Tenant) ID
6c977334-b859-2222-2222-222222222222
Redirect URIs
http://localhost:3000
Certificates & Secrets
None
API Permissions (all have Admin consent)
Microsoft Graph - offline_access, openid
Application ID URI
https://.onmicrosoft.com/PortalApi
Exposed APIs (scopes)
access_as_staff, access_as_user
Portal Client Registration
Property
Value
Application (Client) ID
ec0441a4-89ac-1111-1111-111111111111
Directory (Tenant) ID
6c977334-b859-2222-2222-222222222222
Redirect URIs
http://localhost:3000/Dashboard
http://localhost:3000
Certificates & Secrets
None
API Permissions (all have Admin consent)
Microsoft Graph: - offline_access, openid
Portal API - access_as_staff, access_as_user
MSAL Login & Access Token Code
import { PublicClientApplication } from "#azure/msal-browser";
import { msalConfig, b2cPolicies, protectedResources, loginRequest, loginRequestStaff } from "./authConfig";
let msalInstance = new PublicClientApplication(msalConfig);
export const getMsalInstance = () => msalInstance;
export const login = () => {
msalInstance.config.auth.authority = b2cPolicies.authorities.signIn.authority;
msalInstance.loginRedirect(loginRequest);
}
export const loginAsStaff = () => {
msalInstance.config.auth.authority = b2cPolicies.authorities.signInStaff.authority;
msalInstance.loginRedirect(loginRequestStaff);
}
export const getAccessToken = async (accessToken, isStaffUser) => {
const accounts = msalInstance.getAllAccounts();
if (accounts.length === 0) return null;
let _accessToken = {...accessToken};
if (_accessToken.token == null || _accessToken.expires <= Date()) {
const response = await msalInstance.acquireTokenSilent({
account: accounts[0],
scopes: isStaffUser ? protectedResources.portalApiStaff.scopes : protectedResources.portalApi.scopes
});
const expiryDt = new Date(0).setUTCSeconds((response.idTokenClaims.exp));
_accessToken = { token: response.idToken, expires: expiryDt };
}
return _accessToken;
}
.NET 5 Web API Startup.cs
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(options =>
{
Configuration.Bind("AzureAd", options);
options.TokenValidationParameters.NameClaimType = "name";
}, options => { Configuration.Bind("AzureAd", options); });
services.AddAuthorization();
...
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
...
}
}
When acquiring the access token in the MSAL code I get the following response in response.idToken;
{
"typ": "JWT",
"alg": "RS256",
"kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk"
}.{
"exp": 1669793111,
"nbf": 1669789511,
"ver": "1.0",
"iss": "https://<b2cTenantName>.b2clogin.com/6c977334-b859-2222-2222-222222222222/v2.0/",
"sub": "3a35cfce-de55-4217-9c33-217f14639578",
"aud": "ec0441a4-89ac-1111-1111-111111111111",
"nonce": "ce9b5c4f-010d-43cc-9dce-861e6a087489",
"iat": 1669789511,
"auth_time": 1669789496,
"idp_access_token": "eyJraWQiOiItVWRTSVB...7bBGxGDzNQWqw",
"idp": "https://<idp>/oauth2/auskoi3basJBCcDOy1t7",
"given_name": "John",
"family_name": "Doe",
"name": "John Doe",
"oid": "3a35cfce-de55-4217-9c33-217f14639578",
"emails": [
"John.Doe#my-company.com.au"
],
"tfp": "B2C_1_SignUpIn_Staff"
}.[Signature]
and if I then decode the idp-access-token I get;
{
"kid": "-UdSIPeUmX7e4pqpXRBb7IXu3bCLsQo0hU67PYDhdfM",
"alg": "RS256"
}.{
"ver": 1,
"jti": "AT.r_KbqlRqqur1gxf9u5S8VReP2awA58YjtIfKiBdLokQ",
"iss": "https://<my-company>/oauth2/auskoi3basJBCcDOy1t7",
"aud": "Company-CustomerPortal",
"iat": 1669789481,
"exp": 1669793081,
"cid": "0oakoicxjrL5IMsmd1t7",
"uid": "00ua37oxlxRdmbFgb1t7",
"scp": [
"groups",
"profile",
"openid"
],
"auth_time": 1669789477,
"sub": "John.Doe#my-company.com.au"
}.[Signature]
Neither of which have the access_as_staff scope.
If I configure the client and API code to use the Portal Client clientId (ec0441a4-89ac-1111-1111-111111111111) and submit the access token to an [Authorize] route on the web API I get the following error;
IDW10201: Neither scope or roles claim was found in the bearer token.
If however I configure the API to use the Portal API clientId (d1a138a9-2379-1111-1111-111111111111) and the client to use the Portal Client clientId (ec0441a4-89ac-1111-1111-111111111111) as shown in the example, I get the following error;
IDX10214: Audience validation failed. Audiences: 'ec0441a4-89ac-1111-1111-111111111111'. Did not match: validationParameters.ValidAudience: 'd1a138a9-2379-1111-1111-111111111111' or validationParameters.ValidAudiences: 'null'.
I tried to reproduce the same in my environment:
I tried to change the scope parameter but still I received .
In my case I am calling graph api:
So my scope must be https://graph.microsoft.com/.default
I got the same error when I tried to call an API from my web app.
Error:
SecurityTokenInvalidAudienceException: IDX10214: Audience validation failed. Audiences: 'xxx'. Did not match: validationParameters.ValidAudience: xxxx or validationParameters.ValidAudiences: xxxx
In my case I have given wrong clientId in my clientApp registration in
appsettings.json
{
"AzureAd": {
"Instance": "....",
"Domain": "xxx",
"ClientId": "xxx",
"TenantId": "xxxf3a0cxxb0",
"ClientSecret": "xxxxxxxxx",
"ClientCertificates": [
],
….
},
Make sure to expose the scopes for your API in your API configuration .
Give that (exposed)API permissions to the APP that is clientAPP .
Also check the issuer endpoint,
"iss": "https://<b2cTenantName>.b2clogin.com/xxxx/v2.0/",
If it has v2 endpoint make sure , the value is 2 in> "accessTokenAcceptedVersion": 2,
With all changes , I could call my Api successfully:
Also check this reference : Azure AD B2C: Call an ASP.NET Web API from an ASP.NET Web App - Code Samples | Microsoft Learn
Related
I'm getting an access token from AzureAD using managed identity in my calling API. This user-assigned managed identity has an app role defined by the manifest of the called-API's app registration assigned to it. The token has the app role in its contents. So far, so good (I think.)
When I attach the token to a request and call the intended API, I get the following message:
IDX10214: Audience validation failed. Audiences: '{clientId of called API App registration}'. Did not match: validationParameters.ValidAudience: 'api://{clientId of called API App registration}' or validationParameters.ValidAudiences: 'null'.
The difference between the audience on the token and what the built-in token validation is using as the ValidAudience to compare it to is the preceding "api://". The application URI of the app registration defined in Azure Portal is indeed "api://{clientId of called API App registration}"
I have tried many different variations of request contexts when generating my token... prefixing "api://" to the guid, appending "/.default" to the Application URI, but cannot get the token to be accepted as valid.
This is the configuration section I have on my called application to authorize the token presented:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "the Guid matching the app registration Application ID",
"TenantId": "my tenant id",
"Audience": "api://{the Guid matching the app registration Application ID}"
}
}
This is my Program.cs:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Identity.Web;
using Microsoft.IdentityModel.Logging;
var builder = WebApplication.CreateBuilder(args);
IdentityModelEventSource.ShowPII = true;
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services
.AddApplicationInsightsTelemetry()
.AddHealthChecks()
.AddApplicationInsightsPublisher(saveDetailedReport: true);
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/healthz");
app.Run();
This is my controller:
using theModelNamespace.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web.Resource;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace someMoreNamespace.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class NamesController : ControllerBase
{
[HttpGet("ping")]
//[AllowAnonymous]
public IActionResult PingOnly()
{
return Ok("Alive");
}
[HttpGet()]
//[Authorize(Roles = "Api.Read,Api.ReadWrite,Api.OtherUserApp")]
public async Task<IActionResult> GetNames()
{
AuthenticateResult authResult;
try
{
authResult = await HttpContext.AuthenticateAsync("Bearer");
}
catch (Exception ex)
{
var innerException = ex.InnerException != null ? ex.InnerException.Message : String.Empty;
var responseString = $"Error occurred in authentication: {ex.Message} {innerException}.";
return StatusCode(500, responseString);
}
try
{
HttpContext.ValidateAppRole("Api.OtherUserApp");
return Ok(Data.NameList);
}
catch (Exception ex)
{
var innerException = ex.InnerException != null ? ex.InnerException.Message : String.Empty;
var authResults = (authResult != null && authResult.Principal != null) ? $"succeeded: {authResult.Succeeded}, identityName: {authResult.Principal.Identity?.Name}, {authResult.Principal.Claims?.Select(x => $"{x.Type}: {x.Value}")}" : string.Empty;
authResults = authResults == String.Empty && authResult.Failure != null ? authResult.Failure.Message : authResults;
var claimContents = HttpContext != null && HttpContext.User != null ? String.Join(',', HttpContext.User.Claims.Select(x => $"{x.Type}: {x.Value}")) : String.Empty;
var responseString = $"Error occurred in validation: {ex.Message} {innerException}. \n\nClaim contents: {claimContents}\n\nAuthResults: {authResults}";
return StatusCode(500, responseString);
}
}
[HttpPost()]
//[Authorize(Roles = "Api.ReadWrite")]
public IActionResult PostName([FromBody] NamesModel nameModel)
{
Data.NameList.Add(nameModel);
return Ok(Data.NameList);
}
[HttpGet("Unreachable")]
//[Authorize(Roles = "Api.InvalidScope")]
public IActionResult UnreachableName([FromBody] NamesModel nameModel)
{
Data.NameList.Add(nameModel);
return Ok(Data.NameList);
}
}
}
I have the authorize attributes commented out and the HttpContext.AuthenticateAsync("Bearer") added in for troubleshooting and so I can see the output of the authentication result I listed at the beginning of the post.
I've inspected the token, and the "aud" claim is indeed the clientId of the app registration of the called API, and is not prefixed with "api://" The role I need appears to be contained the expected way (roles: [ "Api.OtherUserApp" )] in the token.
The anonymous calls work fine as expected. It is only the get endpoint which calls AuthenticateAsync which has an issue.
What am I missing here to get the token to be accepted by the called API?
I tried to reproduce the same in my environment.
I received the same error:
SecurityTokenInvalidAudienceException: IDX10214: Audience validation failed. Audiences: '50065xxxxx1e6fbd2ed06e'. Did not match: validationParameters.ValidAudience: 'api://xxxxxx1xx06e' or validationParameters.ValidAudiences: 'null'.
Here as the error says , audiences did not match the one we got in token.
Make sure the value in the audience is noted and check the same if it is equal to clientId or not.
Note that ,The valid audiences we can have is either clientId or AppId URI
Here I am getting an audience of value applicationId or clientId in error which it is not matching my code requested .
as you gave AppId URI i.e; api:// ,for audience ,it is invalid. So the correct one to be given here is ClientId.
ValidAudiences = new List<string>
{
“CLIENTID”,
“APPID URI”
}
Appsettings.json
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "testtenant.onmicrosoft.com",
"ClientId": "xxxxxx",
"TenantId": "xxxxxxxxxxxx",
"ClientSecret": "xxxxx",
"Audience": "<clientId>",
"ClientCertificates": [
],
"CallbackPath": "/signin-oidc"
},
"DownstreamApi": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
//"Scopes": "api://<clientId>/.default"
"Scopes": "https://graph.microsoft.com/.default"
},
Here a scope for a V2 endpoint application can be exposed at api:///access_as_user or api://<clientId>/<scope value> for your webapi .
Make sure your accessTokenAcceptedVersion is 2 for v2 endpoint issuer
For v1 scope is <App ID URI>/.default,
Here when my issuer has v1 endpoint
In that case accessTokenAcceptedVersion is null or 1
I tried to get the user display name, through my api using below code in my controller.
HomeController:
[Authorize]
public async Task<IActionResult> Index()
{
var user = await _graphServiceClient.Me.Request().GetAsync();
ViewData["ApiResult"] = user.DisplayName;
ViewData["Givenname"] = user.GivenName;
return View();
}
I could run the application successfully and call my API enpoint without error.
I'm trying to setup a web api using ASP.Net Core 6 so that users can hit my end points and then I do some work in Teams using a privileged account. I don't think I am wiring up the DI part correctly because I get the following error when making the request to Teams:
MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.
Here is how everything is setup:
Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(builder.Configuration.GetSection("Graph"))
.AddInMemoryTokenCaches();
appsettings.json
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "<< domain >>",
"TenantId": " <<tenant id >>",
"ClientId": " << client id >>",
"ClientSecret": " << client secret >>"
},
"Graph": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "https://graph.microsoft.com/.default"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
Teams.cs
namespace GraphApiService.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class Teams : ControllerBase
{
private readonly GraphServiceClient _graphServiceClient;
public Teams(GraphServiceClient graphServiceClient)
{
_graphServiceClient = graphServiceClient;
}
// GET: api/<Teams>
[HttpGet]
public IEnumerable<string> Get()
{
//Throws an error here!
var teams = _graphServiceClient.Teams[<< team id >>].Request().GetAsync().Result;
return new string[] { "value1", "value2" };
}
}
}
Now if I skip the DI part and just setup the client in the constructor, it works fine.
public Teams()
{
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = " << tenand id >>";
var clientId = "<< client id >>";
var clientSecret = "<<client secret>>";
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret, options);
_graphServiceClient = new GraphServiceClient(clientSecretCredential, scopes);
}
Any help would be appreciated. Thanks!
Well this always happens. Spend two days looking for an answer. Post to a forum, and find the answer a few hours later. I got it to work by using 'WithAppOnly()' like this:
var teams = _graphServiceClient.Teams[teamId].Request().WithAppOnly().GetAsync().Result;
All the examples online use delegated user permissions, so it was hard to find anything on how to to handle app permissions.
I'm trying to implement On-behalf-of user in Asp.net Web API (.net 5). I receive an access_token from the Mobile APP, send it to my Web API. The Web API uses this token to call the GRAPH API to get the user's profile details.
Below is my code
Startup.cs file
services.AddAuthentication("JwtBearer")
.AddJwtBearer("JwtBearer", options =>
{
options.MetadataAddress = $"https://login.microsoftonline.com/{Configuration["b2bAzureAppIdentity:TenantId"]}/v2.0/.well-known/openid-configuration";
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = $"https://sts.windows.net/{Configuration["b2bAzureAppIdentity:TenantId"]}/",
// as audience, both the client id and the identifierUri are allowed (sematically equivalent)
ValidAudiences = new[] { Configuration["b2bAzureAppIdentity:AppIdUri"], Configuration["b2bAzureAppIdentity:ClientId"] }
};
}).AddMicrosoftIdentityWebApi(Configuration, "b2bAzureAppIdentity")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
controller.cs
[HttpGet("GetMyDetails")]
[AuthorizeForScopes(Scopes = new string[] { "user.read" })]
public async Task<IActionResult> GetMyDetails()
{
var user = await _graphServiceClient.Me.Request().GetAsync();
return new OkObjectResult(user.Photo);
}
Appsettings is in the below format
"b2bAzureAppIdentity": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "",
"TenantId": "",
"ClientId": "",
"ClientSecret": "",
"AppIdUri": ""},
"DownstreamApi": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "user.read"},
In Azure, the API permissions and scope are set correctly, this is evident because when I make calls from postman, I'm able to get the accesstoken for on_behalf_of and use it to get the user's profile details by calling https://graph.microsoft.com/v1.0/me
In the controller at this line
var user = await _graphServiceClient.Me.Request().GetAsync();
I get an error: "No account or login hint was passed to the AcquireTokenSilent call. "
I have googled for this error and the solution says that the user should consent the scope however it has already been consented by the admin in Azure portal. Also, the fact that this is working in Postman makes be believe that the configurations for the APP and the API are correct.
Has anyone faced similar issue?
This is happening because the access_token received is not sent along with the request to get the user detail. Here's an example of how to implement on behalf of provider:
// Create a client application.
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantID)
// The Authority is a required parameter when your application is configured
// to accept authentications only from the tenant where it is registered.
.WithAuthority(authority)
.WithClientSecret(clientSecret)
.Build();
// Use the API reference to determine which scopes are appropriate for your API request.
// e.g. - https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http
var scopes = new string[] { "User.Read" };
// Create an authentication provider.
ClientCredentialProvider authenticationProvider = new OnBehalfOfProvider(confidentialClientApplication, scopes);
var jsonWebToken = actionContext.Request.Headers.Authorization.Parameter;
var userAssertion = new UserAssertion(jsonWebToken);
// Configure GraphServiceClient with provider.
GraphServiceClient graphServiceClient = new GraphServiceClient(authenticationProvider);
// Make a request
var me = await graphServiceClient.Me.Request().WithUserAssertion(userAssertion).GetAsync();
In this case the token is added to the request in the call to WithUserAssertion.
Please let me know if this helps, and if you have further questions.
I have implemented azure ad authentication successfully. I am able to
login, and display the user's name.
I now need to call the graph api to access the user's email address.
I have my token type set to "ID" tokens in the azure portal.
Index.Razor
Code {
private HttpClient _httpClient;
public string Name { get; set; }
public string userDisplayName = "";
//this is what I am using to get the user's name
protected override async Task OnInitializedAsync()
{
var authstate = await Authentication_.GetAuthenticationStateAsync();
var user = authstate.User.Identity.Name;
if (user != null)
{
Name = user;
// 1) this is what I'm trying to use right now.
//The Graph API SDK
var attempt= await GraphServiceClient.Me.Request().GetAsync();
}
else
{
Name = "";
}
/*
// 2)this is what I've tried to use to access the graph api
_httpClient = HttpClientFactory.CreateClient();
// get a token
var token = await TokenAcquisitionService.GetAccessTokenForUserAsync(new string[] { "User.Read" });
// make API call
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var dataRequest = await _httpClient.GetAsync("https://graph.microsoft.com/beta/me");
if (dataRequest.IsSuccessStatusCode)
{
var userData = System.Text.Json.JsonDocument.Parse(await dataRequest.Content.ReadAsStreamAsync());
userDisplayName = userData.RootElement.GetProperty("displayName").GetString();
}
}
Startup.cs
var initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy
options.FallbackPolicy = options.DefaultPolicy;
});
services.AddRazorPages();
services.AddAuthorization();
services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
Appsettings.json
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
"TenantId": "xxxxxxxxxxxxxxxxxxxxxxxxx",
"ClientId": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
"CallbackPath": "/.auth/login/aad/callback",
"ClientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"DownstreamApi": {
"BaseUrl": "https://graph.microsoft.com/beta",
"Scopes": "user.read"
},
When I attempt a request via the 1st tried method mentioned in the Index.razor file above (I commented it out with the number 1) I get an error of: "MSAL.Net No account or login hint was passed to the AcquireTokenSilent call"
More details:
This an image of my delegated permissions set in azure portal
Lastly: this is a link the example I followed. https://github.com/wmgdev/BlazorGraphApi
You can add the optional "email" claim if you have control over the Azure AD App Registration:
After doing that, you will have an "emailaddress" claim in authstate.User.Claims
I just tried it in my Blazor app and it works great. I think it is possible for there to not be an email property though, so make sure you null check, etc.
You can use #context.User.Claims to get a login name, in case of, login name is same as an email address, as usual.
< AuthorizeView>
< Authorized>
Hello, #context.User.Claims.First( cl => cl.Type.ToString()=="preferred_username").Value
</Authorized>
<NotAuthorized>
Log in
</NotAuthorized>
</ AuthorizeView>
Retrieving a user's login name in Blazor WASM component
I have an ASP.NET Core API which uses JWT Authentication. Simple setup:
....
string authority = $"https://{configuration["Auth:Authority"]}";
string audience = configuration["Auth:Audience"];
return builder.AddJwtBearer(options =>
{
options.Authority = authority;
options.Audience = audience;
options.TokenValidationParameters = new TokenValidationParameters
{
// NameClaimType = "http://schemas.org/email"
// NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email"
// NameClaimType = "sub"
};
});
(as you can see commented code, I have been trying several settings)
When I decode JWT (using jwt.io) I see there is claim "sub" in JWT and has string value (internal ID of user in dbo)
{
"nbf": 1592585433,
"exp": 1592585763,
"iss": "https://*************",
"aud": "api_10",
"sub": "142",
"scope": [
"openid",
"api_10"
]
}
The problem is that dotnet switch sub to name claim. This returns no (0) results:
principal.Claims.Where(w => w.Type == "sub")
This returns userId I need "142":
principal.Claims.Where(w => w.Type == ClaimTypes.Name || ClaimTypes.NameIdentifier)
What is going on?! Where is my sub claim gone?
Just add JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); on the API's ConfigureServices method. Its suggested on the other comment as well. I verified it on my sample repo.
This issue is because OIDC StandardClaims are renamed on JWT token handler. By adding JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); you will clear the inbound claim type map on the JWT token handler.
Read more here