.AddOpenIdConnect() Middleware Clarification - c#

So, I'm trying to implement an OIDC client application using ASP.NET Core 3.1. I am trying to leverage the .AddOpenIdConnect() and .AddJswtBearer() middleware. However, I need some clarification on what this middleware is doing.
Here is what I currently have for the middleware configuration:
.AddOpenIdConnect(options =>
{
options.Authority = Configuration["auth:oidc:authority"];
options.ClientId = Configuration["auth:oidc:clientid"];
options.ClientSecret = Configuration["auth:oidc:clientsecret"];
options.ResponseType = OpenIdConnectResponseType.Code;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
})
.AddJwtBearer(options =>
{
options.Authority = Configuration["auth:oidc:authority"];
options.Audience = Configuration["auth:oidc:clientid"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = Configuration["auth:oidc:authority"],
ValidAudience = Configuration["auth:oidc:clientid"],
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.Zero
};
}
I notice that requests to the Authorization server's /.well-known/oidc-configuration and /.well-known/keys endpoints are requested when the application is first started, based on my Fiddler capture below
Where does it do that?
I'm trying to also validate that the JWT received from the authorization server is valid (that it hasn't been tampered with between the time the server sent it, and the time that the client has received it). I understood this to happen when I added the TokenValidationParameters object in the .AddJwtBearer() middleware. To test this, I tried changing Valid Audience in the TokenValidationParameters to something like asdkwewrj which I know is not the valid audience for my token. But, I never got an error from the client saying that the audience was invalid. The authentication still worked, and I was able to access my secure dashboard still.
Another thing I'm trying to implement is refresh_token grant_type with this OIDC client. I thought that the options.saveTokens in the .AddOpenIdConnect() middleware would allow me to save the tokens. It looks like they're save as cookies, but these cookies look nothing like my token values (my access token is a JWT, but out of the cookies I see, none of them begin with ey).
In a nutshell, I'm trying to understand the following:
Does this .AddJwtBearer() middleware validate the ID Token for me if I have the correct JwtBearerOptions defined (like I do above)? Or do I need to manually validate the ID token against the JWKs from the JWKs URI?
If I have to manually validate the ID token using the JWKs from the JWKs URI, how do I store these JWKs when the middleware makes the request to the /.well-known/keys endpoint?
How do I get the cookies that correspond to the access token and refresh token, and then send the refresh token to my authorization server?
I noticed that I can utilize options.Events in both of these middlewares. Would any of those solve any of the items I'm trying to accomplish?
Overall, what do these two middlewares handle for me, that I shouldn't need to manually do (i.e token validation and/or token renewal)?
Thank you! I am still fairly new to in-depth ASP.NET development like this, so I appreciate any responses.

First of all, the OIDC authentication scheme and the JWT bearer authentication scheme are independent of each other. OIDC is mostly used for server-side authentication and will pretty much never be used on its own but always with the cookie scheme. The reason for this is that the OIDC scheme will just be used for the authentication but is not able to persist the information on its own. I’ve went into more details in a different answer of mine that also explains how the authentication flow works with OIDC.
As for JWT bearer, this authentication scheme will run on every request since it is completely stateless and expects clients to authenticate themselves using the Authorization header all the time. This makes it primarily used for protecting APIs since browsers wouldn’t be able to provide a JWT for normal browser requests.
So you should first ask yourself whether you are protecting your server-side web application (e.g. using Razor views or Razor pages) in which case you want to use OIDC and the cookies authentication scheme, or if you are protecting your API. Of course, the answer could be “both” in which case you want all of those three schemes but ASP.NET Core will not support this without further configuration.
With that being clarified, let’s get into your questions:
The requests to /.well-known/oidc-configuration and /.well-known/keys are done by both the OIDC and the JWT bearer scheme in order to retrieve information from your identity provider. They will do that regularly to update their data, including information about the signining keys which they will use to validate the tokens. This happens within the scheme handler and is usually not visible to you.
Correctly set up, the JWT bearer authentication will validate the token for you. It will do that by verifying the signatures using the retrieved signing keys, and then it may check additional properties like the specified audience or its lifetime.
You shouldn’t ever need to validate tokens manually. That’s the job of the authentication scheme. You are using the authentication stack so you can just access the user principal within your app without doing anything.
Cookies are protected using data protection so that they are safe against forgery. In order to retrieve the tokens stored with SaveTokens = true, you can use the GetTokenAsync method on your HTTP context.
You can use the authentication events to add to the default behavior of the authentication schemes. For validating your tokens using the standard mechanisms, you shouldn’t need to though.
There is just one middleware: The authentication middleware. It uses the configured authentication schemes to perform the authentication of users so that—once set up correctly—the authenticated user is available throughout the application, e.g. in controllers, MVC filters, Razor views, etc.

