ServiceStack API aspnet core with Azure AD B2C returns 401 for request even with bearer token - asp.net-core

I have a working ServiceStack API that authenticates against a AzureAD tenant. We are trying to move this to start using Azure B2C. The application is build with c# and runs on net 5.0. I've managed to change the configuration to use the 'correct' config. I'm then using Postman to get my access token from my tenant suing the authorization code flow.
However, when i make a request to the api, the response is always a 401 status code.
Where in the servicestack code can I put a break point to see why this failure is happening? I have tried multiple places in our AppHostConfigurator.cs/AppHost.cs files, but the break points doesn't appear to display why a 401 is being sent back as a response. I'm sure it's something related to wrong claims/roles expected etc, maybe the Azure ADB2C application being setup incorrectly, but obviously i need to know exactly so that i can resolve.
I'm setting up the authentication like this:
private static void ConfigureAuthentication(IAppHost host)
{
var authProviders = new List<IAuthProvider> {new NetCoreIdentityAuthProvider(host.AppSettings)};
if (host.AppSettings.GetAllKeys().Contains("AzureAdB2C"))
{
var debugMode = host.AppSettings.Get(nameof(HostConfig.DebugMode), false);
var azureSettings = host.AppSettings.Get<AzureAdB2COptions>("AzureAdB2C");
var jwt = azureSettings.GetB2CJWTProviderReader(debugMode);
jwt.PopulateSessionFilter = (session, payload, request) =>
{
if (session.Email == null && payload.ContainsKey("upn") && payload["upn"].Contains("#"))
session.Email = payload["upn"];
if (session.UserName == null && payload.ContainsKey("unique_name"))
session.UserName = payload["unique_name"];
};
authProviders.Add(jwt);
}
var auth = new AuthFeature(() => new AuthUserSession(), authProviders.ToArray())
{
HtmlRedirect = "/account/signin",
HtmlLogoutRedirect = "/account/signout",
IncludeAssignRoleServices = false,
IncludeRegistrationService = false
};
// remove default service authentication services
auth.ServiceRoutes.Remove(typeof(AuthenticateService));
host.Plugins.Add(auth);
}
We are using swagger as well to call the API (which works as expected). This question is more about that requests that are submitted with a bearer token.
thanks

Please refer to this existing answer for examples of how to validate why a 3rd Party JWT Token is invalid with ServiceStack's JWT Auth Provider.

Related

How to configure Azure AD authentication in Hybrid ASP.NET Core MVC (backend) and Vuejs SPA (frontend)?

