6

I am using Visual Studio 2015 Enterprise and ASP.NET vNext Beta8 to build an endpoint that both issues and consumes JWT tokens. I Originally approached this by generating the tokens myself, as described here. Later a helpful article by @Pinpoint revealed that AspNet.Security.OpenIdConnect.Server (a.k.a. OIDC) can be configured to issue and consume the tokens for me.

So I followed those instructions, stood up an endpoint, and by submitting an x-www-form-urlencoded post from postman I receive back a legit token:

{
  "token_type": "bearer",
  "access_token": "eyJ0eXAiO....",
  "expires_in": "3599"
}

This is great but also where I get stuck. Now, how do I annotate a controller action so that it demands this bearer token?

I thought all I would have to do is decorate my controller method with the [Authorize("Bearer")], add an authentication scheme:

        services.AddAuthorization
        (
            options => 
            {
                options.AddPolicy
                (
                    JwtBearerDefaults.AuthenticationScheme, 
                    builder => 
                    {
                        builder.
                        AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme).
                        RequireAuthenticatedUser().
                        Build();
                    } 
                );
            }
        );

And then call my controller action with the "Authorization bearer eyJ0eXAiO...." header as I had done in my previous example. Sadly, all this approach seems to do though is generate an exception:

An unhandled exception occurred while processing the request.

SocketException: No connection could be made because the target machine actively refused it 127.0.0.1:50000

WebException: Unable to connect to the remote server

HttpRequestException: An error occurred while sending the request.

IOException: IDX10804: Unable to retrieve document from: 'http://localhost:50000/.well-known/openid-configuration'. Microsoft.IdentityModel.Logging.LogHelper.Throw(String message, Type exceptionType, EventLevel logLevel, Exception innerException)

InvalidOperationException: IDX10803: Unable to obtain configuration from: 'http://localhost:50000/.well-known/openid-configuration'. Inner Exception: 'IDX10804: Unable to retrieve document from: 'http://localhost:50000/.well-known/openid-configuration'.'.


Consider the following steps to reproduce (but please don't consider this production worthy code):

  • Apply the ASP.NET Beta8 tooling as described here

  • Open Visual Studio Enterprise 2015 and create a new Web API ASP.NET 5 Preview Template project

  • Change project.json

    {
    "webroot": "wwwroot",
    "version": "1.0.0-*",

    "dependencies": {
    "Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8",
    "Microsoft.AspNet.Mvc": "6.0.0-beta8",
    "Microsoft.AspNet.Server.Kestrel": "1.0.0-beta8",
    "Microsoft.AspNet.Authentication.JwtBearer": "1.0.0-beta8",
    "AspNet.Security.OpenIdConnect.Server": "1.0.0-beta3",
    "Microsoft.AspNet.Authentication.OpenIdConnect": "1.0.0-beta8",
    "Microsoft.Framework.ConfigurationModel.Json": "1.0.0-beta4",
    "Microsoft.AspNet.Diagnostics": "1.0.0-beta8"
    },

    "commands": {
    "web": "Microsoft.AspNet.Server.Kestrel"
    },

    "frameworks": {
    "dnx451": { }
    },

    "exclude": [
    "wwwroot",
    "node_modules"
    ],
    "publishExclude": [
    ".user",
    "
    .vspscc"
    ]
    }

  • Change Startup.cs as follows (this is courtesy of @Pinpoint's original article; I have removed comments and added the AddAuthorization snip):

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthorization
        (
            options => 
            {
                options.AddPolicy
                (
                    JwtBearerDefaults.AuthenticationScheme, 
                    builder => 
                    {
                        builder.
                        AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme).
                        RequireAuthenticatedUser().
                        Build();
                    } 
                );
            }
        );
        services.AddAuthentication();
        services.AddCaching();
        services.AddMvc();
        services.AddOptions();
    }

    // Configure is called after ConfigureServices is called.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, IOptions<AppSettings> appSettings)
    {
        app.UseDeveloperExceptionPage();

        // Add a new middleware validating access tokens issued by the OIDC server.
        app.UseJwtBearerAuthentication(options => {
            options.AutomaticAuthentication = true;
            options.Audience = "http://localhost:50000/";
            options.Authority = "http://localhost:50000/";
            options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>
            (
                metadataAddress : options.Authority + ".well-known/openid-configuration",
                configRetriever : new OpenIdConnectConfigurationRetriever(),
                docRetriever    : new HttpDocumentRetriever { RequireHttps = false }
            );
        });

        // Add a new middleware issuing tokens.
        app.UseOpenIdConnectServer
        (
            configuration => 
            {
                configuration.Options.TokenEndpointPath= "/authorization/v1";
                configuration.Options.AllowInsecureHttp = true;
                configuration.Provider = new OpenIdConnectServerProvider {

                    OnValidateClientAuthentication = context => 
                    {
                        context.Skipped();
                        return Task.FromResult<object>(null);
                    },

                    OnGrantResourceOwnerCredentials = context => 
                    {
                        var identity = new ClaimsIdentity(OpenIdConnectDefaults.AuthenticationScheme);
                        identity.AddClaim( new Claim(ClaimTypes.NameIdentifier, "todo")  );
                        identity.AddClaim( new Claim("urn:customclaim", "value", "token id_token"));
                        context.Validated(new ClaimsPrincipal(identity));
                        return Task.FromResult<object>(null);
                    }
                };
            }
        );

        app.UseMvc();
    }
}
  • Change wizarded ValuesController.cs to specify an Authorize attribute:
