Can I create an Identity Server 4 ASP.NET Core API using 2 different token authentication middleware? - asp.net-core

I am trying to figure out if its possible to write an ASP.NET Core API that consumes an identity server token using either Reference Tokens or JWT tokens based on whatever I've configured my identity server to use. The back-end configuration for IS4 is pretty easy, I'm just not convinced that I can configure 2 different token middlewares and my service will both be ok with it and know what to do.
So the idea is:
If my API gets a jwtToken, it attempts to use the jwt middleware for authorization back to identity server.
If my API gets a reference token, it attempts to use the introspection middleware for authorization back to identity server.
Obviously, if the wrong type of token is provided for whatever is configured on the IS4 service, it will fail.
Handling the token endpoint and revocation endpoint should also be easy enough, it's just the middleware magic I'm concerned with.
I know typically you wouldn't want to do this but we have a niche use case for it. All I'm currently concerned with is whether or not its even possible. I'm not familiar with how the auth middleware works in the back-end.

According to the Identity Server 4 Protecting APIs document, we can see that it supports to use both JWTs and reference tokens in asp.net core.
You can setup ASP.NET Core to dispatch to the right handler based on the incoming token, see this blog post for more information.
services.AddAuthentication("token")
// JWT tokens
.AddJwtBearer("token", options =>
{
options.Authority = Constants.Authority;
options.Audience = "resource1";
options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
// if token does not contain a dot, it is a reference token
options.ForwardDefaultSelector = Selector.ForwardReferenceToken("introspection");
})
// reference tokens
.AddOAuth2Introspection("introspection", options =>
{
options.Authority = Constants.Authority;
options.ClientId = "resource1";
options.ClientSecret = "secret";
});

Supporting both JWTs and reference tokens
In addition to #Zhi Lv post you might need to add Authorization policy, Authentication Schemes to allow validating JWT and reference tokens.
Here is the sample code template replace api name, api secret and audience appropriatly.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddJwtBearer(Options =>
{
Options.Authority = "https://identity.domain.com/identity/";
Options.Audience = "resource1"; //your api baseurl e.g if you want userinfo_endpoint specify https://identity.domain.com/identity/connect/userinfo
Options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
})
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://identity.domain.com/identity/";
options.ApiName = "api name / scope";
options.ApiSecret = "api secret / scope secret";
});
services.AddAuthorization(options =>
{
options.AddPolicy("tokens", x =>
{
x.AddAuthenticationSchemes("jwt", "introspection");
x.RequireAuthenticatedUser();
});
});
}

The way I would do it is to use introspection and claims caching in both cases, so that the API does not need to know or care which type of access token it receives.
The introspection would only occur when an access token is first received. Subsequent requests with the same token then use cached claims.
RESOURCES
Blog Post
Sample C# Code

Related

How to set up OpenIddict to rely on AzureAd without using Microsoft.AspNetCore.Identity.UI