My application is a hybrid approach where use ASP.NET Core MVC as my backend. I have various controllers which my front end uses to pull data from our database and also to do API calls on MS Graph. I am using the following program.cs file to get the authentication initiated when a user first logs on to the site:
//authentication pipline
builder.Services.AddHttpContextAccessor();
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAd", options);
options.Events = new OpenIdConnectEvents
{
//Tap into this event to add a UserID Claim to a new HttpContext identity
OnTokenValidated = context =>
{
//This query returns the UserID from the DB by sending the email address in the claim from Azure AD
string query = "select dbo.A2F_0013_ReturnUserIDForEmail(#Email) as UserID";
string connectionString = builder.Configuration.GetValue<string>("ConnectionStrings:DBContext");
string signInEmailAddress = context.Principal.FindFirstValue("preferred_username");
using (var connection = new SqlConnection(connectionString))
{
var queryResult = connection.QueryFirst(query, new { Email = signInEmailAddress });
var claims = new List<Claim>
{
new Claim("UserID", queryResult.UserID.ToString())
};
var appIdentity = new ClaimsIdentity(claims);
context.Principal.AddIdentity(appIdentity);
}
return Task.CompletedTask;
},
};
}).EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
//Add Transient Services
builder.Services.AddTransient<IOneDrive, OneDrive>();
builder.Services.AddControllers(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
options.Conventions.AllowAnonymousToFolder("/Login");
options.Conventions.AuthorizeFolder("/");
options.Conventions.AuthorizeFolder("/files");
}).AddMicrosoftIdentityUI();
// Add the UI support to handle claims challenges
builder.Services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
builder.Services.AddRequiredScopeAuthorization();
In the Azure AD portal my application is registered as a web app. So when a user initially goes to the site they are redirected to https://login.microsoftonline.com/blahblah to get the login process started. This is automated by the Azure AD identity platform. Then once the login occurs they are redirected to localhost where the VueJS spa is loaded (localhost:43862). My spa uses various axios requests to the controllers and they pull data and vue router loads components. However, my issue is say the user needs to relog in because the cookie is expired or they logged out in another tab. The next axios request made by the expired session does not redirect the user to Azure login screen but instead results in an CORS error. So I need to get my axios requests to force the page redirect to Azure AD login screen (which probably is the worst idea since CORS policy is resulting in error) or have it return a redirect to localhost/login which is my own custom login screen with a button to Azure AD login and shouldnt impact CORS. So how do I intercept this Azure AD redirect to Azure AD login and replace with my own?
I have also tried to return a 401 error code so I could check for that in my axios request but to no avail it does nothing. If I put a breakpoint there it does hit this code but it does not change the status code of the response and I still get 302. My code for that was to try and add to the event :
OnRedirectToIdentityProvider = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
}
My other ideas was maybe I should set my CORS policy to allow redirects from login.microsoft.com? Or would this be bad practice?
I can answer part of your question... First, for our API application which is protected by Azure AD, what the API should do is validating the request whether it contained a correct access token in the request header, if yes, give the response, if no, then give error like 401 or 403. A normal API application shouldn't have a UI to let users sign in. Anyway, if you want to expose an API in an MVC project, it's OK, but for API itself, it shouldn't have a UI.
Let's see sample below, I had a .net 6 web api project, and here's my program.cs:
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
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();
And it requires configurations in appsetting.json.
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "azure_ad_client_id",
"ClientSecret": "client_secret",
"Domain": "tenant_id",
"TenantId": "tenant_id",
//"Audience": "api://azure_ad_client_id_which exposed_api" // here I used the same azure ad app to expose API, so I can comment this property
},
And this is the Controller:
[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
[RequiredScope("Tiny.Read")]
[HttpGet]
public string Get()
{
return "world";
}
}
I had an Azure AD app, and I exposed an API like this:
I also add this API for the same Azure AD app.
Then let's do a test. When I call this API directly, I will get 401 error:
If I used an expired token within the request, I will also get 401 error:
But if I used a correct token(go to https://jwt.io to decode the token, we should see it containing correct scope, for me its "scp": "Tiny.Read",), I will get response:
And till now, the API part had finished. Let's see the client SPA. For SPA, you should integrate MSAL so that you can make your users to sign in via Azure AD, and generate the access token for calling MS graph API or your own API. The code for generating access token should be the same but you should set different scope for different API. In my scenario, my API required a scope Tiny.Read, then I should set in my client App.
Here's an screenshot for generating access token in react. You need to set the scope in your code.
Now you have the method to generate access token, you already know the API url. Then you can send request to call api, using AJAX, using fetch, or something else, sending an http request is ok. And in the calling api part, you also need to handle the response. If the response code is 401, then you need to do some logic, maybe redirect to the sign in page. And you said you had trouble here, you met CORS issue. I can't answer this part. I think it depends on how you redirect to Azure AD sign in page. I'm afraid you can take a look at this sample to learn how to sign in users and call graph api.

How can I change the 'typ' of a token provided by Azure AD?

I have a project setup like this:
React frontend
-> authenticates against...
Identity Server
-> which redirects to...
A Microsoft login
I'm using a Clients Credential Provider and it works great - the IS4 redirects to MS login, and then gets redirected with the access token back, which is then passed on to the React app.
Now, I've been tasked with creating a feature to change the user's password. I'm trying to do this by sending the old+new password to IS4, and then calling the MSGraphClient, but I couldn't make it work.
I've tried the Username/Password provider, because I have all the info needed, but I need to change stuff on the ActiveDirectory settings to make my app public. But even then, I don't like that solution.
I've also tried with the On-behalf-of provider, this is the code:
var scopes = new[] { "User.Read",
"Directory.AccessAsUser.All" };
// Multi-tenant apps can use "common",
// single-tenant apps must use the tenant ID from the Azure portal
var tenantId = "~~";
// Value from app registration
var clientId = "~~";
var clientSecret = "~~";
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
// This is the incoming token to exchange using on-behalf-of flow
var oboToken = HttpContext.Request.Headers.First(h => h.Key == "Authorization").Value.ToString().Replace("Bearer ", "");
var cca = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantId)
.WithClientSecret(clientSecret)
.Build();
// DelegateAuthenticationProvider is a simple auth provider implementation
// that allows you to define an async function to retrieve a token
// Alternatively, you can create a class that implements IAuthenticationProvider
// for more complex scenarios
var authProvider = new DelegateAuthenticationProvider(async (request) => {
// Use Microsoft.Identity.Client to retrieve token
var assertion = new UserAssertion(oboToken);
var result = await cca.AcquireTokenOnBehalfOf(scopes, assertion).ExecuteAsync();
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", result.AccessToken);
});
var graphClient = new GraphServiceClient(authProvider);
And it kinds of work, because the request is made, but the server throws an error:
AADSTS5002727: Invalid JWT header type specified. Allowed types: 'JWT','http://openid.net/specs/jwt/1.0'.
I checked my token on JWT.io, and the typ is at+jwt... Why? Why is MS sending me a type of token that it doesn't support? How can I change it from my side so it's a plain JWT?
Thanks for any advice, and any other possible solution for this.
To resolve the error "AADSTS5002727: Invalid JWT header type specified. Allowed types: JWT,http ://openid.net/specs/jwt/1.0" , please try the below if helpful:
Please check the version of .Net core you are currently using to generate the token. Try using .Net core 2.2 with IS4.
Try setting IdentityServerOptions.AccessTokenJwtType to empty string or JWT on IdentityServerOptions.
In the mentioned code, replace var oboToken variable directly with the value of token.
var oboToken = "JWT_TOKEN_TO_EXCHANGE";
Please note the below point from MsDoc :
Don't attempt to validate or read tokens for any API you don't own,
including the tokens in this example, in your code. Tokens for Microsoft services can use a special format that will not validate as
a JWT, and may also be encrypted for consumer (Microsoft account)
users
If still the error persists, try upgrading clients to a new token validation library that works with the new style tokens.
Please check whether the below links give you any pointer to resolve the issue:
JWT Token always Invalid · Issue #905 · openiddict/openiddict-core · GitHub
IdentityServer .Net Core 3.0 & Owin/Katana Token validation · Issue #3705 · IdentityServer/IdentityServer4 · GitHub