Does this .AddJwtBearer() middleware validate the ID Token for me if I
have the correct JwtBearerOptions defined (like I do above)? Or do I
need to manually validate the ID token against the JWKs from the JWKs
URI?
AddJwtBearer is only used by APIs to validate the access token and create a user (ClaimsPrincipal) out of it. It's all it does. It does not deal with id token.
In general its easier to put the API on a separate service, to make it more clear who is doing what. when you mix both the client and API in the same service, it can be harder to reason about it.
If I have to manually validate the ID token using the JWKs from the
JWKs URI, how do I store these JWKs when the middleware makes the
request to the /.well-known/keys endpoint? How do I get the cookies
that correspond to the access token and refresh token, and then send
the refresh token to my authorization server?
The ID token is validated and handled for you by AddOpenIdConnect. You dont need to validate the ID-token by yourself. AddOpenIdConnect will create the cookie and optionally store the tokens as well in the cookie.
Overall, what do these two middlewares handle for me, that I shouldn't
need to manually do (i.e token validation and/or token renewal)?
To summarize:
Ues .AddOpenIdConnect() for the client, that allows the user to login.
Use .AddJswtBearer() for the backend APIs.
Token renewal is a different story that none of them handles out of the box. For that, you can consider to use IdentityModel.AspNetCore or do something by yourself.

Related

IdentityServer Create and Revoke API Tokens

I have a asp.net core hosted blazor application using
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
// ...
}
.AddEntityFrameworkStores<ApplicationDbContext>()
and
builder.Services.AddIdentityServer()
.AddApiAuthorization<IdentityUser, ApplicationDbContext>()
I want to add a feature, that a user can create an api token with passing an (optional) expiration date, and name.
The created tokens should be revokable.
Ideally the tokens are limited to api endpoints.
Is ServerSideSessions the way to go?
Since IdentityServer is rotating keys, will it be capable of validating tokens that were issued +1 year ago?
The reason for this is, that we have monitoring systems, that monitor data coming from that api. these systems only can issue http calls, no oauth integration.
EDIT
I found https://github.com/DuendeSoftware/Samples/blob/main/IdentityServer/v6/PAT/src/IdentityServer/Pages/PAT/Index.cshtml.cs
It seems like I can create Tokens with a Lifetime with Injecable ITokenService.
Still i didn't find a default way to revoke tokens.
I think IdentityServer is not the right tool to handle API Tokens. What IdentityServer can handle is Oauth Clients and their secrets but that just allowed an oauth capable client to create a token that can be directly used on an API endpoint. Server side session just stores the user data on the server side not in a cookie so this is not the right option.
In my opinion the right way to handle that is to create a custom AuthenticationHandler maybe similar to this:
https://rmauro.dev/api-key-authentication-extending-the-native-implementation/
In there you can get access to a dbcontext for example where the API Tokens are stored and you can validate them.
You can then use AddPolicyScheme to switch between either the apiauthentication or your custom scheme.
https://code-maze.com/dotnet-multiple-authentication-schemes/
Probably something like this:
builder.Services.AddAuthentication( x=> x.DefaultScheme == "TOKEN_OR_JWT")
.AddIdentityServerJwt()
.AddScheme<YourCustomHandler>("YOUR-CUSTOM-SCHEME")
.AddPolicyScheme("TOKEN_OR_JWT", "TOKEN_OR_JWT", options =>
{
// runs on each request
options.ForwardDefaultSelector = context =>
{
// check if an API-TOKEN header is present
string? apiToken = context.Request.Headers["API-TOKEN"];
if (!string.IsNullOrEmpty(apiToken))
return "YOUR-CUSTOM-SCHEME";
// otherwise always use the jwt scheme for identityserver
return IdentityServerJwtConstants.IdentityServerJwtScheme;
};
});

