ASP.NET Core 3+ JWT authentication - c#

Been spending a few days lately getting into how to protect a ASP.NET Core Web API/Web App with JWT authentication and/or OIDC.
I have so far been unable to find proper documentation to the two(?) different middleware extensions (.AddOpenIdConnect and .AddJwtBearer). From googling and experimenting I have arrived at the assumption that the former supports the full OIDC dance (validation, redirecting to authority etc., suitable for a web app) whereas the latter is used for JWT validation only (more suitable for an API).
What articles I have found are all recipes, focused on how to configure for a specific id provider, such as Azure or IdentityServer4, but I have found almost nothing that actually dives into how these component behaves and how each configurable option affects that behavior. My company uses its own id provider for authentication and issuing JWT tokens so most recipes I find only provides the occasional hint, not real understanding.
I realize documentation must exist but I haven't been able to find anything beyond the skeleton doc provided by Microsoft on learn.microsoft.com.
Would really appreciate more in-detail information for how to use these two middlewares; not specifically for a named id provider, but more generally:
Is my assumption correct, that .AddOpenIdConnect performs both JWT validation and supports the OIDC dance, automatically redirecting to the authority, token issuer etc?
Is the middleware dependent on cookie auth; i.e. can I omit the .AddCookie setup?
Would it ever make sense to use both middleware components (.AddOpenIdConnect and .AddJwtBearer)?
Please describe the behavior of each component, and how the options affect that behavior.
What are the required options that have to be specified and which ones are optional?
Is there a need to interact with the OAuth2 "dance" (via events) or are those there for debugging and response/redirect customization only?

AddOpenIdConnect is as you say responsible for the oauth dance, to authenticate user and to create the user session. It handle everything internally, so you dont need to involve the events unless you need to customize it.
You typcically use AddOpenIdConnect with AddCookie, so that AddCookie is responsble for the user session cookie. AddOpenIdConnect is only used for the challenge part of the user login flow.
AddJwtBearer is only for for API/services that receives and authenticate tokens, it will only do valdidation of the token and create a ClaimsPrincipal user based on the token.
you can use both in the same service, but I recommend that you don't because it can get really complex to figure out what is going on. Keep it simple and follow the single responsibility principle and you are good to go. (ie, put them in different services)
A typical AddJwtBearer setup can look like this:
.AddJwtBearer(opt =>
{
opt.Authority = "https://localhost:6001";
opt.Audience = "paymentapi";
opt.TokenValidationParameters.RoleClaimType = "roles";
opt.TokenValidationParameters.NameClaimType = "name";
opt.TokenValidationParameters.ClockSkew = TimeSpan.FromSeconds(0);
// IdentityServer emits a typ header by default, recommended extra check
opt.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
});
A typical AddOpenIdConnect setup can look like this:
.AddOpenIdConnect(options =>
{
options.AccessDeniedPath = "/User/AccessDenied";
options.Authority = _configuration["openid:authority"];
options.ClientId = _configuration["openid:clientid"];
options.ClientSecret = "mysecret";
options.ResponseType = "code";
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("employee");
options.Scope.Add("payment");
options.Scope.Add("offline_access");
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.Prompt = "consent";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
};
});

Related

Load roles from database based in JWT's userId

I'm trying to implement some database fetching in my JWT authentication, so that I can dinamically fetch the roles and other info from the database, I would ideally only want to store the userId in the JWT payload.
Doing that, I can do stuff like store the date where the password was last changed, and refuse the token in case it was issued before that.
The problem is that I have no idea where on the chain I would do such thing, I'm just getting started with .NET/ASP.NET
I have this on my ConfigureServices currently:
var jwtKey = Encoding.ASCII.GetBytes(_configuration["Jwt:Secret"]);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(jwtKey),
ValidateIssuer = false,
ValidateAudience = false,
};
});
And this on my Configure:
app.UseAuthentication();
app.UseAuthorization();
With that, I can access some user info inside a controller's action with User.Identity.Name for example, how would I do that after loading the model from the DB?
REQUIREMENTS
These are usually the main things you will care about:
APIs must have access to domain specific claims so that they can authorize correctly
Tokens returned to internet clients are kept confidential
OPTION 1
The optimal way to meet the requirements is to issue opaque tokens to internet clients, then introspect them before they reach the API, as described in the Phantom Token Approach.
An Authorization Server can then reach out to your domain specific data at the time of token issuance, to include custom claims in access tokens.
OPTION 2
In the API code there are two stages involved in integrating custom claims, and the second of these can be customized:
Verify the JWT access token
Form a ClaimsPrincipal
If your Authorization Server doesn't support opaque access tokens, another option that will work is a Custom Authentication Handler. It adds some complexity to your API though. See this blog post of mine and this code.

