Openiddict-core: Introducing the OpenIddict validation handler

Created on 27 Apr 2018  ·  20Comments  ·  Source: openiddict/openiddict-core

Starting with RC3 and thanks to a great contribution from @kinosang, OpenIddict now has its dedicated validation handler, based on the aspnet-contrib handler (AspNet.Security.OAuth.Validation).

This handler supports both the default token format (opaque) and reference tokens. Like the aspnet-contrib handler, you can use it as a standalone handler (i.e without having to register the OpenIddict core or server services):

// Register the OpenIddict validation handler.
services.AddOpenIddict()
    .AddValidation();

Resource servers that use reference tokens will have to configure the core services and register the appropriate stores to be able to use it:

// Register the OpenIddict services.
services.AddOpenIddict()

    // Register the OpenIddict core services.
    .AddCore(options =>
    {
        // Register the Entity Framework entities and stores.
        options.UseEntityFrameworkCore()
               .UseDbContext<ApplicationDbContext>();
    })

    // Register the OpenIddict validation handler.
    .AddValidation(options => options.UseReferenceTokens());

The aspnet-contrib handler will continue to be fully supported and will still be usable with OpenIddict so existing and new applications can keep using services.AddAuthentication().AddOAuthValidation() instead of services.AddOpenIddict().AddValidation() for opaque token validation.

Note: OpenIddictValidationHandler lives in the OpenIddict.Validation package, which is referenced by the OpenIddict metapackage. You don't have to add a new PackageReference to be able to use it.

enhancement

Most helpful comment

@pholly I tried doing what you suggested but for some reason it didn't work. What I ended up doing is this:

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = "Bearer";
    options.DefaultChallengeScheme = "Bearer";
});

Also, an important thing to remember is that it only works if I add this AFTER the services.AddOpenIddict() call, it doesn't do anything if I place it before. The good thing is this removes the necessity for a custom authentication scheme provider.

@codeaid, @PinpointTownes -- you guys saved me. Thank you so much!

I was migrating my project to Openiddict-RC3 + reference tokens and the final touch (that I spent almost two days struggling with) was that 'Default authentication scheme' thing that I should have put after adding ASP.NET Core Identity & OpenIddict services to my Startup.cs.

@PinpointTownes , do you think it is worth mentioning in some OpenIddict WIKI page, so that those who combine AspNet.Identity and OpenIddict and are new to the ASP.Net Core world could get it solved quickly?

All 20 comments

Hi @PinpointTownes,

I used JWTs because I didn't want to use introspection in my resource servers because introspection makes a request to the auth server for every new token.

Today I had a need to add an api to my auth server. I had trouble configuring my auth server for jwt validation.

I found a comment you made on an issue where you recommended using the built-in opaque tokens if resource servers are .net only. That's my case so I switched to the default.

I got everything working after searching Issues and Stack Overflow and I think some documentation would be beneficial. For example, it took me a while to find out how to authenticate api controllers with tokens when cookies is the default scheme in asp.net core 2.1.

Here are some suggestions:

  1. Something explaining the different types of tokens / validation (opaque, resource, jwt) with use cases and configuration code for auth server and resource servers, including authorizing controllers in both types of servers. This stuff is new for most people and probably most people don't know what asp.net core configures vs openiddict, and what asp.net core configuration openiddict depends on (e.g. DataProtection).

  2. Update the samples to use the new the new .AddValidation handler in resource servers to validate .net's built-in opaque tokens. They could either link to documentation for using other token types or have commented out code for using JWTs or resource tokens. I'm guessing most people want to use openiddict with SPAs so I would focus on the introspection example.

What do you think? I can help if needed.

Thanks!

Philip

Hey,

I got everything working after searching Issues and Stack Overflow and I think some documentation would be beneficial.

I agree with that. I just lack enough free time to work on that personally.

Something explaining the different types of tokens / validation (opaque, resource, jwt) with use cases and configuration code for auth server and resource servers, including authorizing controllers in both types of servers. This stuff is new for most people and probably most people don't know what asp.net core configures vs openiddict, and what asp.net core configuration openiddict depends on (e.g. DataProtection).

