Skip to content

Instantly share code, notes, and snippets.

@dasjestyr
Last active October 14, 2018 20:11
Show Gist options
  • Save dasjestyr/b3abe5a3ad63c368a52be0193e3c72a0 to your computer and use it in GitHub Desktop.
Save dasjestyr/b3abe5a3ad63c368a52be0193e3c72a0 to your computer and use it in GitHub Desktop.
.Net Core Auth/Identity

Creating a custom Identity Provider (without using EF)

A few interfaces need to be implemented:

  • IUserStore<TUserType>
  • IRoleStore<TRoleType>

TUserType and TRoleType are custom classes that represent a user or a role. There are other interfaces that can be implemented which also implement one of the interfaces above. For example IUserRoleStore or IUserPasswordStore which both also implement IUserStore. Use them in combination to obtain more customization, for example if you want to override how roles get stored against the user.

Once those are implemented, you need to enable identity and also set the user and role store implementations:

services // IServiceCollection
    .AddIdentity<UserData, RoleData>(options =>
    {
        options.SignIn.RequireConfirmedEmail = false;
        options.SignIn.RequireConfirmedPhoneNumber = false;
    })

Notice that you can modify validation features as options.

Creating and modifying users/roles

There are built-in managers for using these implementations of IUserStore and IRoleStore, but if you're using a custom type (vs. using the default IdentityUser / IdentityRole) then you need to let the IoC container know:

services
    .AddIdentity<UserData, RoleData>(options =>
    {
        options.SignIn.RequireConfirmedEmail = false;
        options.SignIn.RequireConfirmedPhoneNumber = false;
    })
    .AddUserManager<UserManager<UserData>>()
    .AddRoleManager<RoleManager<RoleData>>()
    .AddSignInManager<SignInManager<UserData>>()
    .AddUserStore<UserStore>()
    .AddRoleStore<RoleStore>()
    .AddDefaultTokenProviders();

I don't know why you have to specify the stores as well, seeing as they're defined in AddIdentity()...

Now that those are defined, the managers have been added to DI and they can now be requested as services. So here is an example of creating a new user, using the managers:

public UsersController(UserManager<UserData> userManager)
{
    _userManager = userManager;
}

[HttpPost, Route("")]
public async Task<IActionResult> RegisterUser([FromBody] UserInfo payload)
{
    if (!ModelState.IsValid) return BadRequest(ModelState);

    var user = new UserData
    {
        FirstName = payload.FirstName,
        LastName = payload.LastName,
        Username = payload.Username,
        Email = payload.Email,
        Password = payload.Password,
        NormalizedUserName = payload.Username,
        NormalizedEmail = payload.Email,
        UserId = Xid.NewXid().ToString()
    };

    var result = await _userManager
        .CreateAsync(user, payload.Password)
        .ConfigureAwait(false);
        
    if (!result.Succeeded)
    {
        foreach(var error in result.Errors)
            ModelState.AddModelError(error.Code, error.Description);
        
        return StatusCode(500, ModelState);
    }

    return Created($"/users/{user.UserId}", user);
}

JWT Bearer Token Auth

Using a JWT token to authenticate

Enable JWT

In ConfigureServices

var clientSecret = Encoding.UTF8.GetBytes(config["CLIENT_SECRET"]);
services.AddAuthentication(options => // services is IServiceCollection
{
    options.DefaultAuthenticateScheme =
        JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme =
        JwtBearerDefaults.AuthenticationScheme;
        
}).AddJwtBearer(options =>
{
    // use whatever options make sense
    options.Audience = config["jwt_audience"];
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidIssuer = config["jwt_issuer"],
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(clientSecret)
    };
});

In ConfigureApp

app.UseAuthentication(); // app is IApplicationBuilder

The IssuerSigningKey is just a symmetric security key using the client secret for the hash.

A couple notes about the JWT token

  • The subject (sub) claim is used by signalR as the UserIdentifer
  • The issuer (iss) should typically be the base url of the auth server (e.g. https://auth.provausio.com)
  • The audience (aud) should typically be the main scope of the token (e.g. https://api.provausio.com)
  • The Identity.Name property in a .net identity is non-standard jwt claim. The type is a urn that is defined in ClaimTypes.Name

Generating a token

See IdentityManagement tab for setting up Identity. Once that is setup, the managers are available.

The basic process is:

  • Receive a username/password post request
  • Search for the user,
  • If they exist, attempt to sign them in.
  • If sign in succeeds, use their info to create a JWT Security Token which will be used to establish a principal/identity on subsequent requests.
  • Write out the JWT token and return it to the user

Signin example

[HttpPost, Route("login"), AllowAnonymous]
public async Task<IActionResult> Login([FromForm] LoginInfo login)
{
    // find the user first
    var user = await _userManager
            .FindByNameAsync(login.Username)
            .ConfigureAwait(false);
            
    if (user == null) return NotFound();
    
    // authenticate them
    var x = await _signInManager
        .PasswordSignInAsync(user.NormalizedUserName, login.Password, false, false)
        .ConfigureAwait(false);
    
    if (!x.Succeeded) return Unauthorized();
    
    // build their access token
    var accessToken = _tokenService.GenerateToken(user);
    
    return Ok(new
    {
        Token = accessToken
    });
}

Token Service implementation

public class TokenService : ITokenService
{
    private readonly IConfiguration _config;

    public TokenService(IConfiguration config)
    {
        _config = config;
    }
    
    public string GenerateToken(UserData user)
    {
        var claims = new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.UserId),
            new Claim(JwtRegisteredClaimNames.Email, user.Email),
            new Claim(JwtRegisteredClaimNames.GivenName, user.FirstName),
            new Claim(JwtRegisteredClaimNames.FamilyName, user.LastName),
            new Claim(JwtRegisteredClaimNames.UniqueName, user.Username),
            new Claim(ClaimTypes.Name, user.Username)
        };
        
        // TODO: insert roles by getting them from one of the managers
        
        var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["CLIENT_SECRET"])); 
        var signingCreds = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
        var expiration = DateTime.UtcNow.AddHours(Convert.ToDouble(_config["jwt_expire_hours"]));

        var token = new JwtSecurityToken(
            _config["jwt_issuer"],
            _config["jwt_audience"],
            claims,
            expires: expiration,
            signingCredentials: signingCreds);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment