How to Get the Code
Check out the code at https://github.com/icedage/IdentityServer.
In This Article
- Solution Structure
- How to run Solution
- Id Token VS Access Token
- Scopes Registration in the Authorization Server
- Clients Registration in the Authorization Server
- Authorization Flows
- How to implement ASP.NET Identity
- How Authentication works in the MVC Client
- How Authentication works in the WebAPI Client
- How Role-based Authorization Works
- Logs
Solution Structure
The solution is constituted by 4 projects:
Security.AuthorizationServer
Infrastructure of IdentityServer3
. This is where we register the Clients and define the Scopes. Security.IdentityManagementTool IdentityManagement
is an MVC application. It is used to create Users, Roles and assign roles to existing Users. It's using Implicit flow and it's registered in the AuthorizationServer
. Security.WebAPI UserController
presents an endpoint that requires role-based authorization. It's using Resource Owner flow. It's also registered in the AuthorizationServer
. Security.Scenarios
: Two scenarios that you can use to run the WebAPI.
Check the README.md in GitHub to see how you can run the solution and trigger the IdentityServer
.
Assuming you run the solution successfully, to verify that IdentityServer
has been configured, bring up Identity ManagementTool
and click on Roles on the menu.
The Endpoint
that renders the Roles
view has been decorated with the Authorize
attribute. That means, if you haven't logged in yet, the IdentityServer
will ask you to authenticate by redirecting you to the IdentityServer
's login page as shown below. Please note, that you can also customise the default login page:
You will see that you are redirected to another url. That is the IdentityServers
address. The application requests OAuth 2.0 authorization for the OpenID Connect scopes (openId
, profile
, etc.) by redirecting the user to an identity provider.
Create Your First User or Use Existing User
You can test the system using the existing user and role. To view the user and its role, bring up Security.IdentityManagementTool/Migrations/Configuration.cs. Before you use it, you need to execute update-database command for IdentityManagementTool
.
Alternatively, you can create your own users and roles using IdentityManagementTool
.
Click on New User, so that you can use the form to add a user to the system.
Next, use the user's credentials you just created to login. Once the user has been authenticated, IdentityServer
returns user's identity, in the form of an Id_Token
as shown below:
To test role-based authorization, you need to create a Role
. I have used the Admin role, so you would need to create an Admin Role and assign it to the user. To create a role, just click New Role on the menu. Click on Users, then select your user and click Add Roles button on the top of the Gridview
. That should bring a pop-up box, where you can select roles.
Id Token VS Access Token
With OpenID
Connect authentication, there is an additional type of OAuth token: an ID token. The ID token, or id_token
, represents the identity of the user being authenticated. This is a separate token from the access token, which is used to retrieve the user’s profile information or other user data requested during the same authorization flow. It's passed to the Check ID Endpoint for preventing replay attacks. The Check ID Endpoint is used to verify that the credentials issued by the OAuth provider were issued to the correct application.
Scopes Registration in the Authorization Server
Scopes are identifiers for resources that a client wants to access. This identifier is sent to the OP during an authentication or token request.
OpenID Connect Clients use scope values, to specify what access privileges are being requested for Access Tokens. The scopes associated with Access Tokens determine what resources will be available when they are used to access protected resources. Protected Resource endpoints MAY perform different actions and return different information based on the scope values and other parameters used when requesting the presented Access Token.
Scopes can be used to request the specific sets of information that clients need. IdentityServer
offers two kinds of scopes, Identity
and Resourse
scopes.
Identity
is for the set of information that relate to user's identity, like roles and claims.
Resource
scopes identify web APIs (also called resource servers).
Below, I need two kinds of scopes. I need information regarding user's identity (Name="roles"
) and I also need access to the WebAPI (Name = "WebAPI"
).
public static IEnumerable<Scope> Get()
{
var scopes = new List<Scope>
{
new Scope
{
Enabled = true,
Name = "roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
},
new Scope
{
Enabled = true,
DisplayName = "WebAPI",
Name = "WebAPI",
Description = "Secure WebAPI",
Type = ScopeType.Resource,
Claims = new List<ScopeClaim>
{
new ScopeClaim(Constants.ClaimTypes.Name),
new ScopeClaim(Constants.ClaimTypes.Role),
}
}
};
scopes.AddRange(StandardScopes.All);
return scopes;
OpenID Connect Clients use scope values . So, who are the Clients?
Clients Registration in the Authorization Server
"A client is a piece of software that requests tokens from IdentityServer - either for authenticating a user or for accessing a resource (also often called a relying party or RP). A client must be registered with the OP."
In the solution, you will find two Clients that need tokens from IdentityServer
, WebAPI
and IdentityManagementTool
. Both need to be registered as shown below:
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
ClientName = "WebAPI Client",
ClientId = "api",
Flow = Flows.ResourceOwner,
ClientSecrets = new List<Secret>
{
new Secret(SecretApi.Sha256())
},
AllowedScopes = new List<string>
{
"WebAPI"
}
},
new Client
{
Enabled = true,
ClientName = "Identity Management Tool",
ClientId = "IdentityManagementTool",
Flow = Flows.Implicit,
RequireConsent = true,
AllowRememberConsent = true,
RedirectUris = new List<string>
{
"http://localhost:55112/"
},
IdentityTokenLifetime = 360,
AccessTokenLifetime = 3600,
AllowedScopes = new List<string>()
{ "openid", "profile" , "roles", "WebAPI" }
},
};
}
Authorization Flows
As you can see, for each Client
, we need to Specify a Flow.
Each client needs to be associated with an appropriate protocol flow for obtaining authorization from the resource owner for access to their data. The OAuth 2.0 protocol defines four primary “grant types”. I will focus on those I have used in my existing client list above.
Implicit and Resource Owner
Implicit grant for browser-based client-side applications: The implicit grant is the most simplistic of all flows, and is optimized for client side web applications running in a browser. The resource owner grants access to the application, and a new access token is immediately minted and passed back to the application.
Resource owner password-based grant: This grant type enables a resource owner’s username and password to be exchanged for an OAuth access token. While the user’s password is still exposed to the client, it does not need to be stored on the device. After the initial authentication, only the OAuth token needs to be stored. Because the password is not stored, the user can revoke access to the app without changing the password, and the token is scoped to a limited set of data, so this grant type still provides enhanced security over traditional username/password authentication.
How to Implement ASP.NET Identity
We need to save users and roles in SQL Server database. Identity framework is doing exactly that. It provides a rich API for managing users and claims. How do we "tell it" to interact with IdentityServer
, so that AuthorizationServer
issues tokens only for the users stored in our local database? The answer is the UserService
class. For more information, please check this link.
IUserService
interface provides semantics for users to authenticate with local accounts as well as external accounts.
The methods on the user service are broken down into methods that relate to authentication and methods that relate to the user’s profile and issuing claims for tokens.
Whenever the user uses the username and password dialogue, the AuthenticateLocalAsync
is triggered. We can choose to override it, if we are to interact with local storage and use Identity
API.
public override Task AuthenticateLocalAsync(LocalAuthenticationContext context)
{
var con = new IdentityDbContext();
var userStore = new UserStore<IdentityUser>(con);
var userManager = new UserManager<IdentityUser>(userStore);
var user = userManager.Users.SingleOrDefault(x => x.UserName == context.UserName);
if (user != null)
{
context.AuthenticateResult =
new IdentityServer3.Core.Models.AuthenticateResult(user.Id, user.UserName);
}
return Task.FromResult(0);
}
How Authentication Works in the MVC Client
IdentityManagementTool
, which is the MVC client, will need to also register its interaction with the AuthenticationServer
in the Startup.cs. A number of things happen in this class:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44396/identity",
ClientId = "IdentityManagementTool",
Scope = "openid profile roles",
RedirectUri = "http://localhost:55112/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
var id = n.AuthenticationTicket.Identity;
var sub = id.FindFirst(IdentityServer3.Core.Constants.ClaimTypes.Subject);
var roles = id.FindAll(IdentityServer3.Core.Constants.ClaimTypes.Role);
var nid = new ClaimsIdentity(
id.AuthenticationType,
IdentityServer3.Core.Constants.ClaimTypes.Name
, IdentityServer3.Core.Constants.ClaimTypes.Role
);
nid.AddClaim(sub);
nid.AddClaims(roles);
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
return Task.FromResult(0);
}
}
});
IdentityServer
can support both OAuth and OpenID Connect. The current solution is using OpenID Connect. Therefore, the Client needs to specify which protocol we need to add in the OWIN runtime. The app.UseOpenIdConnectAuthentication
lets us do exactly that. We add an instance of OpenID Connect. and we also configure IdentityServer
as an Authority that issues token to our client. The Client , which is the IdentityManagementTool
is identified by a given ClientId
. Remember the Clients.cs in the AutorizationServer
, where we registered all the Client
apps that need access tokens. Each Client
has a uniqueId
. That Id
is the same with the ClientId
that you specify in the ClientId
property. This is how Authorization Server and Client know about each other. Authorization Server knows the Clients that it needs to support, and the Client knows the Authority, that is responsible for issuing tokens.
How Authentication Works in the WebAPI Client
In the section, Scopes Registration in the Authorization Server, I talked about scopes, that have two flavors, Identity
and Resource
. The Service
would need to be registered as Resource
. What about the actual Resource
, the Web API? In its Startup.cs, there are couple of things happening there:
app.UseIdentityServerBearerTokenAuthentication
(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44396/identity",
RequiredScopes = new[] { "WebAPI" },
});
AntiForgeryConfig.UniqueClaimTypeIdentifier =
IdentityServer3.Core.Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
ConfigureAuth(app)
How Role-based Authorization Works
In the Web API, one of the endpoints requires role-based authorization. Meaning, it's not enough to authenticate user, but we need to make sure that the user has access to certain resources. You need to retrieve the access token. You can do that in two different contexts. First, you may need to retrieve the access token from the ClaimsPrinicipal
(Implicit flow) or you may need to retrieve the access token programmatically by hitting connect/token. In the current solution, I have used both ways:
Security.Tests
- We need to "tokenize" the request before we call api/users .You need to retrieve the token by calling connect/token. You need to pass credential information in exchange for token:
public HttpRequestWrapper TokenizeRequest(User user, string clientid)
{
var token = GetToken(user, clientid).Result;
_request.AddHeader("Authorization", $"Bearer {token.AccessToken}");
return this;
}
private async Task<TokenResponse> GetToken(User user, string clientid)
{
var client = new TokenClient(Constants.TokenEndpoint, clientid, SecretApi);
return client.RequestResourceOwnerPasswordAsync
(user.UserName, user.Password, "WebAPI").Result;
}
The TokenEndpoint
is connect/token. So under the hood, it makes a call to the connect/token endpoint. For more information, please read OpenID specifications about Token endpoint:
- http://openid.net/specs/openid-connect-core-1_0.html (3.1.3.1. Token Request)
IdentityManagementTool
- You retrieve the token from ClaimsPrincipal
, which means that you need to authenticate through the MVC client (Implicit flow). In that case, you need to make the following changes:
- First, remember the difference between
id_token
and access token. For Role-based authorization flow, we need user’s profile information or other user data requested. In that case, we need to ask from the IdentityServer
to return access token along with the id_token
. In the SecurityTokenValidated
delegate, we add the following: -
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.IdToken
- We need to expand the scopes of
IdentityManagementlTool
, as we now need to access one more area, the WebAPI. In the beginning of the article, we registered scopes and clients in the AuthorizationServer
. One of the Scopes was the WebAPI. So, we also need to inform IdentityManagementTool
client about this new scope. In the IdentityManagementlTool
's StartUp.cs, add WebAPI to the scope list: -
Scope = "openid profile roles WebAPI",
- We also need to update the Client's
AllowedScopes
in the Authorization server. In the IdentityManagementTool
client in your Clients.cs, please add "WebAPI
": -
AllowedScopes = new List<string>() { "openid", "profile" , "roles", "WebAPI"
Logs
Very important! Maybe I should have mentioned that in the beginning of the article. Many things can go wrong (and they will) during configuration. You need to check the logs and see where things go wrong:
Serilog.Log.Logger =
new LoggerConfiguration().MinimumLevel.Debug()
.WriteTo.RollingFile(pathFormat: @"c:\logs\IdSvrAdmin-{Date}.log")
.CreateLogger()
You should be able to access your logs in your C:\logs\IdSvrAdmin-{Date}.log.
The End
Security is one of the most difficult chapters in Software Engineering. I do not claim expertise, so if you find something that needs correction, please let me know. I have tried to compile a list with things you might find, while trying to configure IdentityServer
. I personally had to read the documentation a number of times, posts, forums, comments , complaints... Additionally, if you find it helpful but you are stuck with something, please drop me a message and I will try to fix it.
Useful Resources