Introduction
This article is a short and easy walk-through that will explain how to build an OAuth2 Authorization Server using the Identity Server open source middleware and hosting it inside a .NET Core Web Server.
The authors of the Identity Server project already did a great job providing an amazing documentation and many clear & useful quickstart examples. Sometimes, however, it is a bit complicated to understand how the author of the example got there. And often, rebuilding the same example from scratch helps a lot to understand the technology that we are trying to learn.
This article comes from these considerations. The idea is to share with you my experience while learning this subject, hoping that it can be of some value for other developers as well.
Background
To understand what this article is about, you might want to learn more about:
Let's Create Our Authorization Server
In the next section, I'm going to explain the code (almost) step-by-step. And because I know that none of us likes to read too much, I organized every section with clear paragraph titles, so you can just scroll it all and find the part that might be more interesting for you. Let's go!
Create an Initial WebAPI Project in Visual Studio 2017
The entire example is currently just for VS2017, built using .NET Core 1.1. Here's how to start:
- Open VS2017 and create a new project, choosing a VisualC# -> Web -> ASP.NET Core Web Application (.NET Core). Name it
AuthorizationServer
. Press OK. - Now choose the type WebApi project. Select ASP.NET Core 1.1. Press OK again. The project is created.
Add the NugetPackage IdentityServer4 to the WebAPI Server
- Browse the Package Manager and install the package
IdentityServer4
. I installed the version 1.5.0, latest stable at the time I'm writing.
Add a Welcome Page to the WebAPI Server
To make your authorization server reachable with a browser, and for you to easily understand if the server is up and running, you can add a basic controller and a welcome page. I will detail the steps I followed, for those that are not familiar with the ASP.NET MVC framework:
Add a MVC Controller
- In the folder Controllers, add a new controller named
HomeController
. - Adding the new controller, VS will ask what dependencies to add to the project. You can choose Minimal Dependencies for now.
- At the time I'm writing, after adding the dependencies, I need to add the controller again. I think it is a little bug of the UI....
- Now, adding the controller, VS asks which scaffold to use. Choose MVC Controller - Empty.
- Finally, choose the name
HomeController
. A basic controller that can manage an Index.cshtml page is now created.
Add a MVC View
- Again, in VS2017, right click on the project and select Add new folder.
- Name it Views (don't change it! It's a default option for ASP.NET MVC!).
- Now, inside this Views folder, add another folder named Home.
- Right click now on the folder Home and select Add -> New item -> MVC View Page (ASP.NET Core).
- By default, the name of the view is Index.cshtml which is what we want. Press the button Add to add the new view.
Write the HTML Content of the View
Change the code in the Index.cshtml with the code below, just to create a welcome message:
<html>
<head>
<title>Authorization server. Welcome page.</title>
</head>
<body>
<h1>Authorization server. Welcome page.</h1>
</body>
</html>
Configure the Startup Class
Setup the proper route in the Startup.cs to be able to browse the website:
public void Configure(IApplicationBuilder app,
IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Now browse the file in Properties -> LaunchSettings.json and remove the line containing the property launchUrl
from every section visible in the file.
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"AuthorizationServer": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:50011"
}
Without this property, Visual Studio will launch the home page following the default path, and will point automatically to the web page that we just created.
Launch the Project to Check That It Works...
...and to enjoy the achievement of a first step! Warning: some people reported some issues in VS2017 related to launching a WebAPI project using IISExpress. In case you are one of those, you can change the profile next to the button "Start project" switching from IISExpress to AuthorizationServer.
Configure Sample Scopes and Clients for the Client Credential Workflow
Now we can configure the most important elements of our Authorization Server: clientIds
, clientSecrets
, scopes
.
These three elements are some of the basics for the Client Credential workflow. Don't forget to refer to the OAuth2 Client Registration documentation for more information!
These properties will define WHO can request an access token to your Authorization Server and WHAT can be done with that token.
To activate our initial sample configuration, just create a Config.cs class in our project, that looks like this:
using IdentityServer4.Models;
using System.Collections.Generic;
namespace AuthorizationServer
{
public class Config
{
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("scope.readaccess", "Example API"),
new ApiResource("scope.fullaccess", "Example API"),
new ApiResource("YouCanActuallyDefineTheScopesAsYouLike", "Example API")
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "ClientIdThatCanOnlyRead",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret1".Sha256())
},
AllowedScopes = { "scope.readaccess" }
},
new Client
{
ClientId = "ClientIdWithFullAccess",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret2".Sha256())
},
AllowedScopes = { "scope.fullaccess" }
}
};
}
}
}
Spend a Moment to Understand this Class...
Take a moment to understand how the configuration work in this class. This is the key part of our example.
We are firstly defining three scopes here:
return new List<ApiResource>
{
new ApiResource("scope.readaccess", "Example API"),
new ApiResource("scope.fullaccess", "Example API"),
new ApiResource("YouCanActuallyDefineTheScopesAsYouLike", "Example API")
};
As you can see, we can use as a scope any string
we like. It's just an identifier, nothing else.
When requested, the AuthorizationServer
will issue a JWT Token to a client, and based on the clientId
, will include the proper scope in the token. Since the scope is encrypted in the token, there is no risk that the client that receives the token can change the scope and enable for himself more rights that we want.
The logic to actually use this scope will be in the Web API Server that we will create later (I'm planning to do it soon in another example / article) and will protect using this authorization server. The Web API Server, before DOING real stuff will check that the scope passed from the client contains the right authorization. This is the point where we are leveraging our Authorization Server.
Which ClientId
can request a token, and which scope does it get? This is what is defined in the second part of the configuration class:
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "ClientIdThatCanOnlyRead",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret1".Sha256())
},
AllowedScopes = { "scope.readaccess" }
},
new Client
{
ClientId = "ClientIdWithFullAccess",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret2".Sha256())
},
AllowedScopes = { "scope.fullaccess" }
}
};
}
Enable the IdentityServer Middleware Features
It's time to enable the IdentityServer
features and complete the transformation of our empty web site in a real Authorization Server, giving it the possibility to manage and authenticate the clients that we configured in our Config class above.
To do so, we need just to include a couple of calls to the IdentityServer
objects inside our Startup
class.
Copy and paste the following method in the Startup
class, replacing the old one:
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());
services.AddMvc();
}
The method is "enabling" the IdentityServer
middleware and adding an InMemory
management for our scopes and clients.
This is the key point where we are now using the Config
class created before:
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());
There are many, many more other options that can be configured on identityserver
but are out of scope of this article. Here, we are just creating a quick start. Once again, check out the documentation, the guys there really developed an amazing open source library.
Now, in the method Startup.Configure
, add the line:
app.UseIdentityServer();
At the end, the method should look like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseIdentityServer();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Check Out that Everything Works
We are almost at the end. Our Authorization Server is ready to start!
So, again from VS2017, launch the project. The welcome page shows up to reassure you that the web service is up and running.
Now go to the following address (replace the port number with the port number of your server):
- http://localhost:50151/.well-known/openid-configuration
If everything is working, a JSON file is loaded and shows up in the browser (or you are asked to download it, it depends on your browser). This is the JSON file with all the Configuration information generated from the Identity Server middleware.
Take a look to this JSON file. There is an important part in it, showing that the middleware has correctly understood your configuration. Is the part declaring the scopes that your Authorization Server supports, exactly the ones that you declared in the Config.cs:
"scopes_supported": [
"scope.readaccess",
"scope.fullaccess",
"YouCanActuallyDefineTheScopesAsYouLike",
"offline_access"
],
That's it! ...Well, For Now....
Your first Authorization Server is ready to be used. But, wait...how? A single Authorization Server on its own doesn't help too much if doesn't interact with an API to protect, or with a client to authorize. It needs some friends!
Therefore, in another article, we are going to learn how to protect a Web API server, accepting tokens issued from this Authorization Server. We will learn also how to create a client that can request a token and use it.
Meanwhile, I hope this example can help other people that, like me, would like to start playing a bit with some OAuth2 workflows and the powerful IdentityServer
middleware. Share your feedback and comments!
History
- 2017-04-22: First version
- 2017-04-24: Added a link to download the source code
- 2017-05-07: Fixed a step mentioning a wrong button name