Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use .NET Core Identity with an API

i've created an API and set up JWT auth from the same API (I chose not to use IdentityServer4).

I did this through services.AddAuthentication

And then I created tokens in the controller and it works.

However I now want to add registration etc. But i prefer not to write my own code for hashing passwords, handling registration emails etc.

So I came across ASP.NET Core Identity and it seems like what I need, except from that it adds some UI stuff that I dont need (because its just an API and the UI i want completely independent).

But on MSDN is written:

ASP.NET Core Identity adds user interface (UI) login functionality to ASP.NET Core web apps. To secure web APIs and SPAs, use one of the following:

Azure Active Directory

Azure Active Directory B2C (Azure AD B2C)

IdentityServer4

So is it really a bad idea to use Core Identity just for hashing and registration logic for an API? Cant I just ignore the UI functionality? It's very confusing because I'd rather not use IdentityServer4 or create my own user management logic.

like image 662
Jonathan Daniel Avatar asked Jan 31 '21 19:01

Jonathan Daniel


People also ask

How do I authorize API in NET Core?

Require authorization on a new API By default, the system is configured to easily require authorization for new APIs. To do so, create a new controller and add the [Authorize] attribute to the controller class or to any action within the controller.


Video Answer


2 Answers

Let me just get off my chest that the bundling Identity does with the UI, the cookies and the confusing various extension methods that add this or that, but don't add this or that, is pretty annoying, at least when you build modern web APIs that need no cookies nor UI.

In some projects I also use manual JWT token generation with Identity for the membership features and user/password management.

Basically the simplest thing to do is to check the source code.

  1. AddDefaultIdentity() adds authentication, adds the Identity cookies, adds the UI, and calls AddIdentityCore(); but has no support for roles:
public static IdentityBuilder AddDefaultIdentity<TUser>(this IServiceCollection services, Action<IdentityOptions> configureOptions) where TUser : class
{
    services.AddAuthentication(o =>
    {
        o.DefaultScheme = IdentityConstants.ApplicationScheme;
        o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    })
    .AddIdentityCookies(o => { });

    return services.AddIdentityCore<TUser>(o =>
    {
        o.Stores.MaxLengthForKeys = 128;
        configureOptions?.Invoke(o);
    })
        .AddDefaultUI()
        .AddDefaultTokenProviders();
}
  1. AddIdentityCore() is a more stripped down version that only adds basic services, but it doesn't even add authentication, and also no support for roles (here you can already see what individual services are added, to change/override/remove them if you want):

public static IdentityBuilder AddIdentityCore<TUser>(this IServiceCollection services, Action<IdentityOptions> setupAction)
    where TUser : class
{
    // Services identity depends on
    services.AddOptions().AddLogging();

    // Services used by identity
    services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
    services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
    services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
    services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
    services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
    // No interface for the error describer so we can add errors without rev'ing the interface
    services.TryAddScoped<IdentityErrorDescriber>();
    services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser>>();
    services.TryAddScoped<UserManager<TUser>>();

    if (setupAction != null)
    {
        services.Configure(setupAction);
    }

    return new IdentityBuilder(typeof(TUser), services);
}

Now that kind of makes sense so far, right?

  1. But enter AddIdentity(), which appears to be the most bloated, the only one that supports roles directly, but confusingly enough it doesn't seem to add the UI:
public static IdentityBuilder AddIdentity<TUser, TRole>(
    this IServiceCollection services,
    Action<IdentityOptions> setupAction)
    where TUser : class
    where TRole : class
{
    // Services used by identity
    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
        options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
        options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    })
    .AddCookie(IdentityConstants.ApplicationScheme, o =>
    {
        o.LoginPath = new PathString("/Account/Login");
        o.Events = new CookieAuthenticationEvents
        {
            OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
        };
    })
    .AddCookie(IdentityConstants.ExternalScheme, o =>
    {
        o.Cookie.Name = IdentityConstants.ExternalScheme;
        o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
    })
    .AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o =>
    {
        o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
        o.Events = new CookieAuthenticationEvents
        {
            OnValidatePrincipal = SecurityStampValidator.ValidateAsync<ITwoFactorSecurityStampValidator>
        };
    })
    .AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
    {
        o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
        o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
    });

    // Hosting doesn't add IHttpContextAccessor by default
    services.AddHttpContextAccessor();
    // Identity services
    services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
    services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
    services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
    services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
    services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>();
    // No interface for the error describer so we can add errors without rev'ing the interface
    services.TryAddScoped<IdentityErrorDescriber>();
    services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
    services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
    services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
    services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
    services.TryAddScoped<UserManager<TUser>>();
    services.TryAddScoped<SignInManager<TUser>>();
    services.TryAddScoped<RoleManager<TRole>>();

    if (setupAction != null)
    {
        services.Configure(setupAction);
    }

    return new IdentityBuilder(typeof(TUser), typeof(TRole), services);
}

