Using Microsoft Authentication Library with External Providers (ASP.NET 6) - asp.net

I have an Azure App Service developed in .NET 6. I wish to authenticate users against several providers (Azure AD, Google, etc). Upon following the (I think) accurate tutorial I was able to authenticate against Microsoft accounts using MSAL with the following code:
var builder = WebApplication.CreateBuilder(args);
// Managed identity credential
var credential = new DefaultAzureCredential();
// Add key vault for initial access
builder.Configuration.AddAzureKeyVault(new Uri(builder.Configuration.GetValue<string>("Azure:VaultUri")), credential);
// Integrate key vault into Azure app config
builder.Configuration.AddAzureAppConfiguration(options =>
{
// Load config string from vault
var configConnection = builder.Configuration.GetValue<string>("config");
// Connect to config
options.Connect(configConnection);
options.ConfigureKeyVault(kv => kv.SetCredential(credential));
});
// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// CUSTOM DI
builder.Services.AddSingleton<IDatabaseRepo, CosmosDatabaseRepo>();
// Build
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
The protected controller method is simply decorated with [Authorize].
That works great for Microsoft accounts. However, every attempt to integrate Google into this flow fails. I've done the following:
Follow various example utilizing the Microsoft.AspNetCore.Authentication.Google package. I added AddGoogle method with the client id and secret set. This results in an invalid token error.
The OAuth process works (using Postman) and provides both an access token and id token. But:
The access token fails with invalid_token
The open id token fails saying invalid_token and invalid signature.
If I remove AddMicrosoftIdentityWebApi and only use the AddGoogle method then I always receive a 500 Internal Server Error indicating an auth scheme was not defined. Doesn't matter where I define it- whether in AddAuthorization or decorated on the controller's Authorize attribute.
Attempted using manual JWT validation. This seems problematic because:
It also results in the 500 error whatever I try.
It's not supposed to be required if I understand the packages correctly.
ALSO:
The Google client id has been added to my App Service as an identity provider with the associated App (client) ID.
There's 38974 tutorials out there and none seem to point in the right direction/don't address this issue. Any and all guidance is appreciated.

Related

WebAssembly Razor client app return 401 while calling a protected API in Azure with Admin Role

I have API protected by Azure AD where Authentication is required to get access data, where I expose the API with only one simple scope for now.
The API and client app both are registered in Azure AD.
Roles are also configured for the API, only a a user with Admin role can call my API.
Do I need to assign this Admin role as well to the client App? or AccessApi scope is enough?
Scopes Who can consent Admin consent display name User consent display name State
api://xx User AccessApi AccessApi Enable
And a client application build using webassembly blazor also registered in Azure AD, and its configured with Api permission to use delegated access to AccessApi.
API / Permissions name Type Description Admin consent required Status
myApi (1)
AccessApi Delegated AccessApi No
I configured webassembly blazor client application to authenticate against azure and get token and use that token to call myApi, however I keep getting loading but no data is being displayed without any error.
Im not sure what went wrong here ?
program class of client application:
private static string scope = #"api://xxx/AccessApi";
...
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddScoped<GraphAPIAuthorizationMessageHandler>();
builder.Services.AddHttpClient("ServerAPI",
client => client.BaseAddress = new Uri("https://localhost:44314"))
.AddHttpMessageHandler<GraphAPIAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("ServerAPI"));
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
});
At fetch data razor page I imported all necessary libraries
#using Microsoft.AspNetCore.Authorization
#using Microsoft.AspNetCore.Components.WebAssembly.Authentication
#inject IAccessTokenProvider TokenProvider
#attribute [Authorize]
#inject NavigationManager UriHelper
#inject HttpClient Http
...
data = await Http.GetFromJsonAsync<data[]>(#"API-url--runs-locally-on-docker");
The authorization message handler class
private static string scope = #"api://xxx/AccessApi";
public GraphAPIAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: new[] { "https://localhost:44314" },
scopes: new[] { scope });
}
After authenticated myself with Azure AD account, the client app shows loading.. but no data is being displayed.
at the console level shows this error.
Failed to load resource: the server responded with a status of 401 (Unauthorized)
I followed Microsoft documentation and I'm not sure what I'm missing here.
what could be wrong here ?
Update
The Api expose tab:
Api permission for the client app:
Obviously, your scope is set incorrectly. You should set the scope to the client id of the api application instead of the client id of the client application. In your question, I think your scope should be: api://7b4d6df9-63cd-4ed7-881bxxx/AccessApi.
Parse the token and you should see scp claim and roles claim.

Microsoft Identity Web invalid token in Blazor WASM

