.Net Core 3.1 Merging JWT and OpenIDConnect Authentication - c#

Short version: I am having trouble in merging together the correct Authentication config in my .NET Core MVC Website to allow my users to authenticate against Azure Active Directory, but to also allow a Daemon connection (from a Console App) in, too.
Long version:
I've got a .NET Core MVC website, which authenticates against Azure Active Directory perfectly fine when using the following in the ConfigureServices method in the Startup.cs:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddSignIn("AzureAd", Configuration, options => Configuration.Bind("AzureAd", options));
I am also trying to get my .NET Core Console App to call into the APIs (as a Daemon connection) into the above MVC website (all is configured in the App Registration section in my Microsoft Azure account). I can connect the Console App to the MVC website and it will successfully hit an Action Result in a controller but only if I am using the following in the ConfigureServices method in the Startup.cs of the website:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddProtectedWebApi("AzureAd", Configuration, options => Configuration.Bind("AzureAD", options));
BASICALLY, if I only use the OpenIdConnect option, my web users can access the website but my console app is denied. If I only use the JwtBearer option, then my Console App can connect, but my web users are denied.
I have Google-Bing'd all day and I'm struggling to get a mash-up of these two configurations to work without knocking the other out.
I have tried to use the .AddJwtBearer() method, but am completely confused by it:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddSignIn("AzureAd", Configuration, options => Configuration.Bind("AzureAd", options))
.AddJwtBearer(options => Configuration.Bind("AzureAD", options));
How do these work together, such that both can be in place and my web app works through a browser, and the Console App (Daemon) works too? Can I bind both to my appsettings.json file??
Incidentally, the appsettings.json file looks like this:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "zzzzzzzzzzzzzz.onmicrosoft.com",
"TenantId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
"ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath ": "/signout-callback-oidc",
"ClientSecret": "myAzureClientSecret"
}
}
UPDATE 2020-06-15:
Having working on/off of this for AGES, I've found a suitable resolution that works, hence my awarding the bounty points to #michael-shterenberg. ALSO, I now know that I have a great deal to learn from #gary-archer and his impressive blog site. I just happened to get success from Michael's input.
Here's the mods to the Startup.cs file, within the ASP.NET Core MVC Web App in the diagram above:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddSignIn("AzureAd", Configuration, options =>
Configuration.Bind("AzureAd", options))
.AddJwtBearer(o =>
{
o.Authority = "https://login.microsoftonline.com/common";
o.TokenValidationParameters.ValidateAudience = false;
o.TokenValidationParameters.ValidateIssuer = false;
});
services.AddAuthorization(options =>
{
options.AddPolicy("UserAndApp", builder =>
{
builder.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
builder.AuthenticationSchemes.Add(OpenIdConnectDefaults.AuthenticationScheme);
builder.RequireAuthenticatedUser();
});
});
...coupled with the use of the following attribute on the Controller that I'm trying to call from the Daemon app.
[Authorize("UserAndApp")]
My users can still log into the website using the Azure Active Directory processes and now my automated processes can log in, too.
In case anyone is struggling to understand how the Azure App Registration side of all of this works, try this really explanatory blog post:
Secure a .NET Core API using Bearer Authentication
(I wish that I had seen that earlier, when I was trying to get my head around how the Azure App Registration process works!)

Here is the solution that worked for me (Tested on ASP .NET Core 2.1 and 3.1)
Don't set a default authentication scheme since you have 2 types (Cookies and JWT). i.e. your call to AddAuthentication should be without parameters:
services.AddAuthentication()
.AddAzureAD(options => Configuration.Bind("AzureAd", options))
.AddJwtBearer(o=> {
o.Authority = "https://login.microsoftonline.com/common";
o.TokenValidationParameters.ValidateAudience = false;
o.TokenValidationParameters.ValidateIssuer = false;
});
Note that I explicitly didn't bind your AD configuration because /common is needed to be applied to the authority (or the tenant id)
Also I set validation for audience and issuer to false so that any AAD token will work for testing. You should obviously set the correct audience/issuer
I used AddAzureAd and not AddSignIn (is that a custom external library you are using?)
Create a policy that accepts both authentication schemes:
services.AddAuthorization(options =>
{
options.AddPolicy("UserAndApp", builer =>
{
builer.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
builer.AuthenticationSchemes.Add(AzureADDefaults.AuthenticationScheme);
builer.RequireAuthenticatedUser();
});
});
Replace this with your existing authorization setup
Use the new policy name in your controller:
[Authorize("UserAndApp")]
public class HomeController : Controller
Some explanation on the mechanics:
You don't want to setup automatic authentication scheme since this will be the default schema run in the authorization middleware, while you have 2 different types
The policy will try run both authentication handlers, if one of them succeeds then authentication succeeded
Note: if you send a request with an invalid Bearer token, both authetnication handlers will fail, in this case the AzureADDefaults will "win" since it actually implement a challenge method and will redirect you (status code 302), so make sure to handle this in your app

