Configure the authorization server endpoint

41,883

Solution 1

EDIT (01/28/2021): AspNet.Security.OpenIdConnect.Server has been merged into OpenIddict as part of the 3.0 update. To get started with OpenIddict, visit documentation.openiddict.com.


Okay, let's recap the different OAuth2 middleware (and their respective IAppBuilder extensions) that were offered by OWIN/Katana 3 and the ones that will be ported to ASP.NET Core:

  • app.UseOAuthBearerAuthentication/OAuthBearerAuthenticationMiddleware: its name was not terribly obvious, but it was (and still is, as it has been ported to ASP.NET Core) responsible for validating access tokens issued by the OAuth2 server middleware. It's basically the token counterpart of the cookies middleware and is used to protect your APIs. In ASP.NET Core, it has been enriched with optional OpenID Connect features (it is now able to automatically retrieve the signing certificate from the OpenID Connect server that issued the tokens).

Note: starting with ASP.NET Core beta8, it is now namedapp.UseJwtBearerAuthentication/JwtBearerAuthenticationMiddleware.

  • app.UseOAuthAuthorizationServer/OAuthAuthorizationServerMiddleware: as the name suggests, OAuthAuthorizationServerMiddleware was an OAuth2 authorization server middleware and was used to create and issue access tokens. This middleware won't be ported to ASP.NET Core: OAuth Authorization Service in ASP.NET Core.

  • app.UseOAuthBearerTokens: this extension didn't really correspond to a middleware and was simply a wrapper around app.UseOAuthAuthorizationServer and app.UseOAuthBearerAuthentication. It was part of the ASP.NET Identity package and was just a convenient way to configure both the OAuth2 authorization server and the OAuth2 bearer middleware used to validate access tokens in a single call. It won't be ported to ASP.NET Core.

ASP.NET Core will offer a whole new middleware (and I'm proud to say I designed it):

  • app.UseOAuthAuthentication/OAuthAuthenticationMiddleware: this new middleware is a generic OAuth2 interactive client that behaves exactly like app.UseFacebookAuthentication or app.UseGoogleAuthentication but that supports virtually any standard OAuth2 provider, including yours. Google, Facebook and Microsoft providers have all been updated to inherit from this new base middleware.

So, the middleware you're actually looking for is the OAuth2 authorization server middleware, aka OAuthAuthorizationServerMiddleware.

Though it is considered as an essential component by a large part of the community, it won't be ported to ASP.NET Core.

Luckily, there's already a direct replacement: AspNet.Security.OpenIdConnect.Server (https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server)

This middleware is an advanced fork of the OAuth2 authorization server middleware that comes with Katana 3 but that targets OpenID Connect (which is itself based on OAuth2). It uses the same low-level approach that offers a fine-grained control (via various notifications) and allows you to use your own framework (Nancy, ASP.NET Core MVC) to serve your authorization pages like you could with the OAuth2 server middleware. Configuring it is easy:

ASP.NET Core 1.x:

// Add a new middleware validating access tokens issued by the server.
app.UseOAuthValidation();

// Add a new middleware issuing tokens.
app.UseOpenIdConnectServer(options =>
{
    options.TokenEndpointPath = "/connect/token";

    // Create your own `OpenIdConnectServerProvider` and override
    // ValidateTokenRequest/HandleTokenRequest to support the resource
    // owner password flow exactly like you did with the OAuth2 middleware.
    options.Provider = new AuthorizationProvider();
});

ASP.NET Core 2.x:

// Add a new middleware validating access tokens issued by the server.
services.AddAuthentication()
    .AddOAuthValidation()

    // Add a new middleware issuing tokens.
    .AddOpenIdConnectServer(options =>
    {
        options.TokenEndpointPath = "/connect/token";

        // Create your own `OpenIdConnectServerProvider` and override
        // ValidateTokenRequest/HandleTokenRequest to support the resource
        // owner password flow exactly like you did with the OAuth2 middleware.
        options.Provider = new AuthorizationProvider();
    });

There's an OWIN/Katana 3 version, and an ASP.NET Core version that supports both .NET Desktop and .NET Core.

Don't hesitate to give the Postman sample a try to understand how it works. I'd recommend reading the associated blog post, that explains how you can implement the resource owner password flow.

Feel free to ping me if you still need help. Good luck!

Solution 2

With @Pinpoint's help, we've wired together the rudiments of an answer. It shows how the components wire together without being a complete solution.

Fiddler Demo

With our rudimentary project setup, we were able to make the following request and response in Fiddler.

Request

POST http://localhost:50000/connect/token HTTP/1.1
User-Agent: Fiddler
Host: localhost:50000
Content-Length: 61
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=my_username&password=my_password

Response

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 1687
Content-Type: application/json;charset=UTF-8
Expires: -1
X-Powered-By: ASP.NET
Date: Tue, 16 Jun 2015 01:24:42 GMT

{
  "access_token" : "eyJ0eXAiOi ... 5UVACg",
  "expires_in" : 3600,
  "token_type" : "bearer"
}

The response provides a bearer token that we can use to gain access to the secure part of the app.

Project Structure

This is the structure of our project in Visual Studio. We had to set its Properties > Debug > Port to 50000 so that it acts as the identity server that we configured. Here are the relevant files:

ResourceOwnerPasswordFlow
    Providers
        AuthorizationProvider.cs
    project.json
    Startup.cs

Startup.cs

For readability, I've split the Startup class into two partials.

Startup.ConfigureServices

For the very basics, we only need AddAuthentication().

public partial class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication();
    }
}