Web API Identity User with JWT on two API's sharing the same Auth params

I am really struggling to find the answer to this question. I have created a registration WebApi that is working. I can register a user, again roles, login and receive back a JWT. This api will be hosted online to allow users to access my page.
The frontend is a Blazor Web Assembly Application. This also communicates to another API, this API is installed locally on a clients machine, that allows communication to another application.
A lot of these calls to the local api need to be authorized.
This is the part I am really struggling to find an answer for.
How do I get this API to authorize against the JWT that is being given out by the other API. All documentation I have found bundles these features together.
What have I tried?
I have tried added the same auth properties to this local api, but it appears I will also need to provide it access to the database for it to build, which of course isn't ideal and doesn't seem correct.
If someone could give me some pointers, that would be fantastic!
To authenticate the requests with the other API, that API must validate the token with the same secret it was signed with.
So, if you're issuing the token with:
var jwtHandler = new JwtSecurityTokenHandler();
var accessToken = jwtHandler.CreateJwtSecurityToken(
issuer: _environment.ApplicationName,
audience: _environment.ApplicationName,
subject: principal.Identities.First(),
expires: DateTime.Now.AddMinutes(_options.ExpirationMinutes),
signingCredentials: _options.SigningCredentials // <-- key used to sign the token
);
The other service must validate it with the same key:
services.AddAuthentication(/*...*/).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// ValidateIssuer = false,
// ValidateAudience = false,
ValidateIssuerSigningKey = true, // <--
IssuerSigningKey = jwtOptions.SigningCredentials.Key, // <-- same key
};
});
The only thing you really need to share between the apps is the signing key.
But this isn't ideal if you're dealing with more apps. Would you share the key with all of them? You might, but what happens if you need to reset the key? You'd have to reset it in all apps.
For those cases, a better solution would be to delegate the token signing to a separate service, like Identity Server (or any other OIDC provider), then use & validate the tokens issued by that service in all apps.

What is the point of configuring DefaultScheme and DefaultChallengeScheme on ASP.NET Core?

