Azure AD Authentication with .NET Core Web API - azure

We have a simple setup. Frontend web application gets a token from Azure AD. Frontend uses that token to authenticate requests to our backend .NET Core Web API. Both will be deployed as Azure Web Apps.
We have found a number of resources on this subject (Microsoft Docs, Blogs, Youtube) however, no matter what we try we are getting 401 Unauthorized errors when we send a request from the frontend to the backend.
Here is our setup:
We have 2 app registrations (one for the frontend, one for the backend)
In the Azure Portal I navigated to Azure AD -> App Registrations -> Backend App -> Expose an API -> Add Scope -> Filled out Form:
Then navigate to the Frontend app registration -> API Permissions -> Add a permission -> Add Access to API exposed in step 2.
Then select "Grant Admin Consent"
Onto the Frontend code:
On the Frontend we are using Microsoft's MSAL library. We have it configured as such:
// Frontend msal config
{
msalConfig: {
auth: {
clientId: "{frontend clientId}",
authority: "https://login.microsoftonline.com/{tenantId}/",
redirectUri: appUri,
postLogoutRedirectUri: appUri,
mainWindowRedirectUri: appUri
},
cache: {
cacheLocation: 'localStorage'
}
}
We send a login request:
// login.js
const request = {
scopes: [
"User.Read",
"api://{Backend Application Id From AzureAD App Registration}/access_as_user"
]
};
instance.loginPopup(request).catch(
(error) => {
console.log(error);
}
);
This works as expected and users are able to login and out of our frontend application.
We place our Azure AD provided token in our Authorization header in requests to our backend API (always returns a 401).
Backend code using Microsoft.Identity.Web (.NET 5):
// .Net appsettings.json
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "{backend clientId}",
"TenantId": "{tenantId}"
}
// Startup.cs
using Microsoft.Identity.Web;
ConfigureServices(IServiceCollection services)
{
//other services
services.AddMicrosoftIdentityWebApiAuthentication(Configuration, "AzureAd");
//other services
}
Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// other config
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// other config
}
Any help on how to proceed would be greatly appreciated. We are testing locally right now, but once we get auth working we will be deploying the frontend & backend applications to Azure Web Apps.
UPDATE:
I can see in the 401 response headers that I am getting:
Bearer error="invalid_token", error_description="The signature is invalid"