Yep, great idea. Here's the documentation ticket that tracks this part: https://github.com/openiddict/openiddict-documentation/issues/5.

If you want to work on that, please don't hesitate. Let me know if you need additional details.

Update the samples to use the new the new .AddValidation handler in resource servers to validate .net's built-in opaque tokens.

All the samples have been updated to use it, except the implicit flow that still uses introspection. We could certainly update it to use the validation handler too.

I'll take a stab at it. Are these statements correct?

  1. Reference tokens are opaque tokens, just validated differently. No additional openiddict server configuration is needed to create them.
  2. Reference tokens can be validated using either:

    1. Introspection (openid connect, not openiddict specific), which requires connecting to openid connect server https endpoint for either every new token or every new request. Caching can be configured if validating every request.

    2. Openiddict validation handler, which requires connecting to the openiddict database every request.

I'll take a stab at it. Are these statements correct?

Yep, that's exact. Basically, reference tokens are opaque tokens that are stored in the database and for which we instead return a crypto-secure random 256-bit ID to the client application.

I see. So the openiddict server does have to be configured to serve reference tokens, and sure enough there's options.UseReferenceTokens().

The default opaque tokens can be validated in api servers by either Introspection or shared DataProtection with ApplicationName.

It's nice that validation is now in the openiddict library because users don't have to add aspnet-contrib nuget package source.

Did you consider making a validation extension method for services.AddAuthentication() instead? Like services.AddAuthentcation().AddOpenIddictValidation(). To me that "feels" better in resource servers than having to add openiddict with services.AddOpenIddict().AddValidation(), and is consistent with other validations like JwtBearerAuthentication and OAuthAuthentication. Not a big deal though. Just thought I'd mention it.

Did you consider making a validation extension method for services.AddAuthentication() instead? Like services.AddAuthentcation().AddOpenIddictValidation()

I did! I didn't opt for this option for 3 reasons:

  • The aspnet-contrib validation handler is registered using services.AddAuthentication().AddOAuthValidation(). Having an OpenIddict-specific extension would have been confusing.

  • The AuthenticationBuilder extensions usually return AuthenticationBuilder to enable fluent-style programming. Yet, in OpenIddict, we now have our specialized builders (for core, server and validation) that offer a better intellisense experience and don't fit well with AuthenticationBuilder.

  • ASP.NET Core Identity uses pretty much the same pattern: when you use services.AddIdentity(), it actually calls services.AddAuthentication().AddCookie([application cookie]).AddCookie([external cookie]). At least, OpenIddict is consistent with what Identity does.

Makes sense.

Created pull request finally! Not done yet though.

I was going to create a new issue but seeing how there is a ticket regarding validation I'll post my question here.

Long story short - after being a PHP developer for more than a decade I recently switched to .NET Core. Loving everything so far and only now I realise how lacking PHP is 🤦‍♂️...

Anyway, I'm rewriting my current API from PHP into ASP.NET Core and one of the first things I'm trying to do is port my OAuth2 authentication. I spent several days trying to implement IdentityServer as it kept popping up in search results but all to no avail. Then I found OpenIddict...

It looked much nicer from the first glance and much more user friendly so I decided to have a go. Copy/pasting examples is obviously not difficult but I've struck the same place as I was at with IdentityServer - I can get my API to issue reference tokens including refresh tokens and also exchange refresh tokens for new access tokens. However, for the love of god, no matter what I try, I cannot find a way for the framework to validate the token that I pass in the headers!

I'm not kidding when I say that I'm about to give up on the whole .NET world. I've spent almost two weeks trying to get at least one of these libraries to work but nada.

Here is my Startup.cs file: https://gist.github.com/codeaid/7024791a34d373e4a1951bbd0e177680#file-1-startup-cs

As you can see it's pretty bog standard and is mostly a carbon copy of the sample projects.