Startup.Configure

public partial class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
        JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();

        // Add a new middleware validating access tokens issued by the server.
        app.UseJwtBearerAuthentication(new JwtBearerOptions
        {
            AutomaticAuthenticate = true,
            AutomaticChallenge = true,
            Audience = "resource_server_1",
            Authority = "http://localhost:50000/",
            RequireHttpsMetadata = false
        });

        // Add a new middleware issuing tokens.
        app.UseOpenIdConnectServer(options =>
        {
            // Disable the HTTPS requirement.
            options.AllowInsecureHttp = true;

            // Enable the token endpoint.
            options.TokenEndpointPath = "/connect/token";

            options.Provider = new AuthorizationProvider();

            // Force the OpenID Connect server middleware to use JWT
            // instead of the default opaque/encrypted format.
            options.AccessTokenHandler = new JwtSecurityTokenHandler
            {
                InboundClaimTypeMap = new Dictionary<string, string>(),
                OutboundClaimTypeMap = new Dictionary<string, string>()
            };

            // Register an ephemeral signing key, used to protect the JWT tokens.
            // On production, you'd likely prefer using a signing certificate.
            options.SigningCredentials.AddEphemeralKey();
        });

        app.UseMvc();

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    }
}

AuthorizationProvider.cs

public sealed class AuthorizationProvider : OpenIdConnectServerProvider
{
    public override Task ValidateTokenRequest(ValidateTokenRequestContext context)
    {
        // Reject the token requests that don't use
        // grant_type=password or grant_type=refresh_token.
        if (!context.Request.IsPasswordGrantType() &&
            !context.Request.IsRefreshTokenGrantType())
        {
            context.Reject(
                error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
                description: "Only grant_type=password and refresh_token " +
                             "requests are accepted by this server.");

            return Task.FromResult(0);
        }

        // Since there's only one application and since it's a public client
        // (i.e a client that cannot keep its credentials private), call Skip()
        // to inform the server that the request should be accepted without 
        // enforcing client authentication.
        context.Skip();

        return Task.FromResult(0);
    }