OpenID Connect from Swagger UI with PKCE and Okta in .Net Core

After stepping around controller authorization in the debugger for the past 4 weeks, I finally decided to tackle OpenID Connect authentication in my Swashbuckle-supported .NetCore 5 API. I wish I hadn't, because I spent almost a day so far without a working solution.
Here is a brief recap.
Support for OpenID Connect in Swagger-UI is very recent. The only place where I found this information was in Helen's comment to this question. Swagger Ui 3.38.0 is only available in Swashbuckle 6.0.7.
Once upgraded to the latest Swashbuckle, I started to see a bunch of "discovered" authorization options in Swagger UI. Alas, PKCE does not appear to be in use, based on the error, even though I explicitly set it in Startup.cs:
.UseSwaggerUI(c => c.OAuthUsePkce());
Also, the ClientSecret there does not make sense, because PKCE is supposed to replace this (and I actually don't have a client secret).
My question, does anybody have OpenID Connect with PKCE and Okta working in Swagger UI?
Auth ErrorError, error: invalid_client, description: Browser requests to the token endpoint must use Proof Key for Code Exchange.
I've recently sitched from an implicit flow to code+pkce flow. I ran into the same issue. The key was to configure the token endopoint url. Swagger UI will still show you the client credentials input box, but you can leave this empty when authorizing.
var securityDefinition = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Scheme = "Bearer",
In = ParameterLocation.Header,
Name = "Authorization",
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri(azureAdOptions.AuthorizeEndpoint),
TokenUrl = new Uri(azureAdOptions.TokenEndpoint),
Scopes = azureAdOptions.Applications["Api"].Scopes.ToDictionary(e => e.Value, e => e.Key)
}
}
};
c.AddSecurityDefinition(
"oauth2",
securityDefinition);
I obviously still have to enable pkce support on the SwaggerUiOptions
internal static void ConfigureUi(SwaggerUIOptions c, IConfiguration configuration, string apiName, string environmentName)
{
c.OAuthUsePkce();
}
I use Azure AD, here are the values I've used:
AuthorizationUrl: https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize
TokenUrl: https://login.microsoftonline.com/organizations/oauth2/v2.0/token
Scopes: custom-value
The commit below contains all the details of how it's implemented. It also contains a test sample.
Add support to PKCE for SwaggerUI & update OAuth2Integration sample