If you scroll down you will see AuthorizationController.cs, which works as expected (tokens get generated and persisted in the database.

Last, but not least, I have a TestController.cs, which I'm using to test the authorization. I can't tell you how many days passed until I stumbled upon a random comment by someone saying [Authorize] won't work and needs to be [Authorize(AuthenticationSchemes = "Bearer")]. 😡

As my final hope, I asked a seasoned .NET developer to help me out and even he couldn't get IdentityServer working. Don't know what results he'd have with OpenIddict but seeing how he got to the same state as me, I don't think he'd succeed.

Can one of you smart people please help me out? What am I missing and what do I need to add to make it validate access tokens?

P.S.
Some logs for more information

Creating token:

[21:52:01 Information] OpenIddict.Server.OpenIddictServerHandler
The token response was successfully returned: {
  "token_type": "Bearer",
  "access_token": "[removed for security reasons]",
  "expires_in": 3600,
  "refresh_token": "[removed for security reasons]"
}.

Trying to access /test/test:

[21:55:06 Information] Microsoft.AspNetCore.Hosting.Internal.WebHost
Request starting HTTP/1.1 GET http://localhost:5000/test/test

[21:55:06 Information] OpenIddict.Validation.OpenIddictValidationHandler
Bearer was not authenticated. Failure message: Authentication failed because the access token was invalid.

[21:55:06 Debug] Microsoft.AspNetCore.Routing.Tree.TreeRouter
Request successfully matched the route with name 'null' and template 'test/test'.

[21:55:06 Information] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Route matched with {action = "Test", controller = "Test"}. Executing action Resolve.Api.Controllers.TestController.Test (Resolve.Api)

[21:55:06 Debug] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Execution plan of authorization filters (in the following order): ["Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter"]

[21:55:06 Debug] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Execution plan of resource filters (in the following order): ["Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.SaveTempDataFilter"]

[21:55:06 Debug] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Execution plan of action filters (in the following order): ["Microsoft.AspNetCore.Mvc.Internal.ControllerActionFilter (Order: -2147483648)", "Microsoft.AspNetCore.Mvc.Infrastructure.ModelStateInvalidFilter (Order: -2000)", "Microsoft.AspNetCore.Mvc.ModelBinding.UnsupportedContentTypeFilter"]

[21:55:06 Debug] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Execution plan of exception filters (in the following order): ["None"]

[21:55:06 Debug] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Execution plan of result filters (in the following order): ["Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.SaveTempDataFilter"]

[21:55:06 Information] OpenIddict.Validation.OpenIddictValidationHandler
Bearer was not authenticated. Failure message: Authentication failed because the access token was invalid.

[21:55:06 Information] Microsoft.AspNetCore.Authorization.DefaultAuthorizationService
Authorization failed.

[21:55:06 Information] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.

[21:55:06 Information] Microsoft.AspNetCore.Mvc.ChallengeResult
Executing ChallengeResult with authentication schemes (["Bearer"]).

[21:55:06 Information] OpenIddict.Validation.OpenIddictValidationHandler
AuthenticationScheme: Bearer was challenged.

[21:55:06 Information] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Executed action Foo.Bar.Controllers.TestController.Test (Foo.Bar) in 1.9191ms

Here's the output of my discovery document:

{
    "issuer": "http://localhost:5000/",
    "token_endpoint": "http://localhost:5000/connect/token",
    "userinfo_endpoint": "http://localhost:5000/me",
    "jwks_uri": "http://localhost:5000/.well-known/jwks",
    "grant_types_supported": [
        "password",
        "refresh_token"
    ],
    "scopes_supported": [
        "openid",
        "offline_access"
    ],
    "claims_supported": [
        "aud",
        "exp",
        "iat",
        "iss",
        "jti",
        "sub"
    ],
    "subject_types_supported": [
        "public"
    ],
    "token_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post"
    ],
    "claims_parameter_supported": false,
    "request_parameter_supported": false,
    "request_uri_parameter_supported": false
}

Hey. I drafted some documentation but it hasn't been vetted by @PinpointTownes yet: https://github.com/openiddict/openiddict-documentation/pull/20

