I will divide the post into two parts. One for describing Authorization and Resource server Back-End API and another for AngularJs Client application.
To do so we need to build two applications. One is a REST Web API application which is responsible for generate Token and use this token to grant access to secure resources. Another one is a client application which access the server resources by XHR request with the generated token.
We will take privilege from OWIN middle-ware to generate and authorize the token with CORS support. We also generate JWT which is standard widely used modern Token specification.
You could download the complete source-code from here.
How token base authentication work.
For secured communication between our client (angular app) and resource server (Back-end Web API) , resource server provide a token by using user credential. This Token is signed by a private key. Later on when client app try to access any secured resource by calling an API end-point, he provide the token. Then resource server validate the token and if validation pass then return the success response. Otherwise it returns unauthorized (401) response. This is fully stateless communication.
How resource server validate the provided Jason WebToken.
Json Web Token consist of the part separated by dot(.).
- First part is Token header. This Header hold the value of type of the token and which algorithm it used.
- Second part is payload. This payload hold all the claim informations.
- Third part is signature. Which is encrypted by a private key. This same key will required during validate the token.
During Token validation, resource server decrypt the signature by the above described private key and validate it. If signature is valid, them resource server read the payload where Token expiration time exist. If Token is not expired, resource server grant this time as authorized.
Now it's time to goal through the code-base.
First we will create the Web API application which is responsible for both generation of Token and validation of Token.
Open the visual studio and create a blank Web API project as below image. I am using visual studio version 2017.
During creation of the new project, make sure you choose no authentication mode. We will build our own authorization implementation. We need an user model and user store to hold the user information. To generate Token first we need to check the user credential and then generate the Token. To keep the post simple, we will use in memory user store to store the user information. For production like application you must follow the best practice to store the user information. We will build our applications on top of Microsoft OWIN middle ware.
Now it's time to explain to the responsibility of each code block.
Steps to prepare Authorization server:
Step 1:
Create an empty Web API project by using Visual Studio IDE. Make sure no authentication mode selected. We will create our own Authentication mode.
Step 2:
Install below nuget packages to the project by using Package manager console window
- Install-Package Microsoft.Owin.Cors -v 3.1.0
- Install-Package Microsoft.Owin -v 3.1.0
- Install-Package Microsoft.Owin.Host.SystemWeb -v 3.1.0
- Install-Package Microsoft.Owin.Security -v 3.1.0
- Install-Package Install-Package Microsoft.Owin.Security.OAuth -v 3.1.0
- Install-Package Newtonsoft.Json -v 9.0.1
- Install-Package System.IdentityModel.Tokens.Jwt -v 5.1.5
- Install-Package Microsoft.AspNet.WebApi.Core -v 5.2.3
- Install-Package Microsoft.AspNet.WebApi.Owin -v 5.2.3
Step 3:
Create Startup class. This OWIN based Web API will be boot by Startup class. So we'll put some Web API configuration in this class. public class Startup { public void Configuration(IAppBuilder app) { HttpConfiguration config = new HttpConfiguration(); // Web API routes config.MapHttpAttributeRoutes(); ConfigureOAuth(app); app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll); app.UseWebApi(config); } public void ConfigureOAuth(IAppBuilder app) { OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions() { //We enable http only for Dev enviroment. Otherwise we will set it to false. AllowInsecureHttp = true, TokenEndpointPath = new PathString("/security/token"), AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(10), Provider = new CustomOAuthServerProvider(), AccessTokenFormat = new JsonWebTokenDataFormat() }; // OAuth 2.0 Bearer Access Token Generation app.UseOAuthAuthorizationServer(OAuthServerOptions); app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions { AccessTokenFormat = new JsonWebTokenDataFormat() }); } }
Here we configure our Authorization server settings, Endpoint for token, Authorization provider(which is responsible for validation client credential and grant access token), Custom Access token format(which is responsible for create JWT instead of default DPAPI format token).
Our token generation endpoint will be "http://yourdomain.com/security/token"
Step 4:
Create folder Entities and add class Audience.cs to this folder. This object holds the audience information.
public class Audience
{
[Key]
[MaxLength(32)]
public string ClientId { get; set; }
[MaxLength(100)]
[Required]
public string Base64Secret { get; set; }
[MaxLength(500)]
[Required]
public string Name { get; set; }
}
Add class User.cs under the same folder. This user object hold the user information.
public class User
{
[Key]
public Guid Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string Name { get; set; }
}
Add Client.cs class to the same folder. This Client object holds the client information.
public class Client
{
public string Id { get; set; }
public string Name { get; set; }
public string IndustryType { get; set; }
public string RegionalCountry { get; set; }
public string Address { get; set; }
public int ProjectCount { get; set; }
public string LogoLocation { get; set; }
public static List<Client> Clients => new List<Client>
{
new Client{ Id=Guid.NewGuid().ToString() , Name="Microsoft", RegionalCountry="Bangladesh", Address="Dhaka", ProjectCount=20},
new Client{ Id=Guid.NewGuid().ToString(), Name="Dell", RegionalCountry="Bangladesh", Address="Dhaka", ProjectCount=10},
new Client{ Id=Guid.NewGuid().ToString(), Name="Nestle", RegionalCountry="Bangladesh", Address="Dhaka", ProjectCount=5},
new Client{ Id=Guid.NewGuid().ToString(), Name="Walmart", RegionalCountry="India", Address="Dhaka", ProjectCount=8},
new Client{ Id=Guid.NewGuid().ToString(), Name="MetLife", RegionalCountry="Bangladesh", Address="Dhaka", ProjectCount=3}
};
}
Step 5:
Create a folder InMemoryDataStores and add class AudienceStore.cs to that folder. To keep this article simple, we will use in memory storage to store required data we needed. AudienceStore store the audience data.
public static class AudiencesStore
{
public static ConcurrentDictionary<string, Audience> AudiencesList = new ConcurrentDictionary<string, Audience>();
static AudiencesStore()
{
var clientId = ConfigurationManager.AppSettings.Get("client.id");
AudiencesList.TryAdd(clientId,
new Audience
{
ClientId = clientId,
Base64Secret = ConfigurationManager.AppSettings.Get("secretkey"),
Name = ConfigurationManager.AppSettings.Get("issuer")
});
}
public static Audience AddAudience(string name)
{
var clientId = Guid.NewGuid().ToString("N");
var key = new byte[32];
RNGCryptoServiceProvider.Create().GetBytes(key);
var base64Secret = TextEncodings.Base64Url.Encode(key);
Audience newAudience = new Audience { ClientId = clientId, Base64Secret = base64Secret, Name = name };
AudiencesList.TryAdd(clientId, newAudience);
return newAudience;
}
public static Audience FindAudience(string clientId)
{
Audience audience = null;
if (AudiencesList.TryGetValue(clientId, out audience))
{
return audience;
}
return null;
}
}
Add class UserStore.cs to the same folder. This will responsible to store user list. Again this is in memory volatile storage which will be clear each time IIS reset.
public static class UserStore
{
public static List<User> UsersList = new List<User>();
static UserStore()
{
var userId = Guid.NewGuid();
UsersList.Add(new User
{
Id= userId,
Email = "user01@m.com",
Password = "1!2@3#",
Name = "user01"
});
}
public static User AddUser(string name, string email, string password)
{
var userId = Guid.NewGuid();
User newUser = new User { Id = userId, Email = email, Password=password, Name = name };
UsersList.Add(newUser);
return newUser;
}
public static bool FindUser(string userName, string password)
{
return UsersList.Exists(u => u.Name == userName && u.Password == password);
}
}
Step 6:
Create a folder named Dtos and add class UserDto.cs. This data transfer object(DTO) will be used when we register an user to our application.
public class UserDto
{
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
Step 7:
Create a folder named Providers and add class JwtOAuthServerProvider.cs which is our custom authentication provider. This class derived by OAuthAuthorizationServerProvider and override two method.
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId = string.Empty;
string clientSecret = string.Empty;
if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
}
if (context.ClientId == null)
{
context.SetError("invalid_clientId", "client_Id is not set");
return Task.FromResult<object>(null);
}
var audience = AudiencesStore.FindAudience(context.ClientId);
if (audience == null)
{
context.SetError("invalid_clientId", string.Format("Invalid client_id '{0}'", context.ClientId));
return Task.FromResult<object>(null);
}
context.Validated();
return Task.FromResult<object>(null);
}
The above method is responsible for validate the audienceId/clientId and other information which will need to validate. Here audience means the resource server for which the token will be provided. I will explain this term "Audience" in my other post.
public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
//This is a Dummy check here, you need to do it to your DB
if (context.UserName == null)
{
context.SetError("invalid_username", "username is not set");
return Task.FromResult<object>(null);
}
var userExists = UserStore.FindUser(context.UserName, context.Password);
if (!userExists)
{
context.SetError("invalid_username", $"Invalid username '{context.UserName}' or password '{context.Password}'");
return Task.FromResult<object>(null);
}
var identity = new ClaimsIdentity("JWT");
identity.AddClaim(new Claim("name", context.UserName));
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("role", "Manager"));
identity.AddClaim(new Claim("role", "Supervisor"));
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{
"audience", (context.ClientId == null) ? string.Empty : context.ClientId
}
});
var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
return Task.FromResult<object>(null);
}
Above method is responsible for granting the access token for valid credential. If the credential is valid then this method will prepare Authentication ticket not the actual Token. This ticket contains some claims with some basic information like name, role and so on, what you need during authorization.
Step 8:
Crate a folder named Formats and add class JsonWebTokenDataFormat.cs. This will be responsible for generation of Json Web Token and validate the JWT token.
private const string AudiencePropertyKey = "audience";
private readonly string _issuer = string.Empty;
private readonly string _client_id = string.Empty;
private readonly string _secretkey = string.Empty;
public JsonWebTokenDataFormat()
{
_issuer = ConfigurationManager.AppSettings.Get("issuer");
_client_id = ConfigurationManager.AppSettings.Get("client.id");
_secretkey = ConfigurationManager.AppSettings.Get("secretkey");
}
As you see this class will be initialized three variables _issuer,_client_id, and _secretkey. This thee value will be used to generate the JWT token and validate the token.
Here _issuer will be the authorization server which will generate the token for resource server. _client_id will be the resource servers unique id for which the token is generated. _secret key is used to encrypt the JWT signature during token generating by the authorization server and decrypt the JWT signature during validate the token by the resource server.
In this post I have user the term "Authorization server" and "Resource server".
Let explain those.
In a production ready mvc application, Ideally should have two server. One for Authorization server, which is responsible for validating the user credential and generate the token. Other for Resource server, which is used to validate the Token and serve the resources of the application by the controller action.
public string Protect(AuthenticationTicket data)
{
if (data == null)
{
throw new ArgumentNullException("data");
}
string audienceId = data.Properties.Dictionary.ContainsKey(AudiencePropertyKey) ? data.Properties.Dictionary[AudiencePropertyKey] : null;
if (string.IsNullOrWhiteSpace(audienceId)) throw new InvalidOperationException("AuthenticationTicket.Properties does not include audience");
Audience audience = AudiencesStore.FindAudience(audienceId);
string symmetricKeyAsBase64 = audience.Base64Secret;
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
var securityKey = new SymmetricSecurityKey(TextEncodings.Base64Url.Decode(symmetricKeyAsBase64));
var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var issued = data.Properties.IssuedUtc;
var expires = data.Properties.ExpiresUtc;
var token = new JwtSecurityToken(_issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingCredentials);
var handler = new JwtSecurityTokenHandler();
var jsonWebToken = handler.WriteToken(token);
return jsonWebToken;
}
The above method is responsible for generate the token by using generated ticket which I described in previous section.
public AuthenticationTicket Unprotect(string protectedText)
{
var secret = TextEncodings.Base64Url.Decode(_secretkey);
if (string.IsNullOrWhiteSpace(protectedText))
{
throw new ArgumentNullException("protectedText");
}
var handler = new JwtSecurityTokenHandler();
var token = handler.ReadToken(protectedText) as JwtSecurityToken;
if (token == null)
{
throw new ArgumentOutOfRangeException("protectedText", "Invalid JWT Token");
}
ClaimsPrincipal claimsPrincipal;
try
{
var validationParameters = new TokenValidationParameters { IssuerSigningKey = new SymmetricSecurityKey(secret), ValidateAudience = true, ValidAudiences = new[] { this._client_id }, ValidateIssuer = true, ValidIssuer = this._issuer, ValidateLifetime = true, ValidateIssuerSigningKey = true };
var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken validatedToken = null;
claimsPrincipal = tokenHandler.ValidateToken(protectedText, validationParameters, out validatedToken);
}
catch (Exception)
{
return null;
}
var claimsIdentity = (ClaimsIdentity)claimsPrincipal.Identity;
var authenticationExtra = new AuthenticationProperties(new Dictionary<string, string>());
var returnedIdentity = new ClaimsIdentity(claimsIdentity.Claims, "JWT");
return new AuthenticationTicket(returnedIdentity, authenticationExtra);
}
The above method is responsible for validating the token.
Step 9: Create Controllers folder to the project and add class AccountController.cs. This class is responsible for creation of an user.
// POST api/Account/Register
[AllowAnonymous]
[Route("register")]
public async Task<IHttpActionResult> Register(UserDto userModel)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await Task.Run(() => UserStore.AddUser(userModel.Name, userModel.Email, userModel.Password)); ;
return Ok();
}
You have noticed that, this Register endpoint is decorated by AllowAnonymoius attribute which means, this action is accessible for anonymous user.
Step 10: Add class ClientsController.cs. This is mainly resource server controller.
[Authorize]
[Route("")]
public IHttpActionResult Get()
{
return Ok(Client.Clients);
}
As you see that, the Get action is decorated by Authorize attribute, that means, only authorized user can access this action.
Now we are ready to generate Token. We will use Postman to test our API. You could use any other Rest client application.
Run the project "RestServerApi" from visual studio.
We just send a post request to the endpoint "http://localhost:59598/security/token" using the credential user01/1!2@3# as below image.
During test by postman, keep in mind that you must select "x-www-form-urlencoded" radio button in Body tab. Because the default implementation of OAuthAuthorizationServerHandler only accepts form encoding (i.e. application/x-www-form-urlencoded) and not JSON encoding (application/JSON).
We got our desired response with access_token.
Now we are ready to access our resource server resources.
We just send a Get request to the endpoint "http://localhost:59598/api/clients" using the token which we generated previously. To get success response we just add "Authorization" header with the value format "bearer GeneratedAccessToken" as below image.
We got the success response. Without valid Authorization header we are not able to access the end point. It will show UnAuthorize(401) response for invalid token.
Now its time to explain our Angular client application. To keep the post readable, I would like to explain our client to the next post.
No comments:
Post a Comment