    public override Task HandleTokenRequest(HandleTokenRequestContext context)
    {
        // Only handle grant_type=password token requests and let the
        // OpenID Connect server middleware handle the other grant types.
        if (context.Request.IsPasswordGrantType())
        {
            // Validate the credentials here (e.g using ASP.NET Core Identity).
            // You can call Reject() with an error code/description to reject
            // the request and return a message to the caller.

            var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
            identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "[unique identifier]");

            // By default, claims are not serialized in the access and identity tokens.
            // Use the overload taking a "destinations" parameter to make sure 
            // your claims are correctly serialized in the appropriate tokens.
            identity.AddClaim("urn:customclaim", "value",
                OpenIdConnectConstants.Destinations.AccessToken,
                OpenIdConnectConstants.Destinations.IdentityToken);

            var ticket = new AuthenticationTicket(
                new ClaimsPrincipal(identity),
                new AuthenticationProperties(),
                context.Options.AuthenticationScheme);

            // Call SetResources with the list of resource servers
            // the access token should be issued for.
            ticket.SetResources("resource_server_1");

            // Call SetScopes with the list of scopes you want to grant
            // (specify offline_access to issue a refresh token).
            ticket.SetScopes("profile", "offline_access");

            context.Validate(ticket);
        }

        return Task.FromResult(0);
    }
}

project.json

{
  "dependencies": {
    "AspNet.Security.OpenIdConnect.Server": "1.0.0",
    "Microsoft.AspNetCore.Authentication.JwtBearer": "1.0.0",
    "Microsoft.AspNetCore.Mvc": "1.0.0",
  }

  // other code omitted
}
Share:
41,883

Related videos on Youtube

Shaun Luttin
Author by

Shaun Luttin

My professional work focuses on designing, testing, implementing/securing, and deploying distributed services. I kind be "that guy" too. Ship it!

Updated on July 09, 2022