At first glance -

  • You still have remnants of IdentityServer in your using statements and in .AddAuthentation
  • Remove services.AddAuthentication()... - the whole section - if you are using reference tokens. Reference tokens are validated by the OpenIddict handler. Also, because you're using Identity, I think you still want cookies to be your default authentication scheme so it's best not to set one.
  • In your OpenIddict configuration, you're missing .AddValidation(options => options.UseReferenceTokens()). this is probably why token validation fails

Are you using reference tokens because you want small access token size and want to have more control over revoking access? The default token format works well. IdentityServer and most other authorization servers use JWTs, which you must use if you need to interop with non-.net api servers.

Welcome to .NET land. .NET / ASP.NET Core is great, but because things are changing every release it's a little hard keeping up with updates (e.g. Asp.net core Razor pages, Identity framework). But the framework does a lot of heavy lifting for you and Visual Studio is a great IDE.

The best part about PHP to me is Processwire :) I've considered switching my company over to PHP just because of it. Too much api code, and now authorization server code in .NET though.

For OpenIddict questions, I recommend asking in the Gitter room: https://gitter.im/openiddict/openiddict-core. Kevin Chalet responds pretty quickly there.

Oh my god, @pholly! You are an absolute legend!!! It worked :D Removed the whole AddAuthentication section and added options.UseReferenceTokens() as you suggested and I got this from the endpoint:

[
    {
        "type": "sub",
        "value": "13"
    },
    {
        "type": "name",
        "value": "[email protected]"
    },
    {
        "type": "scope",
        "value": "offline_access"
    }
]

I swear I had that validation option set at one point, but something else must've been broken at that time. I owe you a beer if you're ever in London :)

Actually, while you're here - do you know if there's a way to be able to use just [Authorize] instead of having to define [Authorize(AuthenticationSchemes = "Bearer")] above every controller?

Edit: never mind, I found a potential solution:
https://stackoverflow.com/a/46475072/1288418

Oh that's an answer by @PinpointTownes , the creator of OpenIddict.

If you want all your routes to authorize with bearer authorization, you can set it as the default scheme. Since you installed ASP.NET Core Identity cookies are the default authentication scheme.

To set "Bearer" as your default authentication scheme, try adding this to ConfigureServices after registering Identity and OpenIddict:

c# services.AddAuthentication("Bearer");

@pholly I tried doing what you suggested but for some reason it didn't work. What I ended up doing is this:

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = "Bearer";
    options.DefaultChallengeScheme = "Bearer";
});

Also, an important thing to remember is that it only works if I add this AFTER the services.AddOpenIddict() call, it doesn't do anything if I place it before. The good thing is this removes the necessity for a custom authentication scheme provider.

On a side note - I find it strange that service registration order is important. You don't have that in any of the PHP frameworks normally. You just register them in whatever order you want and they're lazy-loaded when they're needed. Some things are a still bit baffling when it comes to ASP.NET for me but I'm sure it'll just take a bit of time to get used to quirks like that, just a shame that almost no documentation mention these things and it's assumed that everyone just knows that.

Also, an important thing to remember is that it only works if I add this AFTER the services.AddOpenIddict() call, it doesn't do anything if I place it before. The good thing is this removes the necessity for a custom authentication scheme provider.

I don't think it's caused by OpenIddict as we never override the default schemes. It's most likely caused by services.AddIdentity(), that registers its application cookie scheme as the default scheme. And indeed, depending on the order, it may not work as you'd expect.

FWIW, for hybrid cookies+tokens apps, I don't recommend opting for tokens as the default scheme (tho' I agree it's a safer default), as you'll likely be blocked by issues at some point (sadly, Identity and the default account/manage controllers are known not to work correctly if cookies is not the default scheme). The custom scheme provider showed in my SO answer is a more reliable alternative if you don't like decorating your API controllers with [Authorize(AuthenticationSchemes = "Bearer")].

Some things are a still bit baffling when it comes to ASP.NET for me but I'm sure it'll just take a bit of time to get used to quirks like that, just a shame that almost no documentation mention these things and it's assumed that everyone just knows that.

I share your pain: https://github.com/aspnet/Identity/issues/1381

@PinpointTownes I got everything working as I wanted to and your library is working beautifully so big thanks for that!