How to configure the OAuth callback to a different domain in ASP.NET Core authentication

I am Authenticating against an OAuth endpoint where I can only configure 1 callback domain. (and localhost is whitelisted).
I have my web app running in Azure (myapp.azurewebsites.net) and have it available with two custom domains (myapp.cc and myapp.eu). When I use the default setup, the CallbackPath can only be a relative path (to the current domain)
The code documentation of CallbackPath indicates it's relative to the application's base path:
/// <summary>
/// The request path within the application's base path where the user-agent will be returned.
/// The middleware will process this request when it arrives.
/// </summary>
public PathString CallbackPath { get; set; }
I want to make sure the CallBack happens to the (only) domain that I whitelisted on the OAuth backend. I know I can implement everything manually, but I was hoping there would be an easy way to work around this design and still benefit from the baked in Authentication options.
So even if a user is logging on on the myapp.cc or the myapp.eu or the myapp.azurewebsites.net , it should redirect to myapp.azurewebsites.net/ (which is whitelisted on my Auth service)
A part of my Startup.cs file is pasted below:
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "MyService";
})
.AddCookie()
.AddOAuth("MyService", "MyService",
options =>
{
options.ClientId = settings.ClientId;
options.ClientSecret = settings.ClientOauthSecret;
options.CallbackPath = "/relativeonlypath";
options.SaveTokens = true;
options.SignInScheme = IdentityConstants.ExternalScheme;
/// ... removed for brevity
}
);
Any idea on how to implement this?
Thank you
I'm not sure it's possible, because to verify that the user is redirected to your application as part of a "genuine" authentication flow, the ASP.NET OAuth handler performs the following steps:
Before redirecting the user to the OAuth service, ASP.NET Core generates a "correlation" cookie that is tied to the current domain; and
When the user is redirected to the app, the handler looks for this cookie and validates its content.
So if the correlation cookie is generated in step #1 for one domain, let's say myapp.cc, and the user is redirected to another domain, myapp.azurewebsites.net, ASP.NET Core might not be able to read it because the browser will not have included it in the redirection request.
Note
As seen in the first comments, the original thought was to leverage the SameSiteproperty of the correlation cookie to have it sent by the browser to the second domain.
This was all wrong, apologies!
I now think that you have 2 different options:
Redirect every request from myapp.cc and myapp.eu to myapp.azurewebsites.net, so that when the authentication flow happens, we're already on the right domain; or
Redirect the user to the myapp.azurewebsites.net domain before redirecting them to the OAuth server.
I won't go into the first solution, as there's plenty of ways to achieve this.
Here's some code that I haven't tested that could work for the second solution:
services
.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "MyService";
})
.AddCookie()
.AddOAuth("MyService", options =>
{
options.Events.OnRedirectToAuthorizationEndpoint = context =>
{
var currentRequestUri = new Uri(context.Request.GetDisplayUrl());
// 1. If we're not on the correct domain, redirect the user to the same page, but on the expected domain.
// The assumption is that the authentication flow will also kick in on the other domain (see 2).
if (currentRequestUri.Host != "myapp.azurewebsites.net")
{
var applicationRedirectUri = new UriBuilder(currentRequestUri)
{
Host = "myapp.azurewebsites.net"
}.Uri.ToString();
context.Response.Redirect(applicationRedirectUri);
return Task.CompletedTask;
}
// 2. If we reach here, it means we're on the right domain, so we can redirect to the OAuth server.
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
});

Connect OAuth Tokens with a user

I'm trying to figure out the best way to map tokens to a user. I think I've falling across a common problem Authorization vs Authentication.
I'm creating a market place which, my payments service is backed by stripe so I allow logins using stripe currently.
I register my stripe service like so:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddOAuth<OAuthOptions, StripeConnectOAuthHandler<OAuthOptions>>(
StripeConnectDefaults.AuthenticationScheme, options => {
options.SaveTokens = true;
options.ClientId = Configuration["Stripe:ClientId"];
options.ClientSecret = Configuration["Stripe:ClientSecret"];
options.TokenEndpoint = StripeConnectDefaults.TokenEndpoint;
options.AuthorizationEndpoint = StripeConnectDefaults.AuthorizationEndpoint;
options.UserInformationEndpoint = StripeConnectDefaults.UserInformationEndpoint;
options.Scope.Add("read_write");
options.CallbackPath = new PathString("/signin-stripeconnect");
//...
});
Since stripe is what I use to handle the payments I need the token to perform certain behavior like creating a pay event or subscribing to one but I don't want to enforce that my users must have a stripe account to view data on my site.
So I'd like to add additional ways to login, but I need to link these users together
app.UseGoogleAuthentication(new GoogleOptions()
{
AuthenticationScheme = "Google",
DisplayName = "Google",
SignInScheme = COOKIE_AUTH,
ClientId = "sdlfkjgsdlkfjgsdf-sdfadsfasdf.apps.googleusercontent.com",
ClientSecret = "myClientSecretBase64==",
});
However if I do this, I need a way to link my google login and my stripe account login. Prior to now I was using IdentityServer4. Generally Token Servers are sperate from the API. So It seems a bit of overkill to host a token server, if only a single application is going to consume it.
Is there a simple way allow authentication, while still giving the ability to connect to external api's such as stripe?
Note: If the solution requires IdentityServer 4 I don't mind, I just would rather not having to host 2 seperate applications
Hmm, this seems to be caused by a misconception of mine about Identity. Identity already a single user to have different login methods. In my authentication controller I fixed this by attaching an external login to an existing user if the user already exist.
var user = await this._userManager.FindByEmailAsync(email);
if(null == user)
{
user = await this.CreateIdentityUser(info, email);
}
var addLoginResult = await _userManager.AddLoginAsync(user, info);
Warning: You shouldn't be careful linking users, if you irregardlessly link users based on email, you could run into an issue where another user creates a fake account on the provider using the same email.

Strange behaviour when accepting Bearer authentication and OpenIdConnect from a single client with IdentityServer4

I am having some issues after making some tweaks to an IdentityServer4 Quickstart sample solution, specifically the 8_AspNetIdentity sample.
I'll preface this by saying I'm not sure if what I'm trying to do is just not supported, or if I'm doing it wrong.
This sample solution contains the following projects relevant to my question:
an IdentityServer,
an MVC client (named MVCClient) that uses OpenIdConnect to authenticate its users,
a web API client (named API) that uses bearer authentication for its users
a console app (named ResourceOwnerClient) designed to be a client of the API
What I am trying to do is merge the API project into the MVCClient, so that the MVCClient could both authenticate the users from its MVC website with OIDC, and also the ResourceOwnerClient using bearer authentication.
I made the following changes to the MVCClient's Startup.cs:
changed services.AddMvc(); to:
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder(new[]
{
JwtBearerDefaults.AuthenticationScheme,
CookieAuthenticationDefaults.AuthenticationScheme,
"oidc"
})
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
added JWT bearer options to the services.AddAuthentication():
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
})
Now technically this did work, as both the ResourceOwnerClient and the MVC users can successfully authenticate with the MVCClient. I however have one caveat:
When I authenticate with a user from the MVC side, I noticed that there are two identities in my current User. Both are identical in terms of claims, etc. This only happens when I put a breakpoint in the MVCClient, on the IdentityServer there is only one identity.
On the IdentityServer, I have registered a UserClaimsPrincipalFactory which adds my own custom claims to the ClaimsIdentity. In the two identities on the IdentityServer, I can see the claims duplicated. So instead of having one identity with two custom claims, I see two identities which each have 4 custom claims. The CreateAsync method in my UserClaimsPrincipalFactory is also getting hit 5 times for a single login.
Although this behaviour is strange, it does not seem to be having any negative impacts. But this is only a proof of concept for a larger application that I'm building, and I'm afraid I may run into issues in the future because of it.
If anyone has attempted this sort of thing before, or knows why this behaviour could be happening, any help would be appreciated.
While nothing bad should happen with this design, I would completely remake it. Why? Because you are mixing a Client and an ApiResource, and they should be logically separated. A Client is an application, something some user interacts with, even if it was a headless one (i.e an automated service); while an ApiResource consists of resources that are provided to Clients, so no user can interact with it directly.
You could add two authentications against IdentityServer, one as API (and add it as JwtBearer) and one as a Client (and add it as Cookies). You can then use [Authorize(AuthenticationSchemes = "JwtBearer")] and = "Cookies" depending on the function of that Action/Controller.
Leaving that aside, the problem is that your application is getting one Identity for the MVC side and one for the API side, since it has no way of telling which one you want.
Just so you have an idea, this is how one of my IdentityServers with ASP.NET Core Identtiy look like, where you can login against it using the UI and also hit the REST endpoints with a JwtToken:
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = Configuration["IdentityServerUrl"];
options.ApiName = Configuration["ApiName"];
options.RequireHttpsMetadata = false;
})
.AddCookie(IdentityConstants.ApplicationScheme, o =>
{
o.LoginPath = new PathString("/Account/Login");
o.Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
};
})
.AddCookie(IdentityConstants.ExternalScheme, o =>
{
o.Cookie.Name = IdentityConstants.ExternalScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5.0);
})
.AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
})
.AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5.0);
});

