Using Active Directory B2C to secure a Blazor WASM app and Web API - active-directory

I have a scenario where I have a Blazor WASM (client only) app that is secured via AD B2C. As per the documentation I have read, I have registered an application in AD B2C (e.g. BlazorApp) and wired the Blazor app to this instance. This Blazor app makes API calls to a .NET Core Web API where the endpoints are secured (using the [Authorize] attributes). As per the documentation I have read, I have also registered the Web API in AD B2C (e.g. WebApi) and wired the API to connect to this instance.
The problem I have is that when I authenticate in the Blazor app, and then pass the access/id token through the API calls, the Web API can't authenticate that token (unauthorized error response). It works when I wire the Web API to connect to the BlazorApp registration (I'm guessing because that is where the token was issued from). But this seems to go against the recommendation of registering each app/api in AD as a separate registration.
How can I get the WebApi registration to recognise a token issued by BlazorApp? Am I going about this wrong and should I just wire the Web API to talk to the BlazorApp instance?
Additional Information:
Blazor WASM (client) - Program.cs
(sensitive information removed)
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
...
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add(#"https://<tenant name>.onmicrosoft.com/blazorapp/app.read");
options.ProviderOptions.DefaultAccessTokenScopes.Add(#"https://<tenant name>.onmicrosoft.com/blazorapp/app.write");
});
...
await builder.Build().RunAsync();
}
Blazor WASM (client) - appsettings.json
(sensitive information removed)
{
"AzureAdB2C": {
"Authority": "https://<tenant name>.b2clogin.com/<tenant name>.onmicrosoft.com/B2C_1_SignUpSignIn",
"ClientId": "<BlazorApp Application ID>",
"ValidateAuthority": false
}
}
Web API (.NET Core 3.1) - StartUp.cs
(sensitive information removed)
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication(AzureADB2CDefaults.BearerAuthenticationScheme)
.AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
services.AddControllers();
...
}
Web API (.NET Core 3.1) - appsettings.json
(sensitive information removed)
NOTE: Authentication works when I replace the WebApi Application ID with the BlazorApp Application ID
{
"AzureAdB2C": {
"Instance": "https://<tenant name>.b2clogin.com/tfp/",
"ClientId": "<WebAPI Application ID>",
"Domain": "<tenant name>.onmicrosoft.com",
"SignUpSignInPolicyId": "B2C_1_SignUpSignIn"
},
...
}

Your project is not using OBO flow, OBO flow requires three applications, specifically one client-side and two api-side, see here.
Back to the problem, first of all your problem is misconfigured scope, as the api-side app needs to expose its own api, the API permission on your blazor app side gets access to the api-side, so the access to the scope is configured on the api-side, so we need to put https://<tenant name>.onmicrosoft.com/webapiapp/app.read on the scope, your blazor app is only used to configure access.
It works because you use blazor app to request the blazor app's own api, which is feasible, although a little strange, to request yourself.
So for your project, the correct request should be like this.
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
...
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add(#"https://<tenant name>.onmicrosoft.com/webapiapp/app.read");
options.ProviderOptions.DefaultAccessTokenScopes.Add(#"https://<tenant name>.onmicrosoft.com/webapiapp/app.write");
});
...
await builder.Build().RunAsync();
}
{
"AzureAdB2C": {
"Instance": "https://<tenant name>.b2clogin.com/tfp/",
"ClientId": "<Application ID>",
"Domain": "<tenant name>.onmicrosoft.com",
"SignUpSignInPolicyId": "B2C_1_SignUpSignIn"
},
...
}

Related

ASP Net Core API downstream call to Graph API W/ AADB2C

