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.
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 usinghttp://localhost:37734/
later in your question. Try fixing the URL and give it another try.