Navigate to the connect/authorize method in token server with clientid and few other parameters. It shows error message as
Let's see what is the meaning of this error message is.
method. that method is automatically get created.
In token server clients, we have specified only clientid and name, We haven't configured a grant type for our client,
Grant type means, how a client wants to interact with identityserver. We are using OpenIDConnect to talk with token server,
Scopes says what client can acces, Eventhough we have passed client scopes, we haven't mentioned token server side scopes.
scopes are modeled as resources in identiy server. Identity server has two different type of scopes.
Identty resource allows to model a scope that returns bunch of claims, claims means protected values.
Let's add Identity resources and API resources into our application.
are standard openId connect scopes aded referring to Identity server. These values will be returned after authentication.
Next we added a custom identity resource as role that returns role claims for authenticated user.
Run the application, when hit on contact link, it moves into
connect/authorize method. In response it states 302 - Found, and redirects to
/account/login as mentioned in location property of response header.
When we click on login method entry in console, it shows as below. It redirects to /account/login method. In token server, login method is not yet implemented. Let's implement it.
4.4 Implement Login method in Account Controller
Add AccountController into Controllers folder,
Let's implement Login method in Account controller, Login method requires returnUrl as a parameter,
[HttpGet]
public IActionResult Login (string returnUrl)
{
return View();
}
Create Account folder in Views folder and add Login view into Views folder.
We need to create a form to login, add username and password fields in view.
Add LoginViewModel class in ViewModels folder and define properties in it as below.
public class LoginViewModel
{
[Required]
public string UserName { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
public string ReturnUrl { get; set; }
}
Go to Login view, and add username and passwords fields along with remember me option, In asp.net form post action is configured to be Login method.
We have to add Login action method in Account controller with LoginViewModel
@model tokenserver.ViewModels.LoginViewModel
<div class="row">
<div class="col-md-8">
<section>
<form asp-controller="Account" asp-action="Login"
asp-route-returnurl="@ViewData["ReturnUrl"]" method="post"
class="form-horizontal">
<h4>Use a local account to log in.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<input type="hidden" asp-for="ReturnUrl" />
<div class="form-group">
<label asp-for="UserName" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="UserName" class="form-control" />
<span asp-validation-for="UserName" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Password" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<div class="checkbox">
<label asp-for="RememberMe">
<input asp-for="RememberMe" />
@Html.DisplayNameFor(m => m.RememberMe)
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<button type="submit" class="btn btn-default">Log in</button>
</div>
</div>
<p>
<a asp-action="Register"
asp-route-returnurl="@ViewData["ReturnUrl"]">
Register as a new user?</a>
</p>
<p>
<a asp-action="ForgotPassword">Forgot your password?</a>
</p>
</form>
</section>
</div>
</div>
Run the application, It shows Login screen in the browser.
4.5 Attach Users into the token server
Let's add users to login into the system, Create a user class and add Username and password fields. InMemoryUser class is implemented in IdentityServer4.Services.InMemory, this is not supported in IdentityServer4 1.1.0 , It's supported in 1.0.0-rc5. Let's add that into project.json file.
internal class Users
{
public static List<InMemoryUser> GetUsers ()
{
return new List<InMemoryUser>
{
new InMemoryUser
{
Username = "hansamali",
Password = "hansamali"
}
};
}
}
Let's attach users into identity server,
services.AddIdentityServer()
.AddInMemoryClients(Clients.GetClients())
.AddInMemoryApiResources(Rsources.GetApiResources())
.AddInMemoryIdentityResources(Rsources.GetIdentityResources())
.AddInMemoryUsers(Users.GetUsers());
4.6 Implement User Login POST method in Account Controller
Create Login Post method in Account controller,
In this method, we are using IdentityUser in UserManager Store which used to manage persistance in an appalication.
Create UserManager instance, add required reference as
Microsoft.AspNetCore.Mvc
UserManager instance needs to return IdentityUser,
Add required references as
Microsoft.AspNetCore.Identity.EntityFrameworkCore
initialize userManager instance in AccountController constructor.
private UserManager _userManager;
public AccountController(UserManager userManager)
{
_userManager = userManager;
}
In
UserManager instance, call
FindByNameAsync method, It's asynchronous function, we need to use await before it. so we have to change method signature to async, Since async method return type shoud be void or Task<IHttpActionResult> change return type accordingly.
Retrieve identity User by passing user name, and check user password is valid by passing password along with user.
Create a
AuthenticationProperties instance and add necessary reference for it,
Microsoft.AspNetCore.Http.Authentication.
Set Authentication properties to store authentication details across multiple requests with
IsPersistance true property.
call
SignInAsync method with identity user id and username, We need to ensure return url is valid to redirect to authorized endpoint, to check validation we have to create a instance from
IIdentityServerInteractionService instance and initialize it.
private UserManager<IdentityUser> _userManager;
private IIdentityServerInteractionService _interaction;
public AccountController(UserManager<IdentityUser> userManager, IIdentityServerInteractionService interaction)
{
_userManager = userManager;
_interaction = interaction;
}
[HttpPost]
public async Task<IActionResult> Login (LoginViewModel loginViewModel)
{
if(ModelState.IsValid)
{
var identityUser = await _userManager.FindByNameAsync(loginViewModel.UserName);
if(identityUser != null
&& await _userManager.CheckPasswordAsync(identityUser, loginViewModel.Password))
{
AuthenticationProperties properties = null;
if(loginViewModel.RememberMe)
{
properties = new AuthenticationProperties
{
IsPersistent = true
};
}
await HttpContext.Authentication.SignInAsync(identityUser.Id, identityUser.UserName);
if (_interaction.IsValidReturnUrl(loginViewModel.ReturnUrl))
return Redirect(loginViewModel.ReturnUrl);
return Redirect("~/");
}
ModelState.AddModelError("", "Invalid username or password.");
}
return View(loginViewModel);
}
Run the application and provide user credintials and hit on login button and this error comes up. It says unable to resolve service for type Usermanager
4.7 Create Database
Add identity configuration service into ConfigureService method. Add
Microsoft.AspNetCore.Identity.EntityFrameworkCore reference to resolve reference errors.
services.AddIdentity<IdentityUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>();
Enable ASP.NET identity to request pipeline in Configure mthod just above identityserver() method
app.UseIdentity();
We have added entityframework type of store to save identity information. Let's add database context and connection string to the store. Add missing reference as
Microsoft.EntityFrameworkCore
string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;database=demotokenserver;trusted_connection=yes;";
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
We have to create
ApplicationDbContext class to hold db objects, let's add it. Inherit Application Db Context class from Identity Db Context and call base class method in constructor.
internal class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext (DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
}
Startup method implementation as follows,
Run the application and try to login to the system, It says following error It says application cant open the database. We haven't created the database yet. Lets try to created it.
4.8 Add Database Migrations
EFCore supports to implements
clients, scpopes and persistant grant store using relational database provider, because of that in each and every deploy, database will not get created. since we are using a datase to persist.
Create a seperate method in startup class to define database contexts
Persisted Grant Db Context is going to define all grant types application use.
Migrate and apply changes in Persisted Grant Db Context, Add
IdentityServer4.EntityFramework.DbContexts and using
System.Reflection
to provide context related information
scope.ServiceProvider.GetRequiredService().Database.Migrate();
Comment out all inmemory data management and add context for persisted grants.
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityServer()
//.AddInMemoryClients(Clients.GetClients())
//.AddInMemoryApiResources(Rsources.GetApiResources())
//.AddInMemoryIdentityResources(Rsources.GetIdentityResources())
//.AddInMemoryUsers(Users.GetUsers());
.AddOperationalStore(store => store.UseSqlServer(connectionString,
options => options.MigrationsAssembly(migrationsAssembly)));
Go to
Persisted Grant Db Context and view tables available in it. Grant types will be stored in here,
Let's run migrations on
Persisted Grant Db Context, Go to cmd and go to solution path and type
dotnet ef It says no executable find for dotnet-ef
Add
"Microsoft.EntityFrameworkCore.Tools": "1.1.0-preview4-final" in dependencies section in project.json file
Add
"Microsoft.EntityFrameworkCore.Tools.DotNet": "1.1.0-preview4-final"
in tools section since we are using dotnet - ef tools
run
dotnet ef and verify we can run ef migrations from cmd
run
dotnet ef migrations add InitialIdentityServerMigration -c
PersistedGrantDbContext command to migrate Persisted grant db context,
Check solution folder Migrations folder is created
Open
InitialIdentityServerMigration class and see what's there, Persisted Grants table is getting created from Persisted grant db context.
Before runing the application call databse context cration methods in configure method,
private static void InitializeDbTestData(IApplicationBuilder app)
{
using (var scope = app.ApplicationServices
.GetService<IServiceScopeFactory>().CreateScope())
{
scope.ServiceProvider
.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();
}
}
Let's run the application and see what happens, Persisted Grants table is created,
When we run the application, it gives an error, We haven't attached clients from our db context, Only grant types were created, Let's create clients as well
Add
Configuration Db context into application scope
scope.ServiceProvider.GetRequiredService<ConfigurationDbContext>().Database.Migrate();
Attach configuration db context in startup method.
services.AddIdentityServer()
//.AddInMemoryClients(Clients.GetClients())
//.AddInMemoryApiResources(Rsources.GetApiResources())
//.AddInMemoryIdentityResources(Rsources.GetIdentityResources())
//.AddInMemoryUsers(Users.GetUsers());
.AddOperationalStore(store => store.UseSqlServer(connectionString, options => options.MigrationsAssembly(migrationsAssembly)))
.AddConfigurationStore(store => store.UseSqlServer(connectionString, options => options.MigrationsAssembly(migrationsAssembly)));
Run run
dotnet ef migrations add InitialIdentityServerMigration -c ConfigurationDbContext to create Configuration database context.
Open
InitialIdentityServerMigration class for configurations and check what are the available tables,
Api resources, Clients, Identity resources etc.
Run the application to see changes, Token server runs without any issue,
Check the console window, API resources, Identity Resources and Clients tables are getting created.
View
SQL Server object explorer and database is created, Available tables are seen like this. but Users table is not there
Let's add required services to create users table.
scope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
Run
dotnet ef migrations add InitialIdentityServerMigration -c
ApplicationDbContext command to create Users
Open
InitialIdentityServerMigration for application db context, It shows
users,
roles etc
Run the application and see what happens, ASpnet users , roles are created, Refresh the database in
SQL server object explorer
Let's add some data into these tables,
Get instance from configuration database context and add
Clients,
API resources and
Identity resources as follows.
var context = scope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
if (!context.Clients.Any())
{
foreach (var client in Clients.GetClients())
{
context.Clients.Add(client.ToEntity());
}
context.SaveChanges();
}
if (!context.IdentityResources.Any())
{
foreach (var resource in Rsources.GetIdentityResources())
{
context.IdentityResources.Add(resource.ToEntity());
}
context.SaveChanges();
}
if (!context.ApiResources.Any())
{
foreach (var resource in Rsources.GetApiResources())
{
context.ApiResources.Add(resource.ToEntity());
}
context.SaveChanges();
}
Run the application, and data is inserted into the tables, Clients are inserted in clients table, In SQL server object explorer, right click on the clients table and select view data.
Add users into the application,
var usermanager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
if (!usermanager.Users.Any())
{
foreach (var inMemoryUser in Users.GetUsers())
{
var identityUser = new IdentityUser(inMemoryUser.Username)
{
Id = inMemoryUser.Subject
};
foreach (var claim in inMemoryUser.Claims)
{
identityUser.Claims.Add(new IdentityUserClaim<string>
{
UserId = identityUser.Id,
ClaimType = claim.Type,
ClaimValue = claim.Value,
});
}
usermanager.CreateAsync(identityUser, "Password123!").Wait();
}
}
}
4.9 Add Consent View
Run the application, and see what happens, After successfully login to the application it shows an error in consent method.
Consent controller is not defined by token server, let's add it
Add ConsentController into controllers folder. create Consent folder in Views folder and create index view in consent folder.
Go to consent index view and add this line of code.
Run the client and Identity server application. It shows consent view after a successful login. We have authorized contact page with identity server 4.
5 Demo : Create Client application - Service/API application
5.1 Connect to a client using client credentials
We can create a api/service application, when designing security for a web api, for every api request, we should check request is from an authenticated client or not. in each and every request, it passes an access token and validate the access token.
Create a new client with client credential grant types
new Client
{
ClientName = "client",
ClientId = "client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedScopes = { "customAPI.read"}
}
Change data records insert method as follows,
foreach (var client in Clients.GetClients())
{
if(context.Clients.FirstOrDefault(c => c.ClientId == client.ClientId) == null)
context.Clients.Add(client.ToEntity());
}
context.SaveChanges();
Go to Startup class and modify configure services method to return temperory sign in credintilas.
services.AddIdentityServer()
//.AddInMemoryClients(Clients.GetClients())
//.AddInMemoryApiResources(Rsources.GetApiResources())
//.AddInMemoryIdentityResources(Rsources.GetIdentityResources())
//.AddInMemoryUsers(Users.GetUsers())
.AddOperationalStore(store => store.UseSqlServer(connectionString, options => options.MigrationsAssembly(migrationsAssembly)))
.AddConfigurationStore(store => store.UseSqlServer(connectionString, options => options.MigrationsAssembly(migrationsAssembly)))
.AddTemporarySigningCredential();
Run the token server and try to connect to the token server using postman, Go to
http://localhost:5000/connect/token, Add parameters as below.
- grant_type as client_credentials
- scope as customAPI.read
- client_id as client
- client_secret as secret
- username as hansamali
- password as 123456
It sends us an access token, We can use this token to call service methods/ api methods.
Go to a JWT token decoder and check what is available in JSON web token,