Try to add "Audience" parameter into AzureAD section: "Audience": "api://{Client_Id}".
To receive more info about auth you can pass subscribeToJwtBearerMiddlewareDiagnosticsEvents: true into services.AddMicrosoftIdentityWebApiAuthentication and check your logs after unsuccessful request.
Also, you can decode(https://jwt.io) your token and check the payload section if there are required scope and audience.
It could be that token (provided to backend) is actually for Microsoft Graph(check appId here: shawntabrizi.com/aad/…). If "scp" is "openid profile User.Read email" and "aud" is "00000003-0000-0000-c000-000000000000" then you are using the wrong token for your backend. Ask Frontend to check the way how they are receiving a token.
Frontend guy put something like this into code, correct me if I am wrong:
public static msalInterceptorConfigFactory(): MsalInterceptorConfiguration {
const resourceMap = new Map<string, Array<string>>();
resourceMap.set(environment.apiUrl, [environment.scope]);
return {
interactionType: InteractionType.Redirect,
resourceMap
};
}
public static msalGuardConfigFactory(): MsalGuardConfiguration {
return {
interactionType: InteractionType.Redirect,
authRequest: {
scopes: [environment.scope]
}
};
}
# module.ts
providers: [
...
{
provide: MSAL_GUARD_CONFIG,
useFactory: msalGuardConfigFactory
},
{
provide: MSAL_INTERCEPTOR_CONFIG,
useFactory: msalInterceptorConfigFactory
},
...
],

Related

How do I Read/Write configuration data in Azure App Configuration from a Blazor app using the logged in user's credentials?

I am building a web portal for managing the values in some of our Azure App Configuration stores, to provide a user-friendly editor for some complex json-configuration values.
I think I have all the setup required, but am only getting 401 or 403 responses when actually using the client. Documentation for this use case seems to be lacking, so I hope someone could give some guidance on how to implement this.
Some relevant code snippets:
Editor.razor
#using Microsoft.Identity.Web
#inject MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler
#inject ConfigurationService ConfigurationService
<editor/>
#code{
private object Value { get; set; }
protected override async Task OnInitializedAsync()
{
try
{
Value = await ConfigurationService.GetSettings();
}
catch (Exception ex)
{
ConsentHandler.HandleException(ex);
}
await base.OnInitializedAsync();
}
ConfigurationService.cs
using Azure.Data.AppConfiguration;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Web;
private readonly ConfigurationClient _client;
public ConfigurationClient(IOptions<ConfigurationSettings> options, ITokenAcquisition tokenAcquisition)
{
_client = new ConfigurationClient(new Uri(options.Value.Endpoint), new TokenAcquisitionTokenCredential(tokenAcquisition));
}
public async Task<object> GetSettings()
{
return await ConfigurationClient.GetConfigurationSettingAsync("key");
}
Program.cs
//code
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(new []{"User.Read"})
.AddInMemoryTokenCaches();
builder.Services.AddControllersWithViews()
.AddMicrosoftIdentityUI();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
//more code
appsettings.json
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "redacted.onmicrosoft.com",
"TenantId": "redacted",
"ClientId": "redacted",
"ClientSecret": "redacted",
"CallbackPath": "/signin-oidc"
}
}
The web app is registered in Azure AD, with Authentication set up and functioning. I have created a client secret and am using that in the app configuration. No token configuration is set. API permissions are as follows:
Azure AD API Permissions
No settings have been defined under the "Expose an API" tab.
The user I am testing this with is my own, and has the "Contributor" role on the Azure App Configuration resource.
The website allows logging in, and on visiting the relevant page correctly requests the user's permission to access the App Configuration resource. Inspecting traffic reveals the app requests an access token with the .default scope on the configuration resource, and receives one successfully. Using the token results in a 403 forbidden.
The "Contributor" role won't give you access to the data in App Configuration. You need to grant yourself the App Configuration Data Owner role. More details can be found in the document below:
https://learn.microsoft.com/en-us/azure/azure-app-configuration/concept-enable-rbac#azure-built-in-roles-for-azure-app-configuration

Bearer Token not working in AAD B2C .NETCorre

I have a Web API built using ASP.NET Core. I have a React App that will call this API. The identity is managed using AAD B2C. I am running into an issue where the bearer token generated by the app is not recognized by the API.
I am certain that this has to do with my settings because the token itself has all the claims I need (as decoded by JWT.io). However, when I pass it through the code in .NET Core to allow authorization, the ClaimsIdentity has nothing and contains no user information.
I am setting up the instance using the following lines of code:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(options =>
{
configuration.Bind("AzureAdClient", options);
options.TokenValidationParameters.NameClaimType = "name";
}, options => { configuration.Bind("AzureAdClient", options); });
I also have the following configuration:
"AzureAdClient": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "somename.onmicrosoft.com",
"ClientId": "guid here",
"TenantId": "guid here",
"Audience": "https://somename.onmicrosoft.com/tenants-api",
"SignUpSignInPolicyId": "B2C_1_RwSignIn"
}
Am I doing something wrong here?
I was able to get this figured out using a different strategy. The recommended configuration in most examples still does not work.
Instance: https://yourname.b2clogin.com
Domain: yourname.onmicrosoft.com
SignUpSignInPolicyId: the actual name of
your signup/sign in policy
ClientId: the client Id of the API Client.
var section = configuration.GetSection("AzureAdClient");
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = $"{section["Instance"]}/{section["Domain"]}/{section["SignUpSignInPolicyId"]}/v2.0/";
options.Audience = $"{section["ClientId"]}";
});