Our roles model is different so we can't use the stock Microsoft identity database model and all UX that goes with it, more's the pity.
All I want to do is
use OpenIdDict
have AzureAd do authentication
put my own claims into the claims principal so they go into the identity token when OpenIdDict creates it
I'm not interested in IdentityServer for assorted reasons.
I worked through a tutorial and had no trouble building all this using cookie based authn handled in an AccountController but I cannot figure out how to switch over to Azure and could really use some help.
Startup looks like this
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
// .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
// {
// options.LoginPath = "/account/login";
// });
services.AddAuthentication()
.AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
// from package `Microsoft.Identity.Web`
services.AddDbContext<DbContext>(options =>
{
// Configure the context to use an in-memory store.
options.UseInMemoryDatabase(nameof(DbContext));
// Register the entity sets needed by OpenIddict.
options.UseOpenIddict();
});
services.AddHostedService<TestData>();
var openiddictBuilder = services.AddOpenIddict();
// Register the OpenIddict core components.
openiddictBuilder.AddCore(options =>
{
// Configure OpenIddict to use the EF Core stores/models.
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
});
// Register the OpenIddict server components.
openiddictBuilder.AddServer(options =>
{
options
.AllowAuthorizationCodeFlow().RequireProofKeyForCodeExchange()
.AllowClientCredentialsFlow()
.AllowRefreshTokenFlow()
.SetAuthorizationEndpointUris("/connect/authorize")
.SetTokenEndpointUris("/connect/token")
// Encryption and signing of tokens
.AddEphemeralEncryptionKey()
.AddEphemeralSigningKey()
.DisableAccessTokenEncryption()
// Register scopes (permissions)
.RegisterScopes("api")
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
.UseAspNetCore()
.EnableTokenEndpointPassthrough()
.EnableAuthorizationEndpointPassthrough()
;
});
}
There's an AuthorizeController with an Authorize method that looks like this
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the user principal stored in the authentication cookie.
// var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// If the user principal can't be extracted, redirect the user to the login page.
if (!result.Succeeded)
{
var authprops = new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
};
return Challenge(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: authprops);
}
// Create a new claims principal
var claims = new List<Claim>
{
// 'subject' claim which is required
new Claim(OpenIddictConstants.Claims.Subject, result.Principal.Identity.Name),
new Claim(OpenIddictConstants.Claims.Role,"admin").SetDestinations(
OpenIddictConstants.Destinations.IdentityToken),
new Claim(OpenIddictConstants.Claims.Role,"gerbil wrangler").SetDestinations(
OpenIddictConstants.Destinations.IdentityToken)
};
var claimsIdentity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
// Set requested scopes (this is not done automatically)
claimsPrincipal.SetScopes(request.GetScopes());
// Signing in with the OpenIdDict authentiction scheme causes OpenIdDict
// to issue a code which can be exchanged for an access token
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
As I understand the theory of operation, OpenIddict proxies authentication and then issues a new token. That implies that the AzureAd redirect_uri ought to be set to an endpoint provided by OpenIddict, probably something like signin-openiddict and assuming that I'm right about all that, the client talking to OpenIddict will in turn provide a completely distinct and unrelated redirect_uri. But I haven't found any documentation covering this yet, so feel free to educate me.
In OpenIddict (and IdentityServer) the login and token generation are separated.
Those 2 parts are generally:
The user logs in using arbitrary methods and the authentication cookie is created.
The token endpoint reads the authentication cookie and creates tokens.
To use Azure Ad Authentication, you need to alter the first part to generate an authentication cookie using Azure Ad. To achieve this you'll need to implement the ExternalLogin and ExternalLoginCallback as seen in this example: https://github.com/openiddict/openiddict-core/blob/cda55862bcba67bf3de4ba08cf512ee9e2269cf5/samples/Mvc.Server/Controllers/AccountController.cs#L141
Instead of using the SignInManager, you need to create the authentication cookie yourself in the ExternalLoginCallback method. This can be done by using the HttpContext.SignInAsync method.
The second part (token generation) is left unchanged and should work without modification.

.net Core 3.0 & OpenId Connect - How to configure authetication to validate Azure generated non JWT access token against UserInfo in a WebAPI?

So, we have a SPA that is calling a series a microservices that sits behind an API Gateway. The authentication scheme is OpenId flow against Azure AD. So, it goes as follows: SPA (Public client) gets the access code, then the API gateway (Confidential Client) call the token service in order to get the access token; this is what it gets forwarded to the microservices themselves. So, in the end, our microservices will receive just the non-JWT Access Token. It's important to note that this token is NOT a JWT. In order to validate we're forced to use the /openId/UserInfo endpoint in the azure tenant to check the user with the access token.
We've tried used the AddOpenIdConnect extension in startup, as described [here] https://learn.microsoft.com/en-us/dotnet/architecture/microservices/secure-net-microservices-web-applications/
{
//…
// Configure the pipeline to use authentication
app.UseAuthentication();
//…
app.UseMvc();
}
public void ConfigureServices(IServiceCollection services)
{
var identityUrl = Configuration.GetValue<string>("IdentityUrl");
var callBackUrl = Configuration.GetValue<string>("CallBackUrl");
// Add Authentication services
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = identityUrl;
options.SignedOutRedirectUri = callBackUrl;
options.ClientSecret = "secret";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.RequireHttpsMetadata = false;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("orders");
options.Scope.Add("basket");
options.Scope.Add("marketing");
options.Scope.Add("locations");
options.Scope.Add("webshoppingagg");
options.Scope.Add("orders.signalrhub");
});
}
But this assumes the whole OpenId flow. Regardless of putting the Bearer token in the request, application redirects to the login page.
So the question is, is there any out-of-the-box configuration for this? Or we should rely in some custom handler? In such case, how the user can be properly authenticated within the context? We would need to access the HttpContext.User.Claims in the controllers themselves.
Any hint would be greatly appreciated!
Could I ask a couple of questions - partly for my own understanding:
What causes the token to not be a JWT - I don't think the link you posted explains this?
Are you using some form of token exchange from the original one issued to the SPA?
I have an equivalent solution that works, but I am using a free developer account, so maybe my setup is different:
Token validation code
Write up on messages and config
If I understand how to reproduce your setup - and a rough understanding of the config differences - I may be able to help.
Using the user info endpoint to validate tokens doesn't feel right. It would be good to get token validation working in the standard way.

