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:
- Download an install .NET Core version 1.1 from https://www.microsoft.com/net/download/core
- Download and install Visual Studio Code from https://www.visualstudio.com/ [OPTIONAL]
- Download and install Node.js & NPM from https://nodejs.org/
- Download and install Bower with: npm install bower -g
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 AspTokenUse the following command to create an app with individual authentication and SQLite:
cd AspToken
dotnet new mvc --auth Individual --framework netcoreapp1.1
In order to run the application, execute the following commands in sequence:dotnet restorePoint your browser to http://localhost:5000 and you will see a page that looks like this:
dotnet ef database update
bower install bootstrap
dotnet run
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;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:
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) {
}
}
}
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 =>With this:
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
services.AddDbContext<ApplicationDbContext>(options =>{4) Also in the Startup.cs file, add the following code to the ConfigureServices() method right before services AddMvc():
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"));
options.UseOpenIddict();
});
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.
// 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();
});
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();This adds OpenIddict and the OAuth2 token validation middleware to the ASP.NET Core pipeline.
app.UseOpenIddict();
6) In the Startup.cs file, make the following changes in the Configure() method:
Replace this:
app.UseMvc(routes =>With this:
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
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.a6) Finally, click on the Send button to initiate the request. The response from the server will look like this:
password: P@$$w0rd
grant_type: password
{
"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-corehttp://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
No comments:
Post a Comment