Cannot see role claims in access token when contacting my ASP.NET Core 3.1 API from browser app

The browser app
I have a browser app (CRA, TypeScript) which is issuing, after successfully authenticating to Azure AD, a request to my API:
public async acquireAccessToken(): Promise<string | undefined> {
let res: AuthResponse | undefined = undefined;
const params: AuthenticationParameters = {
scopes: ["Users.Read"],
};
try {
res = await this.msal.acquireTokenSilent(params);
} catch (error) {
res = await this.msal.acquireTokenPopup(params);
}
return !res || !res.accessToken ? undefined : res.accessToken;
}
The one before is a utility method to get the access token to contact the API, the actual call is here:
const token = await acquireAccessToken();
const res = await fetch("/controller/test", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
});
console.log(res.text());
Where msal is the UserAgentApplication I am using as client to handle authentication and authorization in my browser app.
I have everything correctly set up in Azure where a registration app is used to represent the browser app, and another registration app is used to describe the API I need to contact.
The API
The API server is an ASP.NET Core 3.1 C# application whose Startup.cs is:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();
app.UseAuthorization();
}
}
I have removed all the extra code and left the parts that concern auth.
The controller I am contacting is:
[ApiController]
[Route("controller")]
public class MyController : ControllerBase
{
[HttpGet("test/")]
[Authorize(Roles = "Admin")]
public async Task<string> Test()
{
return "Ok";
}
[HttpGet("test2/")]
[Authorize]
public async Task<string> Test2()
{
return "Ok";
}
[HttpGet("test3/")]
public async Task<string> Test3()
{
return "Ok";
}
}
Azure
The setup in Azure is simple: apart from the two app registrations for the browser app and the API, I have set in the browser app registration some custom roles and assigned them to some users.
I authenticate in the browser app using a user who has the Admin app role assigned to it.
The problem
When my client app tries to fetch data using these endpoints:
/controller/test3
/controller/test2
Everything is fine as one is unprotected and the other one uses a simple [Authorize].
However when trying to fetch from /controller/test, I get 403 (Forbidden).
Why can't I make the roles work?
More info
While debugging when fetching test2, I can see, in the controller, that this.User is present and there are several claims. Among those claims, I cannot see anything relating to the role. The access token I get has the following form:
{
"aud": "api://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"iss": "https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/",
"iat": 1607034050,
"nbf": 1607034050,
"exp": 1607037950,
"acr": "1",
"aio": "AWQAm/8RAAAAH1j5tZzINJFi5fsMsgf99gcrnqQA+dOhWBpFmsgy3jsr0pFJ0AxvenqthiNLmRqKzqx6l+9SuLlRniAVCTOoqEE7MonnOetO3h7g1/Bm520rS0qiX/gpCCWYm/UwDlJ+",
"amr": [
"pwd"
],
"appid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"appidacr": "0",
"email": "xxx#xxx.xxx",
"idp": "https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/",
"ipaddr": "xxx.xxx.xxx.xxx",
"name": "XXX",
"oid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"rh": "0.AAAASupzDdEU8EyBI3R6nFeJQHVORvhJZ2hDjJoEO5yPUcZ0AEU.",
"scp": "Users.Read",
"sub": "frR45l2dTAIyXZ-3Yn2mGNbBcBX9CrGisgJ4L8zOCd4",
"tid": "0d73ea4a-14d1-4cf0-8123-747a9c578940",
"unique_name": "xxx#xxx.xxx",
"uti": "39dk-rAAP0KiJN5dwhs4AA",
"ver": "1.0"
}
As you can see, no claim relating to roles.
But note that I can successfully get the role in the user token I get when authenticating. I need that claim to flow in the access token too when I use it to contact the API. How?
I found your problem:
You cannot set a custom role in the manifest of the browser application. You need to set a custom role in the manifest of the api application, and then assign the role to the user.
Then you need to use the auth code flow to get the access token, and the roles claims will be included in it.
In fact, the access token is created based on the intended recipient of your token (ie your api). If you want to access the api, you must have permissions or roles. In your question, this role is you Custom, when you grant the user a custom role of the api application, then the user has the role that can access the api application. (Note that this is not to assign the custom role of the client application to the user, because you need to access the api application, so the role of api application is required), then the user can log in to the client application and request a token from the api application. When you obtain the token and use the token to access the API, the API only needs to verify whether the user you log in has the role to access it.
This was a hard nut to crack and is not available in auth0 by default.
You can set roles in the Auth0 id token for OpenId, But you have to write a rule in auth0 dashboard:
Go To Rules section on your Auth0 dashboard and create a new rule:
and then use this code to add the user role claims to the id token, that will be returned in the JWT token claims:
function addRolesToAccessToken(user, context, callback) {
const namespace = 'http://schemas.microsoft.com/ws/2008/06/identity/claims';
const assignedRoles = (context.authorization || {}).roles;
let idTokenClaims = context.idToken || {};
let accessTokenClaims = context.accessToken || {};
idTokenClaims[`${namespace}/role`] = assignedRoles;
accessTokenClaims[`${namespace}/role`] = assignedRoles;
context.idToken = idTokenClaims;
context.accessToken = accessTokenClaims;
return callback(null, user, context);
}
Hope this helps! Cheers