Identity Server 4 Client Credentials for custom endpoint on token Server

I implemented a token server using Identity Server 4.
I added a custom API endpoint to the token server and struggle with the authentication. The custom endpoint is inherited from ControllerBase and has 3 methods (GET, POST, DELETE).
I intend to call the custom endpoint from within another API using a dedicated client with credentials (server to server) implemented as HttpClient in .NET Core. There is no user involved into this.
For getting the access token I use the IdentityModel DiscoveryClient and TokenEndpoint.
So in sum I did the following so far:
setup "regular" identity server and validate it works -> it works
implement custom endpoint and test it without authorizatio -> it works
add another api resource ("api.auth") with a custom scope "api.auth.endpoint1"
setup a client with client credentials allowing access to scope "api.auth.endpoint1".
implement the HttpClient and test setup -> I get an access token via the Identity Model Token Endpoint.
Now, when I call the endpoint using the HttpClient with the access token I received I get response code 200 (OK) but the content is the login page of the identity server.
The documentation of Identity Server 4 state the use of
services.AddAuthentication()
.AddIdentityServerAuthentication("token", isAuth =>
{
isAuth.Authority = "base_address_of_identityserver";
isAuth.ApiName = "name_of_api";
});
as well as the use of
[Authorize(AuthenticationSchemes = "token")]
Unfortunatly the compiler state that .AddIdentityServerAuthentication can't be found. Do I miss a special nuget?
The nugets I use on the token server so far are:
IdentityServer4 (v2.2.0)
IdentityServer4.AspNetIdentity (v2.1.0)
IdentityServer4.EntityFramework (v2.1.1)
Figured out that part. The missing nuget for AddIdentityServerAuthentication is:
IdentityServer4.AccessTokenValidation
Struggling with the authorization based on the custom scope.
Does anyone know how the security has to be configured?
Configure a client with ClientGrantTypes = client_credentials and your api like this:
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.ApiName = "api.auth";
});
Where ApiName is the name of the resource. Please note that resource != scope. In most samples the resource name is equal to the scope name. But not in your case, where resource name is api.auth and scope name is api.auth.endpoint1.
Configure the client to request the scope.
var tokenClient = new TokenClient(disco.TokenEndpoint, clientId, secret);
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api.auth.endpoint1");
IdentityServer will lookup the Resource name and add that to the token as audience (aud) while the scope is added as claim with type scope.
This should be enough to make it work. Also check the sample project.
Custom authentication scheme and scope based policies for different access rights bundled together looks like that:
// Startup.ConfigureServices
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication("CustomAuthEndpointsAuthenticationScheme", options =>
{
options.Authority = "http://localhost:5000";
options.ApiName = "api.auth"; //IdentityServer4.Models.ApiResource.Name aka Audience
});
services.AddAuthorization(options =>
{
options.AddPolicy("Endpoint1Policy", policy => {
policy.AddAuthenticationSchemes(new string[] { "CustomAuthEndpointsAuthenticationScheme" });
policy.RequireScope("api.auth.endpoint1"); } ); //IdentityServer4.Models.Scope.Name
options.AddPolicy("Endpoint2Policy", policy => {
policy.AddAuthenticationSchemes(new string[] { "CustomAuthEndpointsAuthenticationScheme" });
policy.RequireScope("api.auth.endpoint2"); } ); //IdentityServer4.Models.Scope.Name
} );
// securing the custom endpoint controllers with different access rights
[Authorize(AuthenticationSchemes = "CustomAuthEndpointsAuthenticationScheme", Policy = "Endpoint1Policy")]
It seems not to interfere with the IdentityServer4 default endpoints nor with the ASP.NET Core Identity part.