[Route("api/[controller]")]
public class ValuesController : Controller
{
    // GET: api/values
    [Authorize("Bearer")] 
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }
}
  • Run the project, and acquire a token using postman. To acquire a token use x-www-form-urlencoded POST with "grant_type" of "password", "username" anything, "password" anything and "resource" the address of the API endpoint. My particular URL for example is http://localhost:37734/authorization/v1.

  • Copy the Base64 encoded token, then use the token to call the wizarded values controller using postman. To use the token make a GET with the headers Content-Type application/json and Authorization bearer eyJ0eXAiO....(your token). My particular URL is http://localhost:37734/api/values.

  • Observe the exception mentioned previously.

If the [Authorize("Bearer")] approach I'm trying above is the wrong way to go I would be very appreciative if someone could help me understand best practices for how to ingest the JWT token using OIDC.

Thank you.

Community
  • 1
  • 1
42vogons
  • 683
  • 7
  • 19
  • Note: you should consider removing `options.TokenValidationParameters.ValidateAudience` and start using the `resource` parameter when making your token request, as it may become mandatory in a future version. Simply add `&resource=http%3A%2F%2Flocalhost%3A37734%2F` in your form data (JS side) and it should work. I've updated my original post to mention that. – Kévin Chalet Oct 28 '15 at 22:28
  • @Pinpoint, ok thanks. I added the resource request when making my token request and receive back a token like so: { "sub": "todo", "iss": "http://localhost:37734/", "aud": "http://localhost:37734/", "exp": 1446131180, "nbf": 1446127580 } But when I comment out options.TokenValidationParameters.ValidateAudience = false and try to use the token I receive "IDX10208: Unable to validate audience. validationParameters.ValidAudience is null or whitespace and validationParameters.ValidAudiences is null." – 42vogons Oct 29 '15 at 14:09
  • Don't forget to add `options.Audience = "http://localhost:37734/";` to your JWT bearer authentication options ;) – Kévin Chalet Oct 29 '15 at 14:13
  • @Pinpoint, thank you that was the problem. In the meantime fixing that has led me to my next question regarding OIDC [authority configuration](http://stackoverflow.com/questions/33426127/aspnet-security-openidconnect-server-asp-net-vnext-authority-configuration-in). – 42vogons Oct 29 '15 at 23:04
  • @Pinpoint, one more question about using the resource parameter. We have noticed that OIDC seems to require options.Audience and the resource to be _exactly_ the same, so for example even with a missing trailing slash passed to the resource this exception is thrown: IDX10214: Audience validation failed. Audiences: 'https://api.contoso.com'. Did not match: validationParameters.ValidAudience: 'https://api.contoso.com/' or validationParameters.ValidAudiences: 'null'. Is there a way for me to perform audience validation myself, or relax the strictness of the comparison? – 42vogons Oct 30 '15 at 18:39
  • Yes, you can use the `AudienceValidator` in the token validation parameters to register your own validator: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/KDev/src/System.IdentityModel.Tokens/TokenValidationParameters.cs#L179 – Kévin Chalet Oct 30 '15 at 18:54

1 Answers1

3

options.Authority corresponds to the issuer address (i.e the address of your OIDC server).

http://localhost:50000/ doesn't seem to be correct as you're using http://localhost:37734/ later in your question. Try fixing the URL and give it another try.

Kévin Chalet
  • 39,509
  • 7
  • 121
  • 131