identityserver 4 get current user's access_token

I am having trouble getting my current user's access_token.
Here is my setup:
QuickstartIdentityServer (QIS) in aspnet core, identity and EF storage
API (API) in NodeJs. Validates jwt tokens in header against QIS.
SPA angular app that works great with QIS and API and is out of the scope of this question
In a section of the QuickstartIdentityServer (QIS) site (user details page), I would like to call an API endpoint using an access_token to authenticate the request. I am struggling to retrieve the current user's access_token from my QIS site. Whenever I call HttpContext.GetTokenAsync("access_token") I get a null value. I have seen this section of IdSrv4 documentation: https://identityserver4.readthedocs.io/en/release/quickstarts/5_hybrid_and_api_access.html?highlight=gettokenasync but it seems to apply to an MVC client and not my own identity server.
Anyone could shed some light on how to get my user's access_token ?
Thanks
EDIT
Here is a starting point to try to explain better my issue:
https://github.com/IdentityServer/IdentityServer4.Samples/tree/release/Quickstarts/6_AspNetIdentity/src/IdentityServerWithAspNetIdentity
Starting from this QIS project, I would like to get the logged in user's access token. So for instance, if I edit HomeController to add this call:
public async Task<IActionResult> Index()
{
var accessToken = await HttpContext.GetTokenAsync("access_token");
return View(accessToken);
}
I would then be able to call my NodeJS API with this token in the Auth Header.
Hope this explains better my issue.
So I managed to authenticate myself w/ my API using a dedicated Client using client credentials grant and the following call to get an access_token:
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
var tokenClient = new TokenClient(disco.TokenEndpoint, clientId, clientSecret);
var tokenResponse = await tokenClient.RequestClientCredentialsAsync(scope);
Then I can add to my request header to API the access_token returned in tokenResponse:
using(var client = new HttpClient()) {
client.SetBearerToken(tokenResponse.AccessToken);
...
// execute request
}
The downside is that I can't "impersonate" the current currently logged on IS on API side.

Trouble getting ClaimsPrincipal populated when using EasyAuth to authenticate against AAD on Azure App Service in a Asp.Net Core web app