I am learning how security works on ASP.NET Core 2.0 and IdentityServer4. I set up the projects with IdentityServer, API and ASP.NET Core MVC Client App.
ConfigureService method on Client App as in below. Here I am confusing on DefaultScheme and DefaultChallengeScheme. What is the point of configuring those? A detailed description on how it works would be really helpful if possible.
I already seen instead of DefaultScheme, DefaultSignInScheme also works, but how does it work? What is the difference of those?
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
//options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
//options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie("Cookies")
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SignInScheme = "Cookies";
options.RequireHttpsMetadata = false;
options.Authority = "http://localhost:5000/";
options.ClientId = "mvcclient";
options.SaveTokens = true;
});
}
First of all note that you are not using ASP.NET Core Identity there. Identity is the user management stack that builds on top of the authentication system. You appear to be using OpenID Connect with an IdentityServer as the provider, so your web application will only consume the OIDC information but not have to manage its own identities (it may be possible that the IdentityServer is using ASP.NET Core Identity though).
The way the authentication stack works in ASP.NET Core is that you can configure a set of authentication schemes. Some of these schemes are meant to be used in combination, for example the cookie authentication scheme is rarely used on its own, but there are also schemes that can be used completely separate (for example JWT Bearer authentication).
Authentication actions
In the authentication world, there are certain actions that you can perform:
Authenticate: To authenticate basically means to use the given information and attempt to authenticate the user with that information. So this will attempt to create a user identity and make it available for the framework.
For example, the cookie authentication scheme uses cookie data to restore the user identity. Or the JWT Bearer authentication scheme will use the token that is provided as part of the Authorization header in the request to create the user identity.
Challenge: When an authentication scheme is challenged, the scheme should prompt the user to authenticate themselves. This could for example mean that the user gets redirected to a login form, or that there will be a redirect to an external authentication provider.
Forbid: When an authentication scheme is forbidden, the scheme basically just responds with something that tells the user that they may not do whatever they attempted to do. This is commonly a HTTP 403 error, and may be a redirect to some error page.
Sign-in: When an authentication scheme is being signed in, then the scheme is being told to take an existing user (a ClaimsPrincipal) and to persist that in some way. For example, signing a user in on the cookie authentication scheme will basically create a cookie containing that user’s identity.
Sign-out: This is the inverse of sign-in and will basically tell the authentication scheme to remove that persistance. Signing out on the cookie scheme will effectively expire the cookie.
Note that not all authentication schemes can perform all options. Sign-in and sign-out are typically special actions. The cookie authentication scheme is an example that supports signing in and out, but the OIDC scheme for example cannot do that but will rely on a different scheme to sign-in and persist the identity. That’s why you will usually see the cookie scheme with it as well.
Typical authentication flow
Authentication schemes can be used explicitly. When you use one of the authentication extension methods on the HttpContext, for example httpContext.AuthenticateAsync(), then you can always explicitly specify what authentication scheme you want to use for this operation.
So if you, for example, want to sign in with the cookie authentication scheme "Cookie", you could simply call it like this from your code:
var user = new ClaimsPrincipal(…);
await httpContext.SignInAsync(user, "Cookie");
But in practice, calling the authentication directly and explicitly like that is not the most common thing to do. Instead, you will typically rely on the framework to do authentication for you. And for that, the framework needs to know which authentication scheme to use for what operation.
That is what the AuthenticationOptions are for. You can configure those options so that you can explicitly define what authentication scheme to use as the default for each of those authentication actions:
DefaultAuthenticateScheme: Sets the default scheme to use when authenticating.
DefaultChallengeScheme: Sets the default scheme to use when challenging.
DefaultForbidScheme: Sets the default scheme to use when access is forbidden.
DefaultSignInScheme: Sets the default scheme to sign in.
DefaultSignOutScheme: Sets the default scheme to sign out.
DefaultScheme: Sets the default fallback scheme (see below).
You typically don’t configure all those properties. Instead, the framework has some default fallbacks, so you can configure just a subset of those properties. The logic is like this:
Authenticate: DefaultAuthenticateScheme, or DefaultScheme
Challenge: DefaultChallengeScheme, or DefaultScheme
Forbid: DefaultForbidScheme, or DefaultChallengeScheme, or DefaultScheme
Sign-in: DefaultSignInScheme, or DefaultScheme
Sign-out: DefaultSignOutScheme, or DefaultScheme
As you can see, each of the authentication actions falls back to DefaultScheme if the specific action’s default isn’t configured. So what you will typically see is the DefaultScheme being configured, and then the specific actions are configured for those where a different scheme is required.
Your example shows this pretty well: With OIDC, you will need a sign-in scheme that can persist the identity that is provided by the external authentication provider. So you will usually see the OIDC and cookie authentication schemes:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});
For the user, the normal interaction is through the cookie authentication scheme: When they access the web application, the cookie authentication scheme will attempt to authenticate them using their cookie. So using the cookie authentication scheme as the default scheme for all operations.
The exception is when challenging the authentication: In that case, we want the user to be redirected to the OIDC provider, so they can log in there and return with an identity. So we set the default challenge scheme to the OIDC scheme.
In addition, we also link the OIDC scheme with the cookie scheme. When the user gets challenged and logs in with their external authentication provider, they will get sent back to the web application with their external identity. The OIDC scheme cannot persist that identity though, so it signs in using a different scheme—the cookie scheme—which will then persist the identity on behalf of the OIDC scheme. So the cookie scheme will create a cookie for the OIDC identity, and on the next request, the cookie scheme (which is the default scheme) will be able to authenticate the user again using that cookie.
So most of the time, you will be fine with just specifying the default scheme and then depending on your authentication setup maybe change one or two explicit actions. But theoretically, you can totally set up a very complex setup of different defaults and multiple schemes: The framework gives you a lot of flexibility here.