I have an ASP Net Core API where I want to call Graph API. I configure the Authentication as such:
services.AddMicrosoftIdentityWebApiAuthentication(Configuration, configSectionName: Constants.AzureAdB2C)
.EnableTokenAcquisitionToCallDownstreamApi(options => Configuration.Bind(Constants.AzureAdB2C, options))
.AddMicrosoftGraph(Configuration.GetSection("GraphAPI"))
.AddInMemoryTokenCaches();
My appsettings.json file has the following properties:
{
"AzureAdB2C": {
"Instance": "https://tenantName.b2clogin.com/",
"Domain": "tenantName.onmicrosoft.com",
"TenantId": "tenantId",
"ClientId": "appId",
"ClientSecret": "appSecret",
"SignUpSignInPolicyId": "B2C_1_SignUpSignIn",
"ResetPasswordPolicyId": "B2C_1_PasswordReset"
},
"GraphAPI": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "User.Read Directory.ReadWrite.All"
}
}
My b2c app is granted permission to these Graph scopes.
I created an endpoint:
[HttpGet]
[Route("me")]
public Task<User> Me()
{
return this.graphServiceClient.Me.Request().GetAsync();
}
This is where I get this error:
ErrorCode: unsupported_grant_type
Microsoft.Identity.Client.MsalServiceException: AADB2C90086: The supplied grant_type [urn:ietf:params:oauth:grant-type:jwt-bearer] is not supported.
Why can't my API call GraphAPI? All samples that I saw used services.AddMicrosoftIdentityWebAppAuthentication.... Could that be the reason?
On-behalf-of flow in B2C is not supported: https://learn.microsoft.com/en-us/azure/active-directory-b2c/access-tokens.
Web API chains (On-Behalf-Of) is not supported by Azure AD B2C.
You need to acquire the token using application permissions as your application with client credentials flow.
There is some documentation on that: https://learn.microsoft.com/en-us/azure/active-directory-b2c/microsoft-graph-get-started?tabs=app-reg-ga#register-management-application.
The documentation creates a separate app registration for doing that though I think you can just add the app permissions to your existing registration.

Blazor Client side get CORS error when accessing Azure Function using Azure Active directory

I've been trying to deploy my Blazor PWA for 2 days without any success so far, if someone has an idea of what I’m doing wrong I would be really grateful.
hello
I could resolve most of my issues by myself but I'm now stuck on a CORS problem using AAD.
Here's my project setup:
Blazor webassembly client hosted on Static Website Storage (works
great), Net 5
AzureFunctions connected to an Azure Sql Server database (works great
with anonymous authentication in Blazor)
Azure Active Directory I want to use to authenticate the users.
(protecting both the blazor app and the functions)
So I’ve created an App registration, added my static web site address as SPA uri and uncheck both implicit.
In my blazor client, program.cs, I’ve added the following code to connect to AAD:
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); //contains clientId, Authority
options.ProviderOptions.DefaultAccessTokenScopes.Add("https://graph.microsoft.com/User.Read");
options.ProviderOptions.LoginMode = "redirect";
});
That works great too, I can login, authorize view works as expected.
The problem is when I try to authenticate Azure functions with «Login with Azure Active Directory»,
I' tried with both express and advanced configurations (using clientId, tenantId) but when I
Access to fetch at 'https://login.windows.net/tenantid/oauth2/authorize ... (redirected from 'https://myfunctionstorage.azurewebsites.net/api/client/list') from origin 'https://*****9.web.core.windows.net' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
I have of course enabled CORS for my Blazor Client Address on Azure Functions configuration but the problem seems to be about the login windows uri...
Also, if I enable the token id implicit flow in the App registration and access the login url in the browser it works perfectly fine.
All the examples I could find so far are about msal 1.0 and App registration using implicit flow instead of SPA so it doesn't help...
Thank you for your time and your help.
UPDATE:
I’ve done more researches since yesterday and I think it could by related to my HTTPClient, currently I use the basic one (with just a base adress).
But I’ve seen on some example that when using the Client using AAD it needs more parameters, for example:
builder.Services.AddHttpClient("companiesAPI", cl => { cl.BaseAddress = new Uri("https://localhost:5001/api/"); }) .AddHttpMessageHandler(sp => { var handler = sp.GetService<AuthorizationMessageHandler>() .ConfigureHandler( authorizedUrls: new[] { "https://localhost:5001" }, scopes: new[] { "companyApi" } ); return handler; });
Is that AuthorizationMessageHandler needed ?
Also I see some references to the need of using the «use_impersonation» scope.
Are those changes (on HttpClient and the use_impersonation scope) also required when using msal2/SPA app registration ?
Thank you
If want to call the Azure function projected by Azure AD in Blazor webassembly client, please refer to the following steps
If you want to call Azure function projected by Azure AD in angular application, please refer to the following code
Create Azure AD application to protect function
Register Azure AD application. After doing that, please copy Application (client) ID and the Directory (tenant) ID
Configure Redirect URI. Select Web and type <app-url>/.auth/login/aad/callback.
Enable Implicit grant flow
Define API scope and copy it
Create client secret.
Enable Azure Active Directory in your App Service app
Configure CORS policy in Azure Function
Create Client AD application to access function
Register application
Enable Implicit grant flow
configure API permissions. Let your client application have permissions to access function
Code
Create Custom AuthorizationMessageHandler class
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: new[] { "<your function app url>" },
scopes: new[] { "<the function app API scope your define>" });
}
}
Add the following code in Program.cs.
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
// register CustomAuthorizationMessageHandler
builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
// configure httpclient
// call the following code please add packageMicrosoft.Extensions.Http 3.1.0
builder.Services.AddHttpClient("ServerAPI", client =>
client.BaseAddress = new Uri("<your function app url>"))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
// register the httpclient
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("ServerAPI"));
// configure Azure AD auth
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("<the function app API scope your define>");
});
await builder.Build().RunAsync();
}
Call the API
#page "/fetchdata"
#using Microsoft.AspNetCore.Components.WebAssembly.Authentication
#inject HttpClient Http
<h1>Call Azure Function</h1>
<p>This component demonstrates fetching data from the server.</p>
<p>Result: #forecasts</p>
<button class="btn btn-primary" #onclick="CallFun">Call Function</button>
#code {
private string forecasts;
protected async Task CallFun()
{
forecasts = await Http.GetStringAsync("api/http");
}
}