It feels like the architecture is not quite right, and you need to separate the 2 roles performed by your Web Back End:
CURRENT WEB ONLY ARCHITECTURE
You have a Web UI front end that uses auth cookies
You have a Web Back End that requires cookies for view requests
You have a Web Back End that requires cookies for API requests
You have a Console Client that cannot use cookies so cannot call API entry points
MULTI CLIENT ARCHITECTURE
You'll need to update the web back end to include API entry points that are secured by OAuth 2.0 access tokens and not by cookies. The console app will then be able to call your web back end.
.NET CORE SUB PATHS
Introduce an additional /api subpath in your web back end. The UseWhen feature will allow you to do this without impacting other web back end behaviour:
/*
* Apply API behaviour to only subpaths, without impacting the rest of the app
*/
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments(new PathString("/api")),
api => {
api.useAuthentication();
api.useJwtBearer();
});
EXAMPLE .NET CORE API
For an example that uses subpaths, see my Sample .Net Core API. The startup class is where ASP.Net middleware is wired with different authenticaiton handling for different subpaths.
FUTURE POSSIBILITIES
Once you have the above logical separation you could potentially evolve it further in future, eg to a completely cookieless model:
Develop APIs as completely independent components based only on tokens
Update the Web UI to an SPA that uses client side security libraries
My blog at https://authguidance.com follows this approach, and my sample APIs all support any type of client.

Related

PayPal oauth2 authorization and .net core