I am just finished trying to implement this: https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2/tree/master/1.%20Desktop%20app%20calls%20Web%20API
Instead of using a WPF application I am using a Blazor WASM client.
I added the token to the outgoing requests to my .net core API but I always get a 401.
Blazor authenticates well and gets the token back, seems to be working fine but:
the audience has the wrong GUID
"scp" (scope) is missing, hence the token being invalid for usage
If I run the sample from the link mentioned above and decode the token I can see a correct AUD & SCP in the token. So it's probably something with my configuration in Blazor?
Config in Blazor
// AD authentication
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("api://xxx-xxxx-xxxx-xxxx/access_as_user");
});
builder.Services
.AddHttpClient<IApiClient, ApiClient>(client => client.BaseAddress = _baseUri)
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
Response
Bearer error="invalid_token", error_description="The audience '63ee4227-xxxx-xxxx-xxxx' is invalid"
The audience GUID is the clientID of my Blazor app registration
Code in Startup.cs
...
services.AddProtectedWebApi(Configuration)
.AddInMemoryTokenCaches();
...
...
app.UseAuthentication();
app.UseAuthorization();
...
Any idea what could have been wrong?
When I was learning to use Blazor WebAssembly I followed this tutorial which does something very similar and it may be easier for you to implement as you don't have to try and convert any codefrom WPF.
However, one thing to try is to change this:
options.ProviderOptions.DefaultAccessTokenScopes.Add("api://xxx-xxxx-xxxx-xxxx/access_as_user");
To this:
options.ProviderOptions.DefaultAccessTokenScopes.Add("xxx-xxxx-xxxx-xxxx/access_as_user");
In my application I am calling two API endpoints, one is hosted within the same domain as the Blazor application and the other is on a separate domain. When requesting a token for the external domain the api:// prefix must be used. However, for the same domain it must be excluded.

MSAL - Application can request permission even if it's not configured in 'API Permissions'