Secure .Net Core 3 Web API with AAD Token

Due to some technical constraints, we are doing Username/Password AAD authentication when user login.
Users will input their username and password into our custom login page and our application calls IPublicClientApplication.AcquireTokenByUsernamePassword.
I'm planning to use the returned token to call another Web API application(also connecting to the same AAD). In the Web API application, I did the following:
Added the following code in startup services.AddAuthentication(AzureADDefaults.JwtBearerAuthenticationScheme).AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
include the following settings in my appsettings.json file
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "<Application ID>",
"TenantId": "<Tenant ID>"
}
Secure my web api using [Authorize]
I then use Postman to construct a call to the Web API based on the returned token. I included Authorization: Bearer <JWT Token>. The Web API returns
Bearer error="invalid_token", error_description="The signature is invalid"
My questions are
Can Web API application validate the username/password acquired token?
If the token can be validated in Web API application, how can I do it since I'm getting the above error?
I test in my site and it work well, you could refer to the following steps:
1.Register Webapi app in azure ad.
2.Click Expose an API and Add a scope e.g. webread.
3.Click Manifest, change accessTokenAcceptedVersion to 2.0.
4.In visual studio webapi ConfigureServices:
services.AddAuthentication(AzureADDefaults.JwtBearerAuthenticationScheme).AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme,
options =>
{
options.Authority += "/v2.0";
options.TokenValidationParameters.ValidAudiences = new[]
{
options.Audience,
$"api://{options.Audience}"
};
});
5.Register client app in azure ad.
6.Click Authentication, set Default client type as Yes.
7.Click Api Permission>Add a permission, select My APIs and choose the webapi your registered before.
8.In visual studio client app, set scope with webread:
string[] scopes = new string[] { "api://1890e822-xxxxxxxxxxxxxxxx/webread" };
Hope it helps you.
From the document you provided you are using MSAL to get access token using Resource Owner Flow in Azure AD V2.0 endpoint .
From document , when validating access token which issued from Azure AD V2.0 , you should add /v2.0 to Authority :
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
// This is a Microsoft identity platform web API.
options.Authority += "/v2.0";
// The web API accepts as audiences both the Client ID (options.Audience) and api://{ClientID}.
options.TokenValidationParameters.ValidAudiences = new []
{
options.Audience,
$"api://{options.Audience}"
};
});

Ocelot Integration with Azure Active Directory Authentication .Net Core 3.1