So I'm trying to set up PayPal sign-in within my .net core application. I'm using this 3rd party library to do so: https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
I'm building my application upon Umbraco CMS, and within the appropriate section I use the following:
builder.AddMemberExternalLogins(logins =>
{
logins.AddMemberLogin(
memberAuthenticationBuilder =>
{
memberAuthenticationBuilder.AddPaypal(
memberAuthenticationBuilder.SchemeForMembers(PayPalMemberExternalLoginProviderOptions.SchemeName),
options =>
{
options.ClientId = clientId;
options.ClientSecret = clientSecret;
});
});
});
Out of the box, this does not work. From reading around it seems that it seems the authorization endpoints are using the live values, and these are not appropriate for testing with Paypal's sandbox endpoints.
After reading the following thread: https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers/issues/198 I can see that this user had to update the endpoints manually to use different values:
.AddPaypal(options =>
{
options.ClientId = Startup.Configuration["Authentication:Paypal:ClientId"];
options.ClientSecret = Startup.Configuration["Authentication:Paypal:ClientSecret"];
options.AuthorizationEndpoint = "https://www.sandbox.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize";
options.TokenEndpoint = "https://api.sandbox.paypal.com/v1/identity/openidconnect/tokenservice";
options.UserInformationEndpoint = "https://api.sandbox.paypal.com/v1/identity/openidconnect/userinfo?schema=openid";
})
Now once updating these values, the authorization seems to work. But I've found it to be very unreliable. On my personal dev machine it seems to allow me to authorize, but when testing with another colleague, he finds he is constantly logged out and sometimes it doesn't authorize at all.
When I look at the previous thread I can see that this was written in 2017, and I'm wondering if the endpoints have been updated or the process has been changed entirely by Paypal, but I can't find any documentation on this.
Basically I'm looking for the simplest way to integrate Paypal sign-in on my application. When I read Paypal's documentation (https://developer.paypal.com/docs/log-in-with-paypal/integrate/) I only see mention of refreshing the token, I don't see anything about an authorization endpoint or user information endpoint.
I'm getting quite confused and I'm wondering if anyone has successfully managed to set up a Paypal sign-in with a .net core application in 2022 and whether they have any sample code.
Any pointers would be greatly appreciated. Thanks.
That does seem like an old integration. I would advise following https://developer.paypal.com/docs/log-in-with-paypal/integrate/ from scratch, which can be implemented in any language for the eventual server-side calls. The initial client-side authorization is done in either the JS popup or using a redirect (JS is better)
After exchanging the resulting authorization_code for a refresh token, that token can then be used to obtain information about the user's account (or do API operations involving it), according to the scopes that were authorized.

User.Claims is empty for every page outside of Areas/Identity

Authorization doesn't work on Razor pages outside of Areas/Identity, cannot figure out why:
Steps to reproduce:
Fresh app. ASP.NET Core Blazor WASM Hosted
Create page: Areas/Identity/Index2.cshtml
Create page: Areas/NewArea/NewPage.cshtml
Register and Log in.
Access Index2 and NewPage and break in OnGet method while debugging.
Index2: property User.Claims contains multiple claims.
NewPage: property User.Claims is empty
Why the authorization is not "propagated" into NewPage?
When I set #page "/Identity/NewPage" it works so it is somehow related to Identity Area (??).
EDITS:
(Placing page into Pages folder of the server project results the same (no auth))
Reason why do I care:
For quick loading speed I want my index page (Index.cshtml) to be razor page (not Blazor wasm). I need to display login status on index page.
More research:
I have found exactly the same (unresolved, but closed) issue in aspnetcore repo. https://github.com/dotnet/aspnetcore/issues/34080
This issue describe the same scenario I have: Authorise normal Razor Pages in a Blazor WebAssemby App? and there is also a solution:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
})
Another same issue on SO: Blazor WebAssembly with server side Razor page authorize problem
But I still don't understand why is this happening.
EDIT - Solution
The solution with changing DefaultAuthenticateScheme didn't work well after all - it break authorization for api controllers and wasm app.
This may be caused by my setup - I am not actually using server app for "hosting" the Blazor. Blazor app is just SPA using server app for auth and api.
Anyway: Instead of changing default schema for "everything" I change it only for particular .cshtml pages:
[AllowAnonymous]
[Authorize(AuthenticationSchemes = "Identity.Application")]
With that ↑ everything works. But you have to paste this into every .cshtml page... Or - you can configure it in Program.cs:
//add policy which uses ApplicationScheme
services.AddAuthorization(config =>
{
config.AddPolicy("RazorPagesAuthPolicy",
policyBuilder =>
{
policyBuilder.AddAuthenticationSchemes(IdentityConstants.ApplicationScheme);
policyBuilder.RequireAssertion(context => true);//has to be here
});
}
//...
services.AddAuthentication()//keep this
.AddIdentityServerJwt();
//...
services.AddRazorPages(cfg =>
{
cfg.Conventions.AddFolderApplicationModelConvention("/", mo =>
{//add authorize and allowAnonymous filters
mo.Filters.Add(new AuthorizeFilter("RazorPagesAuthPolicy"));
mo.Filters.Add(new AllowAnonymousFilter());
}
);
}
);
which I am not sure if it is way to go
As I've previously mentioned, you can either use cookies authentication or Jwt authentication in order to access the ClaimsPrincipal from the Server project. The solution proposed is fine, and it is used to configure the cookies middleware used by the identity system. Incidentally, it should be:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
}).AddIdentityServerJwt();
But I still don't understand why is this happening
Do you mean why the code above works ?
The code above works because it configures the cookies middleware to authenticate the user, tells the Identity System what scheme to use, and how to challenge the user. Understand this, the Server side of your app is not authenticated, unless you configure it to perform authentication, which differs from the authentication performed when you are re-directed from the WebAssembly Blazor App. I'll try to clarify this by giving you this example:
If you created a Blazor Server App with individual accounts, and then added client SignalR to enable a chat system in your app, and you want to secure your hub object with the Authorize attribute, an authenticated user to your Blazor Server App won't be able to access secured end points on the hub, unless he is authenticated on the hub as well. How to authenticate the user ? Simply by passing the application cookie to the hub. This is more or less simialr to the issue under discussion; that is, though the Identity folder and its content are authenticated (and authorized), the hosting Server App is not.
Note: Its hard to internalized this on first shot, but rest assured that the proposed code not only works but is also 100% kosher.

ASP.NET Core MVC Azure AD Authentication Loop on Azure App Service