Authorisation using MSAL for a function in Azure Static Web App

I'm trying to authenticate and authorise a user for an Azure function that's created and exposed using an Azure Static Web App, using Azure AD and MSAL. The user can succesfully access the API if I configure the app to use the older AAD v1 flow but not with MSAL. The setup/use-case:
a Single Page Application (SPA) deployed and hosted as an Azure Static Web App using basic HTML and JS (this is a demo 'Hello World' app)
The app has authentication integrated using MSAL. Specifically msal-browser.js version 2.6.1. An identity token is retrieved using:
msal.PublicClientApplication(msalConfig).loginPopup(loginRequest)
where msalConfig contains:
```
auth: {
clientId: "<CLIENTID>",
authority: "https://login.microsoftonline.com/<TENANT_ID>"
}
```
The user is authenticated and an identity token returned.
The static web app exposes a sample function GetMessage which returns some dummy text
If the route to the function is unprotected the SPA can call the function successfully and the text is returned to the browser/SPA
If the route to the function is protected via routes.json the request to the function (correctly) returns a 401 unless the user is authenticated and authorised.
{
"routes": [
{
"route": "/api/*",
"allowedRoles": ["Authenticated"]
}
]
}
To authenticate the user via MSAL I am attempting to retrieve an access token which I put into the Bearer header of the function call:
```
async function getAPI() {
const currentAcc = myMSALObj.getAccountByHomeId(accountId);
if (currentAcc) {
const response = await getTokenPopup(silentRequest, currentAcc).catch(error => {
console.log(error);
});
console.log("Got token " + response.accessToken)
const accToke = response.accessToken
const headers = new Headers();
const bearer = `Bearer ${accToke}`;
headers.append("Authorization", bearer);
const options = {
method: "GET",
headers: headers
};
let { text } = await( await fetch('/api/GetMessage',options)).json();
document.querySelector('#name').textContent = text;
}
}
```
The token is retrieved and validates in jwt.ms but the function always returns 403 - forbidden. It appears to make no difference if change scopes or user roles although it's possible there's a magic combination I'm missing.
This process works perfectly if the function I'm calling is the Micrsoft Graph - i.e. https://graph.microsoft.com/v1.0/me - it's only failing on our own static web apps function. I can't see a way of accessing logs on the Azure server side to understand why it might be failing.
Using the AAD v1 flow i.e. calling http://APP_URL/.auth/login/aad works perfectly - but it doesn't use the access token. It uses a Cookie called StaticWebAppsAuthCookie (a single call to APP_URL/.auth/login/aad is enough to authenticate and authorise the user). An example of that can be found here
I understood that MSAL was the flow Azure AD was moving toward so is there way to authorise the user via an MSAL flow? Speficially using Azure AD, a static web app and a function exposed within the static web app (not as a standalone Azure Function app).
I believe when calling an Azure Function API contained within your Static Web App, the service inserts its own auth token into the header. If you are relying on the Authorization Bearer header to pass your token from app to api, it may be overwritten.
In my case, I was sending token:
{
"typ": "JWT",
"alg": "RS256",
"kid": "{key value}"
}
{
"iss": "https://{domain}.b2clogin.com/{tenant ID}/v2.0/",
"aud": "{client ID}",
...
}
but my Azure Functions API was receiving token:
{
"typ": "JWT",
"alg": "HS256",
}
{
"iss": "https://{ID}.scm.azurewebsites.net",
"aud": "https://{ID}.azurewebsites.net/azurefunctions",
...
}
This issue is being tracked here: https://github.com/Azure/static-web-apps/issues/34