Speaking about the order - I can't really comment as my knowledge is still limited but from what I see in the logs now the framework is not accessing cookies, but only the token from the header, which is exactly what I wanted. As I mentioned it's an API so cookies is the last thing I want to have.

Regarding the custom scheme provider - I'll look into it a bit later but, honestly, everything seems to work by just doing what I mentioned in my previous comment. Hopefully it stays that way. If not, I know where to find a fix :)

Hello, first of all thanks for your great library and all.

Currently I have one problem with the OpenIddictValidationHandler.
At the moment the ValidationHandler runs after any middleware, this makes it hard to actually do the following:

// content of a IMiddlware class
ublic async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
_userMiddleware.LogDebug($"User IsAuthenticated: {context.User.Identity.IsAuthenticated}");
            if (context.User.Identity.IsAuthenticated)
            {
                // get the user from the database or do rbac, whatever
           }
}

because normally the User would already be Authenticated at that point, (i.e. cookie authentication, Identity framework, etc...) [IsAuthenticated] would return true.

But with the OpenIddictValidationHandler the validation/authentication happens after MVC is run:

dbug: Finder.Web.Middleware.UserMiddleware[0]
      User IsAuthenticated: False
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
      Route matched with {action = "GetMessage", controller = "Resource", page = ""}. Executing action Finder.Web.Controllers.ResourceController.GetMessage (Finder.Web)
dbug: OpenIddict.Validation.OpenIddictValidationHandler[8]
      AuthenticationScheme: Bearer was successfully authenticated.
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[1]
      Authorization was successful.

as you can see, it happens after Mvc is initalized app.UseMvc(); while my knowledge to dotnet core is still limited I think it would be good, if the Validation can happen before most middleware's,

I'm also unsure if the flag is really necessary, but for consistency it would be good to have it before the Middleware chain.

(Running return Content($"IsAuthenticated: {User.Identity.IsAuthenticated}") inside a ApiController will yield True, so basically the behavior does not match dotnet core)

Edit:

Looks like the problem is because I'm using:

options.Filters.Add(new AuthorizeFilter(policy));

instead of [Authorize], etc...

Hello, first of all thanks for your great library and all.

Thanks for the kind words!

as you can see, it happens after Mvc is initalized app.UseMvc(); while my knowledge to dotnet core is still limited I think it would be good, if the Validation can happen before most middleware's,

It's not something we can safely control from our side. In 2.0, this is possible if these 2 conditions are met:

  • app.UseAuthentication() appears very early in your Configure method. If it's invoked too late, HttpContext.User will not be populated.
  • OpenIddictValidationDefaults.AuthenticationScheme is configured as the default "authenticate scheme" in the ASP.NET Core authentication options:
services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = OpenIddictValidationDefaults.AuthenticationScheme;
});

Note: the conditions I describe in my post are absolutely not specific to our validation handlers. All the authentication handlers in ASP.NET Core 2.0 work the same way.

@pholly I tried doing what you suggested but for some reason it didn't work. What I ended up doing is this:

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = "Bearer";
    options.DefaultChallengeScheme = "Bearer";
});

Also, an important thing to remember is that it only works if I add this AFTER the services.AddOpenIddict() call, it doesn't do anything if I place it before. The good thing is this removes the necessity for a custom authentication scheme provider.

@codeaid, @PinpointTownes -- you guys saved me. Thank you so much!

I was migrating my project to Openiddict-RC3 + reference tokens and the final touch (that I spent almost two days struggling with) was that 'Default authentication scheme' thing that I should have put after adding ASP.NET Core Identity & OpenIddict services to my Startup.cs.

@PinpointTownes , do you think it is worth mentioning in some OpenIddict WIKI page, so that those who combine AspNet.Identity and OpenIddict and are new to the ASP.Net Core world could get it solved quickly?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jayrulez picture jayrulez  ·  17Comments

kevinchalet picture kevinchalet  ·  40Comments

kevinchalet picture kevinchalet  ·  19Comments

ArcadeRenegade picture ArcadeRenegade  ·  13Comments

ngohungphuc picture ngohungphuc  ·  15Comments