Skip to content

11 - Identity in Rest API

Rest identity

No cookies in AJAX requests No Rest controllers in ASP.NET Core for Identity

Industrial strength solutions

  • https://github.com/IdentityServer
  • Full OpenID and OAuth 2.0
  • Integrates with ASP.NET Identity database
  • https://identityserver.io
  • Licensing issues (free for free open source work)


  • https://www.keycloak.org

JWT

No Cookies! (Sometimes confusing when you do both normal web and Rest API-s from the same server)

Ajax uses JSON Web Tokens (JWT) – RFC 7519

  • Test and verify on https://jwt.io
  • Digitally signed (usually not encrypted) - HMAC, RSA, ECDSA
  • HTTPS is mandatory

JWT Format

  • Format: Header.Payload.Signature
    • Every part is Base64Url encoded


  • Header: Type and Signing algorithm
  • Payload: Claims (registered, public, private)
  • Signature: createSignature(Header + "." + Payload, secret)

JWT based auth

  • In authentication, when the user successfully logs in using their credentials, a JSON Web Token will be returned. Since tokens are credentials, great care must be taken to prevent security issues.

  • Whenever the user wants to access a protected route or resource, the user agent should send the JWT, typically in the Authorization header using the Bearer schema.

Authorization: Bearer <token>

  • If the token is sent in the Authorization header, Cross-Origin Resource Sharing (CORS) won't be an issue as it doesn't use cookies.

Token auth simple flow

DI

JWT problems

With signed tokens, all the information contained within the token is exposed to users or other parties, even though they are unable to change it.

JWT simple implementation

Configure app settings for JWT (add new section)

1
2
3
4
5
"JWT": {
    "Key": "some_secret_password_dont_share.123",
    "Issuer": "itcollege.taltech.ee",
    "ExpireDays": 30
}

Install via unget Microsoft.AspNetCore.Authentication.JwtBearer;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
Services
    .AddAuthentication()
    .AddCookie(options => { options.SlidingExpiration = true; })
    .AddJwtBearer(cfg =>
    {
        cfg.RequireHttpsMetadata = false;
        cfg.SaveToken = true;
        cfg.TokenValidationParameters = new TokenValidationParameters
        {
            ValidIssuer = Configuration["JWT:Issuer"],
            ValidAudience = Configuration["JWT:Issuer"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"])),
            ClockSkew = TimeSpan.Zero // remove delay of token when expire
        };
    });

If no cookie support is needed, make JWT auth default

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// =============== JWT support ===============
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
Services
    .AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddCookie(options => { options.SlidingExpiration = true; })
    .AddJwtBearer(cfg =>
    {
        cfg.RequireHttpsMetadata = false;
        cfg.SaveToken = true;
        cfg.TokenValidationParameters = new TokenValidationParameters
        {
            ValidIssuer = Configuration["JwtIssuer"],
            ValidAudience = Configuration["JwtIssuer"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
            ClockSkew = TimeSpan.Zero // remove delay of token when expire
        };
    });

Generate JWT

  • Add controller for register and login Rest calls - return token on success

  • Create method to generate JWT tokens

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static string GenerateJwt(IEnumerable<Claim> claims, string key, string issuer, int expiresInDays)
{
    var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
    var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
    var expires = DateTime.Now.AddDays(expiresInDays);
    var token = new JwtSecurityToken(
        issuer: issuer,
        audience: issuer,
        claims: claims,
        expires: expires,
        signingCredentials: signingCredentials
    );
    return new JwtSecurityTokenHandler().WriteToken(token);
}

Use token

On web api request, include token in header

  • Authorization: Bearer eyJhbGciOiJIUzI1NiIsI.....
  • In web-api controllers – use the same methods and attributes, as in the pure html/MVC side.
  • [Authorize] will use the authorization mechanism in the same order, as the are added to middleware (so cookie auth can be first - bad for REST)
  • Force use of specific schema
    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

Token auth with refresh

DI

Token will finally expire - in more secure apps token lifetime is really short (1 minute).
To avoid constant login, another token is used - refresh token (random guid for example).
Refresh token lifetime is much longer (days).
When token expiration is detected, user can request new token by sending old token and refresh token to refesh endpoint.
User info is extracted from old token, then from permanent storage (IdenityUser) user info is retrieved along with refresh token info (value end expiration time).
When user supplied refresh token matches with stored refresh token - new token and refresh token (refresh token rotation) is issued.
Some grace period might be need (up to 60 seconds) before revoking old refresh token.
If user logs-in from multiple instances - refresh token collection.

1
2
3
4
5
public class AppUser : IdentityUser<Guid>
{
    public string? RefreshToken { get; set; }
    public DateTime RefreshTokenExpiryTime { get; set; }
}

ASPNET Core Identity methods

1
2
3
4
var appUser = await _userManager.FindByEmailAsync(dto.Email);
var result = await _signInManager.CheckPasswordSignInAsync(appUser, dto.Password, false);
var result = await _userManager.CreateAsync(appUser, dto.Password);
var claimsPrincipal = await _signInManager.CreateUserPrincipalAsync(appUser);