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.

6
  • 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. Commented Oct 28, 2015 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": "localhost:37734", "aud": "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." Commented Oct 29, 2015 at 14:09
  • Don't forget to add options.Audience = "http://localhost:37734/"; to your JWT bearer authentication options ;) Commented Oct 29, 2015 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. Commented Oct 29, 2015 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: 'api.contoso.com'. Did not match: validationParameters.ValidAudience: '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? Commented Oct 30, 2015 at 18:39

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.