IdentityServer4 ValidIssuers

Is there any way to tell IdentityServer4's authentication system to allow multiple issuers for the tokens?
I have an application that is using Identity Server to issue bearer tokens, and as long as the front end and the back end use the same URL to get tokens from authentication works fine.
However, I now have a need to have the same site accessed through multiple CNAMEs, meaning that the client will request tokens from two different URLs.
The error that is sent to the logs is:
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware[7]
Bearer was not authenticated. Failure message: IDX10205: Issuer validation failed. Issuer: 'http://domainb.com'. Did not match: validationParameters.ValidIssuer: 'http://domaina.com' or validationParameters.ValidIssuers: 'null'.
The presence of a ValidIssuers collection seems to indicate that you can set multiple places from which the API will accept tokens, but I cannot find anything like that exposed in options exposed by UseIdentityServerAuthentication.
I am aware of the Authority option, but that only allows me to set a single valid authority.
Is there are any way of setting multiple valid issuers, or setting it to use something other than the hostname as the issuer id?
UPDATE
My identity server configuration on the server side looks like this:
services.AddIdentityServer(options => {
options.IssuerUri = "http://authserver"; })
.AddAspNetIdentity<ApplicationUser>();
this is from the auth server side of things.
On the client API, the UseIdentityServerAuthentication call looks like this:
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions()
{
Authority = AppSettingsConfigurationRoot["Authentication:AuthorityEndpoint"],
RequireHttpsMetadata = false,
ApiName = "rqapi",
AutomaticAuthenticate = true,
ClaimsIssuer = "http://localhost:5001"
});
The address in the {{AppSettingsConfigurationROot["Authentication:AuthorityEndpoint"] is usually set at the public DNS name of the server so that the token issuer as seen by AngularJS matches the URL of the IdentityServer from the point of view of the C# API.
As Original Poster wrote in a comment, the (now, 2020, deprecated) IdentityServer4.AccessTokenValidation package doesn't expose the right options. To read more about the recent deprecation check this blogpost, but if you still are using it, here's how I solved this issue.
The AddIdentityServerAuthentication(...) extension method is a wrapper (the code is super readable!) to combine two authentication schemes:
JwtBearer
OAuth2Introspection
It uses its own configuration class, and simply doesn't expose all the JwtBearer options (possibly just an omission, possibly because some options are not valid for both schemes.
If -like me- you only need JwtBearer you might get away with simply using just that, and using the ValidIssuers array. So:
services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
options.Authority = "https://example.org";
options.Audience = "foo-api"; // options.ApiName in the IDS4 variant
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuers = new[]
{
"https://example.org", // first issuer
"https://example.com", // some other issuer
},
NameClaimType = "name", // To mimick IDS4's variant
RoleClaimType = "role", // To mimick IDS4's variant
};
});
As far as I understand, this will use example.org as the Authority and get the openid-configuration and so forth from that domain. But any JWT token offered to this API would be accepted as long as one of the ValidIssuers is the iss (issuer claim) in the token.

Categories