Enabling Azure AD Auth for Swagger UI hosted in Service Fabric

We enabled Swagger for Web API application which is hosted on Azure Service Fabric. We want to enable security on Swagger UI. So I followed below url which enables security –
https://blogs.msdn.microsoft.com/pratushb/2016/04/28/enable-swagger-to-authenticate-against-azure-ad/
https://github.com/domaindrivendev/Swashbuckle/issues/671 (response from Oleksandr-Tokmakov)
I could see the “Available authorizations” popup and I could see AAD authentication done successfully on another tab on click of Authorize button. But once authentication completed, I see the token not returns back to swagger ui and the authentication tab not closes.
Below is the code I used. (I created two AAD, one for Web Services hosted on Service Fabric and another for Swagger UI)
config.EnableSwagger(
c =>
{
c.SingleApiVersion("v1", "Title of Service");
c.OAuth2("oauth2")
.Description("OAuth2 Implicit Grant")
.Flow("implicit")
.AuthorizationUrl("https://login.microsoftonline.com/tenentId-guid/oauth2/authorize")
.Scopes(scopes =>
{
scopes.Add("user_impersonation", "Access Services Local Swagger Secure");
});
c.OperationFilter<AssignOAuth2SecurityRequirements>();
}
).EnableSwaggerUi(c =>
{
c.EnableOAuth2Support(
clientId: "Swagger AAD application Client Id",
clientSecret: "Swagger AAD application Key",
realm: "https://localhost:444/swagger/ui/o2c-html",
appName: "https://serviceslocal/swagger/", // Free text, no reference to AAD
scopeSeperator: "",
additionalQueryStringParams: new Dictionary<string, string>() { { "resource", "Web API AAD application Client Id" } }
);
}
);
public class AssignOAuth2SecurityRequirements : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
// Correspond each "Authorize" role to an oauth2 scope
var scopes = apiDescription.ActionDescriptor.GetFilterPipeline()
.Select(filterInfo => filterInfo.Instance)
.OfType<AuthorizeAttribute>()
.SelectMany(attr => attr.Roles.Split(','))
.Distinct();
if (scopes.Any())
{
if (operation.security == null)
operation.security = new List<IDictionary<string, IEnumerable<string>>>();
var oAuthRequirements = new Dictionary<string, IEnumerable<string>>
{
{ "oauth2", scopes }
};
operation.security.Add(oAuthRequirements);
}
}
}
I'm using Auth0, but here's what I got for OAuth2 that works for me.
options.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = Path.Combine(auth0Settings["Authority"].Value, "authorize")
});
app.UseSwaggerUI(c =>
{
c.RoutePrefix = "docs";
c.InjectOnCompleteJavaScript("/swagger-ui.js");
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Easy Streak API");
c.ConfigureOAuth2(auth0Settings["ClientId"].Value, auth0Settings["ClientSecret"].Value, auth0Settings["Authority"].Value, "EasyStreak API");
});

Resources