Secure access only to controller in IdentityServer4

I created a basic IdentityServer4 as per tutorials http://docs.identityserver.io/en/release/quickstarts/0_overview.html.
This consists of APIserver, JSClient and ID4 Server.
OVerall all is good, now i am trying to go one step further, I want to create a basic controller in ID4Server called AuthorizedUserController that is restful, and that can only be accessed by Authorised user from JSClient.
I cannot find any examples on how to achieve this currently so hoping for some guidance.
You could make IdentityServer include bearer token authentication:
services.AddAuthentication()
.AddIdentityServerAuthentication("bearer", options =>
{
options.Authority = "you identityserver base url";
options.ApiName = "identityserver_api";
});
And then have an authorization policy that checks for the scheme and the client ID claim:
services.AddAuthorization(options =>
{
options.AddPolicy("JsClient", config =>
{
config.AddAuthenticationSchemes("bearer");
config.RequireClaim("client_id", "my javascript client");
});
});
And then add an authorize attribute to your controller that specifies this authorization policy:
[Authorize("JsClient")]

JWT Bearer Token Flow

What I want is a method of JWT Generation and JWT Consumption in ASP.NET Core.
No OAuth2 flow, I do have the IdentityServerv3 working with OAuth2 but it is just overkill for a single app accessing an API when I own both sides.
The main source of difficulty I am having is finding out the equivalent of Microsoft.Owin.Security.Jwt in ASP.NET Core. Nothing in this list https://www.myget.org/gallery/aspnetvnext seems to relate. Or is that package actually to stay relevant in with ASP.NET Core?
If you're looking for a (simple) way to generate your own JWT tokens, you should directly use the JwtSecurityTokenHandler. You can find it in the System.IdentityModel.Tokens package on the MyGet repository you mentioned (but the version is a bit old now) or directly on the Azure AD repository, in the System.IdentityModel.Tokens.Jwt package: https://www.myget.org/gallery/azureadwebstacknightly
Of course, using a standard protocol to issue and retrieve your JWT tokens is more than recommended and OAuth2 and OpenID Connect are probably the best candidates for that.
Note that IdentityServer is not the only server that works on ASP.NET 5. I'm personally working on an advanced fork of the OAuth2 authorization server middleware that comes with Katana 3 and that offers a different approach: https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server
app.UseOAuthBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Audience = "http://localhost:54540/",
Authority = "http://localhost:54540/"
});
app.UseOpenIdConnectServer(options =>
{
options.Provider = new AuthorizationProvider();
});
To learn more about this project, I'd recommend reading http://kevinchalet.com/2016/07/13/creating-your-own-openid-connect-server-with-asos-introduction/.
Feel free to ping me on https://jabbr.net/#/rooms/AspNetCore if you need more information.
I've started using OpenIddict and I think it is exactly what you need.
This is essentially all the configuration I needed:
ConfigureServices:
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddOpenIddictCore<Application>(config => config.UseEntityFramework());
Configure
app.UseOpenIddictCore(builder =>
{
// tell openiddict you're wanting to use jwt tokens
builder.Options.UseJwtTokens();
// NOTE: for dev consumption only! for live, this is not encouraged!
builder.Options.AllowInsecureHttp = true;
builder.Options.ApplicationCanDisplayErrors = true;
});
// use jwt bearer authentication
app.UseJwtBearerAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.RequireHttpsMetadata = false;
options.Audience = "http://localhost:58292/";
options.Authority = "http://localhost:58292/";
});
There are one or two other minor things, such as your DbContext needs to derive from OpenIddictContext<ApplicationUser, Application, ApplicationRole, string>.
You can see a full length explanation (including links to the github repo) on this blog post of mine:
http://capesean.co.za/blog/asp-net-5-jwt-tokens/