I have an ASP.NET Core MVC application and I integrated Azure AD into it using the following code:
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.TokenValidationParameters.ValidateIssuer = true;
});
The Azure AD Configuration contains these properties:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]",
"TenantId": "<tenant-id>",
"ClientId": "<client-id>",
"CallbackPath": "/signin-oidc"
},
"DownstreamApi": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "user.read User.ReadBasic.All"
}
It all worked locally and when deployed to an Azure App Service we did not have any problems.
I needed to integrate Microsoft Graph for a new module in the Web App (I need to look up the users in specific groups of the Azure AD).
I followed the Microsoft Graph Tutorial to implement Microsoft Graph.
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
// Handling SameSite cookie according to https://learn.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1
options.HandleSameSiteCookieCompatibility();
});
// Sign-in users with the Microsoft identity platform
string[] initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
It all works locally but as soon as the app is deployed into an Azure App Service then app goes into a redirect loop when the user is authenticated.
I reversed the changes to the original code and it gets fixed but then I can't use Microsoft Graph because I do not have the access token.
I checked all the redirect urls in the App Registration and they seem to be fine. I even included
https://<app-name>.azurewebsites.net/
in addition to
https://<app-name>.azurewebsites.net/sigin-oidc
just to be sure it wasn't a redirect url issue.
I thought it was the Katana bug but that seems to have been fixed in .NET Core.
I enabled HTTPS Only on the Azure App Service but the issue still persists.
Has anyone experienced something like this issue? I've been at it for a couple of days and I can't solve it.
Any helped is greatly appreciated.
I tried publishing the microsoft tutorial linked in the question to a new Azure App Service with a new App Registration in Azure AD.
It signed in without a problem.
After that I suspected that there might be a problem with the App Registration. I came to that conclusion because the Azure AD Registration that the MVC Web App was using was created directly in Visual Studio and not manually in the Azure Portal.
Finally, all I did was create a new App Registration in Azure AD and published the MVC Web App with the new client secret and client id. Everything seem to work fine on the published Azure App Service.

Add token authentication for webApi controller to existing asp.net MVC 5 application

I currently have a Web API controller added to an existing MVC 5 project (not using .net core) and I was able to successfully create and get data from the controller that I have set up. The purpose of the API is to pass data between it and a mobile application that uses the same data source that the MVC project uses (I will also be calling existing methods in the project from the API so I would prefer the API exist in the MVC project). I am now looking for a way to add token authentication to the API, as I only want logged in users in the mobile application to be allowed to access the API. How can I achieve this?
The simplest solution should be to use the Token Validation Middleware from the IdentityServer 3 suite.Just add the nuget package and configure your application following the doc:
public class Startup
{
public void Configuration(IAppBuilder app)
{
// turn off any default mapping on the JWT handler
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44333/core",
RequiredScopes = new[] { "api1" }
});
app.UseWebApi(WebApiConfig.Register());
}
}
It's ok to set app.UseIdentityServerBearerTokenAuthentication() only prior to app.UseCookieAuthentication() and app.UseOpenIdConnectAuthentication() and call GlobalConfiguration.Configure(WebApiConfig.Register) in Global.asaxSuch approach allows to combine token and cookie-based auth in one MVC application.The only problem for today is that IdentityServer 3 family tools are frozen and support System.IdentityModel 4 and OWIN 3 only, so
update:
The preferred solution for ASP.NET 4.6+ becomes IdentityServer3.Contrib.AccessTokenValidation -- a fork, refactored according to the recent framework changes.

SuppressDefaultHostAuthentication in WebApi.Owin also suppressing authentication outside webapi

I am running into a problem with a solution where I used parts from the Visual Studio SPA template for having the Account Controller in WebApi with Oauth Authentication.
app.UseOAuthBearerTokens(OAuthOptions);
Then I in the owin webapi registration is doing
config.SuppressDefaultHostAuthentication();
but this also supresses the default cookie authentication outside the webapi environment. Is this the intention. If so, how can I set up WebApi to supress cookie authentication but its still active accross the environment for other requests?
It seems that it is a good practice, especially when you mix an OWIN-hosted app and a regular one on the same IIS dir, to setup WebApi with the app.Map.
public void Configuration(IAppBuilder app)
{
var configuration = WebApiConfiguration.HttpConfiguration;
app.Map("/api", inner =>
{
inner.UseWebApi(configuration);
});
}
I had all my controllers configured with a "api" prefix route, and I just moved that to the map function instead. Now webapi is running isolated and it works out with the rest of my application. Thanks #PinpointTownes for pointing me in this direction.

Categories