All in all what you probably need is the AddIdentityCore(), plus you have to use AddAuthentication() on your own.

Also, if you use AddIdentity(), be sure to run your AddAuthentication() configuration after calling AddIdentity(), because you have to override the default authentication schemes (I ran into problems related to this, but can't remember the details).

(Another tidbit of information that might be interesting for people reading this is the distinction between SignInManager.PasswordSignInAsync(), SignInManager.CheckPasswordSignInAsync() and UserManager.CheckPasswordAsync(). These are all public methods you can find and call for authorization purposes. PasswordSignInAsync() implements two-factor signin (also sets cookies; probably only when using AddIdentity() or AddDefaultIdentity()) and calls CheckPasswordSignInAsync(), which implements user lockout handling and calls UserManager.CheckPasswordAsync(), which just checks the password. So to get a proper authentication it's better not to call UserManager.CheckPasswordAsync() directly, but to do it through CheckPasswordSignInAsync(). But, in a single-factor JWT token scenario, calling PasswordSignInAsync() is probably not needed (and you can run into redirect issues). If you have included UseAuthentication()/AddAuthentication() in the Startup with the proper JwtBearer token schemes set, then the next time the client sends a request with a valid token attached, the authentication middleware will kick in, and the client will be 'signed in'; i.e. any valid JWT token will allow client to access controller actions protected with [Authorize].)

And IdentityServer is thankfully completely separate from Identity. In fact the decent implementation of IdentityServer is to use it as a standalone literal identity server that issues tokens for your services. But since ASP.NET Core has no token generation capability built-in, a lot of people end up running this bloated server inside their apps just to be able to use JWT tokens, even though they have a single app and they have no real use for a central authority. I don't mean to hate on it, it's a really great solution with a lot of features, but it would be nice to have something simpler for the more common use cases.

like image 184
Leaky Avatar answered Oct 21 '22 12:10

Leaky


You just configure Identity to use JWT bearer tokens. In my case I'm using encrypted token, so depending on your use-case you may want to adjust the configuration:

// In Startup.ConfigureServices...
services.AddDefaultIdentity<ApplicationUser>(
    options =>
    {
        // Configure password options etc.
    })
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

// Configure authentication
services.AddAuthentication(
    opt =>
    {
        opt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
            options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = false,
            ValidateAudience = false,
            TokenDecryptionKey =
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my key")),
            RequireSignedTokens = false, // False because I'm encrypting the token instead
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        };
    });


// Down in Startup.Configure add authn+authz middlewares
app.UseAuthentication();
app.UseAuthorization();

Then generate a token when the user wants to sign in:

var encKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my key"));
var encCreds = new EncryptingCredentials(encKey, SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512);

var claimsIdentity = await _claimsIdentiyFactory.CreateAsync(user);

var desc = new SecurityTokenDescriptor
{
    Subject = claimsIdentity,
    Expires = DateTime.UtcNow.AddMinutes(_configuration.Identity.JwtExpirationMinutes),
    Issuer = _configuration.Identity.JwtIssuer,
    Audience = _configuration.Identity.JwtAudience,
    EncryptingCredentials = encCreds
};

var token = new JwtSecurityTokenHandler().CreateEncodedJwt(desc);
// Return it to the user

You can then use the UserManager to handle creating new users and retrieving users, while SignInManager can be used to check for valid login/credentials before generating the token.

like image 29
Xerillio Avatar answered Oct 21 '22 11:10

Xerillio