OpenIddict provides a simple solution to implement an OpenID Connect server for any ASP.NET Core 1.1 application.
The official GitHub project is located at
https://github.com/openiddict/openiddict-core. In this tutorial, I will show you how to easily implement
OpenIdDict with a very basic ASP.NET Core 1.1 MVC/WebAPI project created using the dotnet command line interface. Let’s get started:
Pre-requisites:
Creating web application project in Visual Studio 2015
To ensure that you have the correct version of .NET Core installed on your computer, execute the following command in a terminal window:
dotnet --version
If the number 1.01 appears then you have the correct version. We need to know what templates we can use to generate our ASP.NET application using the
dotnet command line interface. Type the following inside your terminal window:
dotnet new --list
At the bottom of the output, you should see this:
Templates Short Name Language Tags
----------------------------------------------------------------------
Console Application console [C#], F# Common/Console
Class library classlib [C#], F# Common/Library
Unit Test Project mstest [C#], F# Test/MSTest
xUnit Test Project xunit [C#], F# Test/xUnit
ASP.NET Core Empty web [C#] Web/Empty
ASP.NET Core Web App mvc [C#], F# Web/MVCASP.NET Core Web API webapi [C#] Web/WebAPI
Solution File sln Solution
Let us use the MVC template to generate an ASP.NET Core 1.1 application. In your workspace, create a folder named
AspToken and go into that folder as follows:
mkdir AspToken
cd AspToken
Use the following command to create an app with individual authentication and SQLite:
dotnet new mvc --auth Individual --framework netcoreapp1.1
In order to run the application, execute the following commands in sequence:
dotnet restore
dotnet ef database update
bower install bootstrap
dotnet run
Point your browser to
http://localhost:5000 and you will see a page that looks like this:
Click on
Register on the top right-side and create a user. I created a user with email =
a@a.a and password =
P@$$w0rd.
Once you create the user, logout.
Open the root folder of your application in Visual Studio Code (or any other editor you prefer). In Visual Studio Code, it will look like this:
Inside the
Controllers folder, create a file named
ValuesController.cs and add to it the following code:
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
namespace AspToken.Controllers {
[Produces("application/json")]
[Route("api/Values")]
public class ValuesController : Controller {
// GET: api/Values
[HttpGet]
public IEnumerable<string> Get() {
return new string[] { "value1", "value2" };
}
// GET: api/Values/5
[HttpGet("{id}", Name = "Get")]
public string Get(int id) {
return "value";
}
// POST: api/Values
[HttpPost]
public void Post([FromBody]string value) {
}
// PUT: api/Values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody]string value) {
}
// DELETE: api/ApiWithActions/5
[HttpDelete("{id}")]
public void Delete(int id) {
}
}
}
Stop the web server, execute “
dotnet build” followed by “
dotnet run”. The route that will be used for this controller is “
api/Values”. To view the output from this controller, add
/api/values to the localhost address in your browser. The following will be what the API looks like in a browser:
To enforce authentication, add the
[Authorize] annotation to the
ValuesController class. If you run your app again and refresh the page, you will see the following:
Obviously, a login dialog is of no use to API services because we need to find a way to send our credentials through API calls by passing tokens rather than using a login form. It is at this juncture that we decide to use the
OpenIdDict framework.
Adding OpenIdDict to the web application project
1) Create a file in the main solution directory named
NuGet.Config and add to it the following XML markup:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="aspnet-contrib" value="https://www.myget.org/F/aspnet-contrib/api/v3/index.json" />
</packageSources>
</configuration>
Contents of your root solution directory will look like this:
The
NuGet.Config file is needed because
OpenIdDict is not yet an official Nuget release at the time of writing this article.
2) Update your
.csproj project file with the following dependencies:
<!-- OpenIdDict -->
<PackageReference Include="AspNet.Security.OAuth.Validation" Version="1.0.0-*" />
<PackageReference Include="OpenIddict" Version="1.0.0-*" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="1.0.0-*" />
<PackageReference Include="OpenIddict.Mvc" Version="1.0.0-*" />
3) In the
Startup.cs file, make the following changes at the top of
ConfigureServices() method:
Replace this:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
With this:
services.AddDbContext<ApplicationDbContext>(options =>{
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"));
options.UseOpenIddict();
});
4) Also in the
Startup.cs file, add the following code to the
ConfigureServices() method right before services
AddMvc():
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
});
services.AddOpenIddict(options =>
{
// Register the Entity Framework stores.
options.AddEntityFrameworkCoreStores<ApplicationDbContext>();
// Register the ASP.NET Core MVC binder used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
options.AddMvcBinders();
// Enable the token endpoint.
options.EnableTokenEndpoint("/connect/token");
// Enable the password flow.
options.AllowPasswordFlow();
// During development, you can disable the HTTPS requirement.
options.DisableHttpsRequirement();
});
The above code sets up an endpoint for login through token authentication. A controller will be added later that provides an endpoint at
/connect/token.
Build your app to ensure there are no syntax or compiler errors.
5) Add the following code to the
Configure() method in
Startup.cs between
app.UseIdentity() and
app.UseMvc():
app.UseOAuthValidation();
app.UseOpenIddict();
This adds
OpenIddict and the
OAuth2 token validation middleware to the ASP.NET Core pipeline.
6) In the
Startup.cs file, make the following changes in the
Configure() method:
Replace this:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
With this:
app.UseMvcWithDefaultRoute();
7) You need to add an authorization controller to provide the endpoint
/connect/token. Copy the
authorization controller from the OpenIdDict project on GitHub and add it to the Controllers folder. Alternatively, you can instead use the code below for the
AuthorizationController class:
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server;
using AspToken.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using OpenIddict.Core;
namespace AspToken.Controllers
{
public class AuthorizationController : Controller {
private readonly IOptions<IdentityOptions> _identityOptions;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
public AuthorizationController(
IOptions<IdentityOptions> identityOptions,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager) {
_identityOptions = identityOptions;
_signInManager = signInManager;
_userManager = userManager;
}
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request) {
Debug.Assert(request.IsTokenRequest(),
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
if (request.IsPasswordGrantType()) {
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null) {
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the user is allowed to sign in.
if (!await _signInManager.CanSignInAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Reject the token request if two-factor authentication has been enabled by the user.
if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Ensure the user is not already locked out.
if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the password is valid.
if (!await _userManager.CheckPasswordAsync(user, request.Password)) {
if (_userManager.SupportsUserLockout) {
await _userManager.AccessFailedAsync(user);
}
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
if (_userManager.SupportsUserLockout) {
await _userManager.ResetAccessFailedCountAsync(user);
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
} else if (request.IsRefreshTokenGrantType()) {
// Retrieve the claims principal stored in the refresh token.
var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(
OpenIdConnectServerDefaults.AuthenticationScheme);
// Retrieve the user profile corresponding to the refresh token.
// Note: if you want to automatically invalidate the refresh token
// when the user password/roles change, use the following line instead:
// var user = _signInManager.ValidateSecurityStampAsync(info.Principal);
var user = await _userManager.GetUserAsync(info.Principal);
if (user == null) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The refresh token is no longer valid."
});
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The user is no longer allowed to sign in."
});
}
// Create a new authentication ticket, but reuse the properties stored
// in the refresh token, including the scopes originally granted.
var ticket = await CreateTicketAsync(request, user, info.Properties);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}
private async Task<AuthenticationTicket> CreateTicketAsync(
OpenIdConnectRequest request, ApplicationUser user,
AuthenticationProperties properties = null) {
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(principal, properties,
OpenIdConnectServerDefaults.AuthenticationScheme);
if (!request.IsRefreshTokenGrantType()) {
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted
// to allow OpenIddict to return a refresh token.
ticket.SetScopes(new[] {
OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess,
OpenIddictConstants.Scopes.Roles
}.Intersect(request.GetScopes()));
}
ticket.SetResources("resource_server");
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in ticket.Principal.Claims) {
// Never include the security stamp in the access and identity tokens, as it's a secret value.
if (claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType) {
continue;
}
var destinations = new List<string> {
OpenIdConnectConstants.Destinations.AccessToken
};
// Only add the iterated claim to the id_token if the corresponding scope was granted to the client application.
// The other claims will only be added to the access_token, which is encrypted when using the default format.
if ((claim.Type == OpenIdConnectConstants.Claims.Name && ticket.HasScope(OpenIdConnectConstants.Scopes.Profile)) ||
(claim.Type == OpenIdConnectConstants.Claims.Email && ticket.HasScope(OpenIdConnectConstants.Scopes.Email)) ||
(claim.Type == OpenIdConnectConstants.Claims.Role && ticket.HasScope(OpenIddictConstants.Claims.Roles)))
{
destinations.Add(OpenIdConnectConstants.Destinations.IdentityToken);
}
claim.SetDestinations(destinations);
}
return ticket;
}
}
}
Make sure your application builds without any errors.
Testing authorization with postman
Before proceeding with testing token authentication, make sure you already created a user with email and password.
There is a handy Google Chrome extension called “
postman” that you can install from the Chrome Web Store. The icon of the extension I am referring to looks like this:
In Postman, do the following:
1) Select
POST for the HTTP method.
2) Enter the Web API login endpoint. In the above example, the login endpoint is
http://localhost:5000/connect/token/. In your case, you may have to only correct the port number.
3) Click on
Body.
4) Click on
x-www.form-urlencoded.
5) In entered the following parameter name/value pairs:
username: a@a.a
password: P@$$w0rd
grant_type: password
6) Finally, click on the
Send button to initiate the request. The response from the server will look like this:
{
"resource": "resource_server",
"token_type": "Bearer",
"access_token": "CfDJ8AJFDskine9Jhavjj9lbv4Rr7pEocsejCXl24utNyr58SbYfZ6-7gDEnZE6CtTGkGIo7YI6HbTVNfxBpKRGtJsZS--RcukRJ9r5da1kVzWHa_Lx0E1om5LMlxBuq_t-OTuyXi5izZMdGVYLi9ldcyweP1nowKm-xFnq87TzbZtkEfHhTGxaoCA2UP7prT9kpNFHR--svvgk14U6R_uMkuE7alUteTwiFWE9PgzbcwEygZWwAk4AN07QrnBbK565z96TMaoxx2giJakJefs40q8uw8aSlHcNJv7xLmeLoI8sDCYoyqjPeSdyn-cv2g_9ZEhL1JnaUeXx8pl5YuIFH2nJwum1hwSemtARx14cgm0Bb4hFQORtKwNInKRDn8PuFYjibK66JRDUb33a1408JJrJsnmN6nzatWSe4Z8tBpi1mZ58Vht6qooT8gPW_shQleZh_qrmmZdcmXRfyJ-uULnS1kD2ml6h-9v_hydrP1a5dwpT4DPus58Gas3gZJAWPjY-llSYBHaw9X2q0_4pH-lX4SvlRkCFVdfoL37-VYCxW",
"expires_in": 3600
}
In order to access our ValuesController, we will need to pass the access_token.
You can now add the access_token to your “Authorization” header, with the value prefixed by “Bearer “. Example:
Authorization: Bearer CfDJ8BNb0JA0Y9lNgKzWkKXsTwf-4RPIpzDaVFES95ETNNIx-q_Qn2hW2PA5he39PPGHPuSCPadHaOeLWYK5hlJe-ZBLZjojcwjZYJTUNP6uhgG3CikaN-ES1tadOyJmEmaOjLe2QeY09AaziWP3SB7quEIPpFOeKyKMieHuOBVx_-R51XyUyVLRMfI7fWEKeEt35PbXMPYOochUybSLaxrNkQL9x1Wuj0l2igspn4hnpmvG76a_VEuRSxyDfZy6VsukPhxfzhlcrd_bvIchg5uMGEiJmbOvbRbIR5c_6Wya2uD6s5yMk8NYyAE6cebLMU_4sZ0pA2Qkzlilun_5sD7GsdlpE9pbzwv3I6liAI0twnsCoB06e4KD5bJUYCTIdTu-qQ2-GTdSlxk3iiCvHNl72R9t3hRvU8VuKld2g4Wc1PWsKzL1IkFbNIG_2CoHWmaAv09OnnpD3rrE-E6BA4gCkvRTUflf6Dc_yvN03InTs_SNOPyybjtVI_MG5o2CKfRClU9ERvFQYHBeqhZlnt2cLm2SBOx0wbexwBYF0nwWjjxLbWBndkodLqVEMtIOrbptRaacJa2MrpfIMbOvax_xvogqPzwZ8vu97BU4IvgTvpGw52HDLj9UZgN4TC_7ZDFd41ZQla7qvBN2Z4o3NjY-sdFJ6K7uXkx0CI9KFivhG_dpSB4VXoXer5ndDc5sExm1iGhjUe5qLJYnf37cm_a_ov7nYSFnkKeWuLgX0mzc2XQOzztFM8UXSfJmdftEBoD9kz_nNQpSWewkhnVQzDRGFl-UhCbkgM7Z78fYIqylT39-
Back in postman, do the following:
1) Select
GET for the HTTP method.
2) Enter the ValuesController endpoint. In the above example, the login endpoint is
http://localhost:5000/api/values/. Again, you may have to only correct the port number.
3) Click on
Headers.
4) Enter
Authorization for the header parameter.
5) For the
Authorization header parameter value, enter the access_token prefixed by the word
Bearer followed by a space.
6) Click on the
Send button. The response should be the expected Web API output from the
ValuesController as follows:
You should now be able to secure your Web API data in ASP.NET Core 1.1 so that only those who are authenticated can view the data.
References:
https://github.com/openiddict/openiddict-core
http://overengineer.net/Using-OpenIddict-to-easily-add-token-authentication-to-your-.NET-web-apps
http://kerryritter.com/authorizing-your-net-core-mvc6-api-requests-with-openiddict-and-identity/
http://stackoverflow.com/questions/41122686/the-type-or-namespace-name-openiddictdbcontext-could-not-be-found
https://github.com/openiddict/openiddict-samples/tree/master/samples/RefreshFlow