How to get a token for downstream service in AAD

I have an MVC application in which I use OpenIdConnectAuthenticationMiddleware to authenticate the user against AAD. This MVC application uses a few backend services that require the user's authentication context.
If I register these services separately in AAD, I can get a token for them using
AuthenticationContext.AcquireTokenSilentAsync. But registering these services separately with AAD seems wrong as they would require the user to consent to them separately (they are really part of the application).
So I'd like to use the JWT token I got from AAD when the user authenticated and use that as the bearer token for calling the downstream services. I realize that these services need to have the same audience as the MVC application.
But how do I get that JWT token. The ClaimPrincipal's first identity does not have a bootstrap context.
Please note that having your services admit tokens with the same audience opens you up to token forwarding attacks. I would not recommend that. Also, the consent should happen in a single page and with a single click - hence in terms of user impact there isn't really much difference.
That said. If you are really set in it, you can enforce the presence of the token in the bootstrapcontext by switching to true the flag SaveSignInToken. See
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters{SaveSigninToken=true},
PostLogoutRedirectUri = postLogoutRedirectUri
});
Edit The below is one way to achieve this, but it has some security implications. There is also a flag you can set for using the bootstrap context. Please see vibronet's answer for more.
In the OpenIdConnectAuthenticationOptions, if you configure a handler for the SecurityTokenValidated or AuthorizationCodeRecieved notifications, you can access the id_token in the notification's properties. You can then use that id_token as the bearer token in your service calls. There are several different ways you might make that id_token available in your controllers.
One caveat: the id_token will have the clientId of your web app as the aud claim, not the app id uri. So in your services, you should use the clientId guid as your audience.

Persisting the OAuth2 bearer token when using Thinktecture Identity Server

I've been following the Thinktecture Identity Server example of OAuth2 Resource Owner Password Flow found at http://leastprivilege.com/2012/11/01/oauth2-in-thinktecture-identityserver-v2-resource-owner-password-flow/
I have the example working and returning JWT tokens successfully via the following process
Use the Thinktecture OAuth2Client to retrieve the access token
Retrieve the signing certificate from the "Trusted People" store on the client machine
Using the certificate and creating a new JwtSecurityTokenHandler and TokenValidationParameters and calling tokenHandler.ValidateToken to get a ClaimsPrincipal
From here I am authorized, but I am uncertain of the best way to persist the token for further requests. I tried using
var sessionToken = new SessionSecurityToken(principal, TimeSpan.FromHour(8));
FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(sessionToken);
But I do not have a SessionAuthenticationModule registered. I tried using the Identity and Access wizard to get this in place, but it makes many changes to config and tries to set things up for passive authentication.
I could use a traditional FormsAuthentication cookie (.aspnetAuth) but I remember discussion that an advantage of the .FedAuth cookie was that it was naturally split into several cookies if the size grew too big.
I'm struggling to find an article that completes the picture for me. I need the bearer token for accessing various APIs further down the stack. I have working examples of this for SSO/passive authentication, because most of the work is done for you. I'm just not sure of the best pattern for use when using the Resource Owner Password flow.
So
Have I missed a more straightforward way to achieve this with Thinktecture Identity Model and Server?
Should I try to create a FedAuth cookie so that I can reuse the various Messagehandler/filter components that are already setup for WIF?
Otherwise - is there anything particularly wrong with simply putting the access token in the UserData section of the FormsAuthentication cookie?
Try to look at this question: WIF Security Token Caching.
I believe this code might do
var sessionSecurityToken = new SessionSecurityToken(principal, TimeSpan.FromHours(Convert.ToInt32(System.Web.Configuration.WebConfigurationManager.AppSettings["SessionSecurityTokenLifeTime"])))
{
IsPersistent = true, // Make persistent
IsReferenceMode = true // Cache on server
};
FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(sessionSecurityToken);

Categories

Resources