We have a web app built on Asp.Net core. It doesn't contain any authentication middleware configured in it.
We are hosting on Azure App Service and using the Authentication/Authorization option (EasyAuth) to authenticate against Azure AD.
The authentication works well - we get the requisite headers inserted and we can see the authenticated identity at /.auth/me. But the HttpContext.User property doesn't get populated.
Is this a compatibility issue for Asp.Net core? Or am I doing something wrong?
I've created a custom middleware that populates the User property until this gets solved by the Azure Team.
It reads the headers from the App Service Authentication and create a a user that will be recognized by the [Authorize] and has a claim on name.
// Azure app service will send the x-ms-client-principal-id when authenticated
app.Use(async (context, next) =>
{
// Create a user on current thread from provided header
if (context.Request.Headers.ContainsKey("X-MS-CLIENT-PRINCIPAL-ID"))
{
// Read headers from Azure
var azureAppServicePrincipalIdHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-ID"][0];
var azureAppServicePrincipalNameHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"][0];
// Create claims id
var claims = new Claim[] {
new System.Security.Claims.Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", azureAppServicePrincipalIdHeader),
new System.Security.Claims.Claim("name", azureAppServicePrincipalNameHeader)
};
// Set user in current context as claims principal
var identity = new GenericIdentity(azureAppServicePrincipalIdHeader);
identity.AddClaims(claims);
// Set current thread user to identity
context.User = new GenericPrincipal(identity, null);
};
await next.Invoke();
});
Yes, this is a compatibility issue. ASP.NET Core does not support flowing identity info from an IIS module (like Easy Auth) to the app code, unfortunately. This means HttpContext.User and similar code won't work like it does with regular ASP.NET.
The workaround for now is to invoke your web app's /.auth/me endpoint from your server code to get the user claims. You can then cache this data as appropriate using the x-ms-client-principal-id request header value as the cache key. The /.auth/me call will need to be properly authenticated in the same way that calls to your web app need to be authenticated (auth cookie or request header token).
I wrote a small basic middleware to do this. It will create an identity based off of the .auth/me endpoint. The identity is created in the authentication pipeline so that [authorize] attributes and policies work with the identity.
You can find it here:
https://github.com/lpunderscore/azureappservice-authentication-middleware
or on nuget:
https://www.nuget.org/packages/AzureAppserviceAuthenticationMiddleware/
Once added, just add this line to your startup:
app.UseAzureAppServiceAuthentication();
The following code decrypts the AAD token from the Azure App Service HTTP header and populates HttpContext.User with the claims. It's rough as you'd want to cache the configuration rather than look it up on every request:
OpenIdConnectConfigurationRetriever r = new OpenIdConnectConfigurationRetriever();
ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(options.Endpoint, r);
OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync();
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKeys = config.SigningKeys.ToList(),
ValidateIssuer = true,
ValidIssuer = config.Issuer,
ValidateAudience = true,
ValidAudience = options.Audience,
ValidateLifetime = true,
ClockSkew = new TimeSpan(0, 0, 10)
};
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
ClaimsPrincipal principal = null;
SecurityToken validToken = null;
string token = context.Request.Headers["X-MS-TOKEN-AAD-ID-TOKEN"];
if (!String.IsNullOrWhiteSpace(token))
{
principal = handler.ValidateToken(token, tokenValidationParameters, out validToken);
var validJwt = validToken as JwtSecurityToken;
if (validJwt == null) { throw new ArgumentException("Invalid JWT"); }
if (principal != null)
{
context.User.AddIdentities(principal.Identities);
}
}
It only works for Azure AD. To support other ID providers (Facebook, Twitter, etc) you'd have to detect the relevant headers and figure out how to parse each provider's token. However, it should just be variations on the above theme.
You can give this library a try. I faced a similar problem and created this to simplify the use.
https://github.com/dasiths/NEasyAuthMiddleware
Azure App Service Authentication (EasyAuth) middleware for ASP.NET
CORE with fully customizable components with support for local
debugging
It hydrates the HttpContext.User by registering a custom authentication handler. To make things easier when running locally, it even has the ability to use a json file to load mocked claims.