I have created a small .NET Core 3.1 console application using the MSAL library which requests scope api://55a047a1-a0d1-4b6b-9896-751a848e1e06/testscope2
Custom API exposes two scopes
api://55a047a1-a0d1-4b6b-9896-751a848e1e06/testscope1
api://55a047a1-a0d1-4b6b-9896-751a848e1e06/testscope2
I have also configured another application named test-app in Azure Active Directory which represents my console application.
I have configured only one API permission (api://55a047a1-a0d1-4b6b-9896-751a848e1e06/testscope1) for this application. My understanding is with this configuration in place client app will only be able to request for scope test1 and it won't allow test-app to request for scope2
Below is screenshot
This is my code:
//<PackageReference Include="Microsoft.Identity.Client" Version="4.13.0" />
namespace console_client
{
class Program
{
static void Main(string[] args)
{
#region Azure AD parameters
var clientId = "dddeefa5-d95c-4931-a53d-2382deee27c3";
var tenant = "-- MY TENANT ID--";
var instance = "https://login.microsoftonline.com/";
#endregion
var client = PublicClientApplicationBuilder
.Create(clientId)
.WithDefaultRedirectUri()
.WithAuthority($"{instance}{tenant}")
.Build();
List<string> scopes = new List<string>();
try
{
// I was under impression that this call will throw as exception as
// this app is requesting 'testscope2' which is not included in API Permissions
// while configuring test-app in Azure Active Directory (dddeefa5-d95c-4931-a53d-2382deee27c3 )
// But I was able to retrieve token back with testscope2 in it.
scopes.Add("api://55a047a1-a0d1-4b6b-9896-751a848e1e06/testscope2");
var authenticationResult = client.AcquireTokenInteractive(scopes).ExecuteAsync().Result;
Console.WriteLine($"Interactive Access token is : {authenticationResult.AccessToken}");
}
catch (Exception ex)
{
System.Console.WriteLine($"******* {ex.Message}");
}
}
}
}
Question
Am I missing anything? Why am I getting the access token back even if the app doesn't have permission configured?
Thanks
TL;DR it is a feature.
With the v2 endpoint / MSAL, you can request for scopes that are not defined in your app manifest.
The ones in your app registration are the static permissions required by your application.
But your application can also request dynamic permissions at login time.
The user/admin would still need to consent to that of course, an app won't get a permission without consent.
Your app seems to be a single-tenant app so this doesn't really make a difference for you.
It is mainly for multi-tenant SaaS applications that can require the minimum needed permissions in the app registration/manifest, and then request more permissions for opt-in features as they are needed.
By the way, if you want to use the permissions defined in your app registration, you can request a special scope: api://55a047a1-a0d1-4b6b-9896-751a848e1e06/.default (your app ID URI or client id + "/.default").
This will make AAD look at your app registration to decide which permissions to check consent for.

How to test Azure Active Directory locally (reply URLs)

I have promoted a test .NET Web Api to an Azure application service and included an app registration under Azure Active Directory. I then went to do some testing locally and noticed that Azure wanted to use the reply URL in the app registration after login. The reply URL in the app registration is the URL for the application service. My local instance will be something like https://localhost:44377/. How are you supposed to test changes locally after doing an initial deploy to Azure? All I can think to do is create another app registration for testing, use my local host reply URL, then update my web.config to point to that development app registration. Then prior to publishing again, update the web.config back to the other app registration.
Below is the code I used for authentication which was based on the standard template from a simple MVC project. The values app registration are being used for the redirect URL but maybe I am supposed to override those values below while testing?
public class AccountController : Controller
{
public void SignIn()
{
// Send an OpenID Connect sign-in request.
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
If you want to test locally, just add localhost as a reply URL and ensure that the web.config also lists localhost.
Please refer to this repository if you have not done so already: https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect
You can have multiple reply urls by specifying which you want to use in the authentication request. You do that when configuring your authentication in Startup.cs. You need to add a RedirectUri to your OpenIdConnectAuthenticationOptions.Notifications.RedirectToIdentityProvider
var openIdOptions = new OpenIdConnectAuthenticationOptions
{
//...
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = (context) =>
{
context.ProtocolMessage.RedirectUri = "<current reply uri>";
return Task.FromResult(0);
}
}
// ...
};
That reply uri can be pulled from your web.config or generated dynamically using context.Request.
If you want to use a different AD App after going to production, you can have two apps and put the client id and secret in the web.config.

Using Identity Server 3, ClaimsPrinciple null even after successful bearer token authentication

I have a test console app which I'm pointing at a local instance of Identity Server 3 to request an access token. The following code does this and returns my token fine (passing a single scope "scope.test.client").
static TokenResponse GetClientToken(string clientId, string clientSecret, string[] scopes)
{
var uri = new Uri(string.Concat(ID_BASE_URI, ID_URL_TOKEN));
var client = new TokenClient(
uri.AbsoluteUri,
clientId,
clientSecret);
return client.RequestClientCredentialsAsync(string.Join(" ", scopes)).Result;
I then use this token to call an API also running locally. This takes the TokenResponse obtained above and passed it to this method:
static void CallApi(string url, TokenResponse response)
{
try
{
using (var client = new HttpClient())
{
client.SetBearerToken(response.AccessToken);
Console.WriteLine(client.GetStringAsync(url).Result);
}
}
catch (Exception x)
{
Console.WriteLine(string.Format("Exception: {0}", x.Message));
}
}
The API (an ASP.NET WebApi project) uses an Owin Startup class to enforce bearer token authentication for all requests:
appBuilder.Map(baseApiUrl, inner =>
{
inner.UseWebApi(GlobalConfiguration.Configuration);
// Enforce bearer token authentication for all API requests
inner.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://identityserver/core",
ValidationMode = ValidationMode.ValidationEndpoint,
RequiredScopes = new[] { "scope.test.client" }
});
});
It also ensures all API requests are handled by a custom authorize attribute:
GlobalConfiguration.Configuration.Filters.Add(new DefaultApiAuthorizeAttribute());
Debugging this API, the first line in my overridden OnAuthorize method (in DefaultApiAuthorizeAttribute) is this:
var caller = actionContext.RequestContext.Principal as System.Security.Claims.ClaimsPrincipal;
If I break on this line I can see that actionContext.RequestContext.Principal is always null. However, I can see that ((System.Web.Http.Owin.OwinHttpRequestContext)actionContext.RequestContext).Request.Headers contains an Authorization header with the bearer token passed from my console app.
So it would seem that the API project is not authenticating the bearer token. Certainly the Identity Server logs suggest it isn't being hit at all after issuing the initial access token. So I'd appreciate your expert advice about why this might not be happening, or at least some pointers about where to look.
I suspect it might have something to do with SSL. Both sites are hosted locally under self-signed SSL certs, although Identity Server is configured to not require SSL and uses the idsrv3test.pfx development certificate for signing. I do have another test MVC web app which delegates authentication to the same IS3 instance which works fine locally, so I believe my IS3 instance is configured correctly.
You need to call UseIdentityServerBearerTokenAuthentication before you call UseWebApi. When you set up an OWIN Middleware Pipeline, the order is important.
In your case, Web API will be handling your requests before they get sent onto Identity Server (if they get sent on at all).
I imagine a range of possible issues could have the impact I described, but in my case I was able to find the cause by adding a diagnostics log to my consuming API. This led me to discover that the problem was an assembly conflict. The Owin middleware was looking for a Newtonsoft.JSON assembly with version 8.0.0.0 but my consuming API (actually running on top of a CMS intance) was using 7.0.0.0.
For anyone else who wants to find the answer fast, rather than spend hours tweaking configurations, here's the documentation that describes how to add this logging: https://identityserver.github.io/Documentation/docsv2/consuming/diagnostics.html

Resources