Context: My company is moving to an API Gateway to manage all our or services in our network. Each service is currently authenticated using azure AD. Everything is single tenant and just allows company users.
Question: Am I understanding this correctly?
Problem: When I call http:localhost:5000/authResource/get I get a 401 response back from Ocelot. After tracing through the error, I see that I get null back when the Ocelot Authentication middle ware attempts to authenticate using the AzureADJwtBearer scheme.
I followed the advice from questions related to Ocelot and azure ad but even following that I am unable to get anything to work. (Mostly from this answer: How set up Ocelot Api Gateway with Azure Active Directory)
I think I may be misunderstanding how the authentication is supposed to work. My current understanding is that I tell Ocelot to authenticate against one of my Apis using the AzureADJwtBearer scheme. In my configuration I have the information for that specific api set up (ClientId, TenantId, etc).
Once I call the route, I am expecting Ocelot to make a call to Microsoft to start authentication. At this point, I expect the same flow as the Apis provide, i.e. I make the call and get the Microsoft login page which then redirects me back to the application once I enter my username and password.
After redirecting me back to Ocelot I guess (this is the part I am fuzzy on), I expect ocelot to store the access token Microsoft sends back FOR THE SPECIFIC RESOURCE I JUST REQUESTED. Then I expect ocelot to attach the token to an Auth header and then send the request for the resource I initially asked for.
To clarify this, I will include the code I have for my startup files, and my ocelot.json file.
From Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddProtectedWebApi(Configuration)
.AddProtectedApiCallsWebApis(Configuration)
.AddInMemoryTokenCaches();
services.AddOcelot(Configuration);
services.AddControllers();
}
Also from Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseOcelot().Wait();
app.UseAuthentication();
}
My ocelot.json file for the authenticated resource is as follows (names changed for security reasons):
{
"DownstreamPathTemplate": "/api/Controller/Get",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 5003
}
],
"UpstreamPathTemplate": "/authResource/get",
"UpstreamHttpMethod": [ "GET" ],
"AuthenticationOptions": {
"AuthenticationProviderKey": "AzureADJwtBearer",
"AllowedScopes": []
}
}
To solidify how I understand this, I will go through an example using the sample api shown in the ocelot configuration.
I wish to access resource http://localhost:5003/api/Controller/Get which is a protected API meaning I can only get a response from this if I provide an authorization header with my get request.
I make a request through my ocelot gateway for the url http://localhost:5000/authResource/get (I am hosting Ocelot on localhost:5000).
Ocelot sees it needs to authenticate to access this resource, so it makes a request using the AzureADJwtBearer scheme.
I am redirected to microsoft to login. Once done, I am sent back to the Ocelot application with an access token in tow.
Ocelot takes this taken, creates the Auth header, and finally calls http://localhost:5003/api/Controller/Get and returns the result.
Working Example for .Net Core 3.1
I ended up getting this working using the Microsoft.Identity.Web library (current version located at https://github.com/AzureAD/microsoft-identity-web/tree/master/src/Microsoft.Identity.Web)
First up, my ocelot configuration file (ocelot.json):
{
"ReRoutes": [
{
"DownstreamPathTemplate": "/myapp/api/{everything}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "apphost.com",
"Port": 443
}
],
"UpstreamPathTemplate": "/api/{everything}",
"UpstreamHttpMethod": [ "GET", "POST" ],
"AuthenticationOptions": {
"AuthenticationProviderKey": "Bearer",
"AllowedScopes": []
}
}
]
}
Notice the AuthenticationProviderKey has the value Bearer.
My appsettings.json file contains my azure configurations:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "mydomain.com",
"TenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
Then finally, my Startup.cs file contains something like this (reduced for brevity)
public void ConfigureServices(IServiceCollection services)
{
services.AddProtectedWebApi(Configuration)
.AddProtectedApiCallsWebApis(Configuration)
.AddInMemoryTokenCaches();
services.AddOcelot(Configuration);
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
IdentityModelEventSource.ShowPII = true;
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseOcelot().Wait();
app.UseAuthentication();
}
With this all set up, I can pass in a bearer token generated using my azure credentials, and the ocelot gateway will correctly validate it.

Defining the Authority

Currently I'm doing the following
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration.GetValue<string>("IdentityServer:Authority");
options.RequireHttpsMetadata = Configuration.GetValue<bool>("IdentityServer:RequireHttps");
options.ApiName = Configuration.GetValue<string>("IdentityServer:UserManagementApi:ResourceName");
options.SaveToken = true;
})
.AddCookie(cfg => cfg.SlidingExpiration = true);
Where I'm getting the Authority from the config file. However, we are upgrading our app to be multi-tenant and need to set these values depending on the tenant being called. Is it possible to set these values from a service that will find the tenant at runtime using IOC.
We are using Dotnet Code 2.2 and Lamar IOC.
There is what appears to be a duplicate of this, however, I have the following issue as we are running a number of microservices.
Each Tenant has it's own IdentityServer4 tenant, that uses it's own database. This is a standalone service to manage the users.
Each Api uses it's own tenanted IdentityServer api to authenticate.
So
Api Site ----> connects to IdentityServer4 Site
eg.
api1.company1.com connects to auth.company1.com to authenticate
api2.company1.com connects to auth.company1.com to authenticate
api1.company2.com connects to auth.company2.com to authenticate
api2.company2.com connects to auth.company2.com to authenticate
Each Api will call other api's and pass the user token on to authenticate. I need a way to tell the api what auth to use to authenticate. Using a shared Authority, when getting the token you get the following
{
"nbf": 1556619889,
"exp": 1556623489,
"iss": "http://shared-domain-for-every-tenant,
"aud": [
"http://shared-domain-for-every-tenant/resources",
"api1",
"api2"
],
"client_id": "postman",
"scope": [
"api1",
"api1"
]
}

Resources