Comments

  • Shaun Luttin
    Shaun Luttin almost 2 years

    Question

    How do we use a bearer token with ASP.NET 5 using a username and password flow? For our scenario, we want to let a user register and login using AJAX calls without needing to use an external login.

    To do this, we need to have an authorization server endpoint. In the previous versions of ASP.NET we would do the following and then login at the ourdomain.com/Token URL.

    // Configure the application for OAuth based flow
    PublicClientId = "self";
    OAuthOptions = new OAuthAuthorizationServerOptions
    {
        TokenEndpointPath = new PathString("/Token"),
        Provider = new ApplicationOAuthProvider(PublicClientId),
        AccessTokenExpireTimeSpan = TimeSpan.FromDays(14)
    };
    

    In the current version of ASP.NET, though, the above doesn't work. We've been trying to figure out the new approach. aspnet/identity example on GitHub, for instance, configures Facebook, Google, and Twitter authentication but does not appear to configure a non-external OAuth authorization server endpoint, unless that's what AddDefaultTokenProviders() does, in which case we're wondering what the URL to the provider would be.

    Research

    We've learned from reading the source here that we can add "bearer authentication middleware" to the HTTP pipeline by calling IAppBuilder.UseOAuthBearerAuthentication in our Startup class. This is a good start though we're still not sure of how to set its token endpoint. This didn't work:

    public void Configure(IApplicationBuilder app)
    {  
        app.UseOAuthBearerAuthentication(options =>
        {
            options.MetadataAddress = "meta";
        });
    
        // if this isn't here, we just get a 404
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello World.");
        });
    }
    

    On going to ourdomain.com/meta we just receive our hello world page.

    Further research showed that we can also use the IAppBuilder.UseOAuthAuthentication extension method, and that it takes a OAuthAuthenticationOptions parameter. That parameter has a TokenEndpoint property. So though we're not sure what we're doing, we tried this, which of course didn't work.

    public void Configure(IApplicationBuilder app)
    {
        app.UseOAuthAuthentication("What is this?", options =>
        {
            options.TokenEndpoint = "/token";
            options.AuthorizationEndpoint = "/oauth";
            options.ClientId = "What is this?";
            options.ClientSecret = "What is this?";
            options.SignInScheme = "What is this?";
            options.AutomaticAuthentication = true;
        });
    
        // if this isn't here, we just get a 404
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello World.");
        });
    }
    

    In other words, in going to ourdomain.com/token, there is no error there is just again our hello world page.

    • NikhilGoud
      NikhilGoud over 6 years
      Need same functionality in .Net Core 2.0, can it be achieved?
  • Shaun Luttin
    Shaun Luttin almost 9 years
    From reading what you wrote and relating it to this terrific post by Sakimura, it appears that ASP.NET 5 lets us be both the relying party and the identity provider or to rely on an external, third-party identity provider. Is that right?
  • Kévin Chalet
    Kévin Chalet almost 9 years
    OTB, yep, except that you can't natively add an identity provider to your app anymore (the OAuth2 authorization server has not been ported). In other words, you can be a "relying party" (or a "client application" to be exact) and protect the "resource server" with JWT tokens (your API), but you can't be the "identity provider". The project I mentioned allows you to add this last role to your app, so that you can be your own identity provider.
  • Shaun Luttin
    Shaun Luttin almost 9 years
    The OAuth 2.0 specification uses the term "Authentication Server." Is that term synonymous with the term Identity Provider that the OpenID Connect documentation uses?
  • Kévin Chalet
    Kévin Chalet almost 9 years
    Actually, the OAuth2 specifications only use the term "authorization server" and not "authentication server" (purist will tell you that's because OAuth2 is an authorization protocol). But of course, in OpenID Connect, an identity provider is an authorization/authentication server, so yeah, these concepts are definitely synonyms.
  • Shaun Luttin
    Shaun Luttin almost 9 years
    If we just want to do username/password authentication at our server and then return an access token, then can we just 1. use ASP.NET Identity to authenticate the username/password and then 2. call Context.Authentication.SignIn(OpenIdConnectDefaults.Authenti‌​cationScheme, new ClaimsPrincipal(identity)) with a new simple identity? It seems like this would be a simple way to implement RESTful signin in our application, if we don't mind storing users credentials.
  • Kévin Chalet
    Kévin Chalet almost 9 years
    Yup, but I'd recommend directly using the OpenIdConnectServerProvider.GrantResourceOwnerCredentials notification for that: use ASP.NET Identity to create a principal from the username/password couple and call notification.Validated(principal);. You can call notification.Rejected() to reject the token request if the username of the password is incorrect.
  • Kévin Chalet
    Kévin Chalet almost 9 years
    Note that if you just want to support username/password authentication, you can disable the authorization endpoint, which is useless in this case (set OpenIdConnectServerOptions.AuthorizationEndpoint to PathString.Empty). You can also remove AuthenticationController and AuthorizationController, and simply keep AuthorizationProvider.
  • Shaun Luttin
    Shaun Luttin almost 9 years
    I see that by default the AuthorizationEndpoint is "/connect/authorize" and that makes sense that we need to set it to PathString.Empty. Now, I'm trying to determine what triggers the notifications to fire. In other words, how do we call notification.Validated(principal)? I see that you call this within the AuthorizationProvider though I don't know what calls the AuthorizationProvider's methods. For instance, what calls ValidateClientAuthentication? From the source, it looks like IOpenIdConnectServerProvider is directly processing HTTP; we don't directly call its methods.
  • Kévin Chalet
    Kévin Chalet almost 9 years
    Absolutely. IOpenIdConnectServerProvider's notifications are automatically called by OpenIdConnectServerHandler: you don't have to care about invoking them (and BTW, you can't, since *Notification classes' constructors have all been marked as internal). For instance, ValidateClientAuthentication is always called when a request arrives at the token endpoint: github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Serv‌​er/…
  • Shaun Luttin
    Shaun Luttin almost 9 years
    Perfect. I'm slowly start to grok how this works. I appreciate the instructions. I appreciate how well you documented the source code. Now that I'm reading those comments, the flow is making more sense. GrantResourceOwnerCredentials is really well documented, for instance.
  • Kévin Chalet
    Kévin Chalet almost 9 years
    Ah ah, thanks, but most of these comments have been added by the Katana team, back to the OAuthAuthorizationServerMiddleware era! I only added the comments documenting the new notifications and the different samples.
  • Shaun Luttin
    Shaun Luttin almost 9 years
    We got it working. If you have the time, please provide feedback on the rudimentary setup in the answer that I provided. That would help a great deal.
  • Kévin Chalet
    Kévin Chalet almost 9 years
    It looks pretty nice. One remark though: referencing MVC 6 and its services shouldn't be necessary. Have you tried calling services.AddAuthentication(); from ConfigureServices? It internally invokes services.AddWebEncoders(), which registers IUrlEncoder. Also, await Task.Delay(1); should be replaced by return Task.FromResult<object>(null); to avoid an unnecessary thread switch.
  • Kévin Chalet
    Kévin Chalet almost 9 years
    If you use the last package, you'll also need to add caching services in ConfigureServices: services.AddCaching() (currently part of the Microsoft.Framework.Caching.Memory package)
  • Kévin Chalet
    Kévin Chalet almost 9 years
    The last package = 1.0.0-beta2-0174, which corresponds to this commit: github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Serv‌​er/…
  • Evan
    Evan over 8 years
    For those who don't see the overload for AddClaim that takes the destination(I don't for whatever reason), you can call .WithDestination() on each claim.
  • Kévin Chalet
    Kévin Chalet over 8 years
    I updated the code sample to use SetResources and SetScopes instead of relying on the resource parameter, whose automatic handling will be removed in the next beta: github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Serv‌​er/….
  • superjos
    superjos about 8 years
    @ShaunLuttin in "Without our rudimentary [...]" did you actually mean With ?
  • superjos
    superjos almost 8 years
    BTW for present requirements, a more fit provider name would probably be AuthenticationProvider
  • Kévin Chalet
    Kévin Chalet almost 8 years
    @superjos actually, AuthorizationProvider was fine, as OIDC covers both authorization and authentication. It should also be noted that Shaun doesn't use scope=openid in his sample, so you don't get back an id_token (used for authentication), just an access token that you can send to the API server to access the owner's resources. This sample is probably more about authorization than authentication (but I'm probably nit-picking).
  • superjos
    superjos almost 8 years
    Auth***ation is always a delicate topic, so thank you for nit-picking! From posted scenario (the same as the one I'm working at) it seemed to me that the token, the provider and the whole OIDC behind was only used for authentication. That's why the rename suggestion. Could you share some info/link on that scope=openid thing? I was not aware about that difference.
  • Kévin Chalet
    Kévin Chalet almost 8 years
    Sure, you can learn more about the magic openid scope by reading the OpenID Connect specification: openid.net/specs/openid-connect-core-1_0.html. Though they are not exactly OpenID Connect-specific grants, the OIDC server middleware extends the same concept to the resource owner password credentials and client credentials grants. Add scope=openid and call ticket.SetScopes("openid") and you'll automatically get an identity token.
  • Kévin Chalet
    Kévin Chalet almost 8 years
    Answer updated to use the ASP.NET Core RC2 and ASOS beta5 packages.
  • lostaman
    lostaman almost 8 years
    @Pinpoint I probably doing something wrong here. I'm trying to use this sample in my project (RC2) and cannot get it working. For test purposes I ended up copying this sample as it is (changing just port number). Tokens are generated but when I try to call my API controller (marked with Authorize attribute) with Authorization header Bearer %access_token% I always get 401. Just don't know what possibly I'm doing wrong here.
  • Kévin Chalet
    Kévin Chalet almost 8 years
    @lostaman FWIW, I just tested the snippet as-is in an empty ASP.NET Core RC2 project and it worked like a charm. Have you tried enabling logging? I guess posting a repro would help.
  • lostaman
    lostaman almost 8 years
    @Pinpoint Hm... I've created the new empty solution and tried this sample. Same issue. See my solution here github.com/kamarouski/TestAuth/tree/master/TestAuth
  • Kévin Chalet
    Kévin Chalet almost 8 years
    You need to move the app.UseMvc() call after app.UseJwtBearerAuthentication() or users will never be authenticated when reaching MVC. See stackoverflow.com/a/31998674/542757 for more information.
  • lostaman
    lostaman almost 8 years
    This fixed the problem. Thanks @Pinpoint so much for your help.
  • Kévin Chalet
    Kévin Chalet almost 8 years
    Updated to use the ASP.NET Core RTM/ASOS beta6 bits.
  • NikhilGoud
    NikhilGoud over 6 years
    @Pinpoint How would we achieve same functionality in .net core 2.0
  • Alex Kvitchastiy
    Alex Kvitchastiy almost 3 years
    @KévinChalet hi, is it possible to use AuthorizationProvider in your new openiddict-core ?