Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Core 2.0 Bearer Auth without Identity

I thought I had a pretty simple goal in mind when I set out a day ago to implement a self-contained bearer auth webapi on .NET core 2.0, but I have yet to get anything remotely working. Here's a list of what I'm trying to do:

  • Implement a bearer token protected webapi
  • Issue tokens & refresh tokens from an endpoint in the same project
  • Use the [Authorize] attribute to control access to api surface
  • Not use ASP.Net Identity (I have much lighter weight user/membership reqs)

I'm totally fine with building identity/claims/principal in login and adding that to request context, but I've not seen a single example on how to issue and consume auth/refresh tokens in a Core 2.0 webapi without Identity. I've seen the 1.x MSDN example of cookies without Identity, but that didn't get me far enough in understanding to meet the requirements above.

I feel like this might be a common scenario and it shouldn't be this hard (maybe it's not, maybe just lack of documentation/examples?). As far as I can tell, IdentityServer4 is not compatible with Core 2.0 Auth, opendiddict seems to require Identity. I also don't want to host the token endpoint in a separate process, but within the same webapi instance.

Can anyone point me to a concrete example, or at least give some guidance as to what best steps/options are?

like image 786
pseabury Avatar asked Aug 16 '17 13:08

pseabury


2 Answers

Did an edit to make it compatible with ASP.NET Core 2.0.


Firstly, some Nuget packages:

  • Microsoft.AspNetCore.Authentication.JwtBearer
  • Microsoft.AspNetCore.Identity
  • System.IdentityModel.Tokens.Jwt
  • System.Security.Cryptography.Csp

Then some basic data transfer objects.

// Presumably you will have an equivalent user account class with a user name. public class User {     public string UserName { get; set; } }  public class JsonWebToken {     public string access_token { get; set; }      public string token_type { get; set; } = "bearer";      public int expires_in { get; set; }      public string refresh_token { get; set; } } 

Getting into the proper functionality, you'll need a login/token web method to actually send the authorization token to the user.

[Route("api/token")] public class TokenController : Controller {     private ITokenProvider _tokenProvider;      public TokenController(ITokenProvider tokenProvider) // We'll create this later, don't worry.     {         _tokenProvider = tokenProvider;     }      public JsonWebToken Get([FromQuery] string grant_type, [FromQuery] string username, [FromQuery] string password, [FromQuery] string refresh_token)     {         // Authenticate depending on the grant type.         User user = grant_type == "refresh_token" ? GetUserByToken(refresh_token) : GetUserByCredentials(username, password);          if (user == null)             throw new UnauthorizedAccessException("No!");          int ageInMinutes = 20;  // However long you want...          DateTime expiry = DateTime.UtcNow.AddMinutes(ageInMinutes);          var token = new JsonWebToken {             access_token = _tokenProvider.CreateToken(user, expiry),             expires_in   = ageInMinutes * 60         };          if (grant_type != "refresh_token")             token.refresh_token = GenerateRefreshToken(user);          return token;     }      private User GetUserByToken(string refreshToken)     {         // TODO: Check token against your database.         if (refreshToken == "test")             return new User { UserName = "test" };          return null;     }      private User GetUserByCredentials(string username, string password)     {         // TODO: Check username/password against your database.         if (username == password)             return new User { UserName = username };          return null;     }      private string GenerateRefreshToken(User user)     {         // TODO: Create and persist a refresh token.         return "test";     } } 

You probably noticed the token creation is still just "magic" passed through by some imaginary ITokenProvider. Define the token provider interface.

public interface ITokenProvider {     string CreateToken(User user, DateTime expiry);      // TokenValidationParameters is from Microsoft.IdentityModel.Tokens     TokenValidationParameters GetValidationParameters(); } 

I implemented the token creation with an RSA security key on a JWT. So...

public class RsaJwtTokenProvider : ITokenProvider {     private RsaSecurityKey _key;     private string _algorithm;     private string _issuer;     private string _audience;      public RsaJwtTokenProvider(string issuer, string audience, string keyName)     {         var parameters = new CspParameters { KeyContainerName = keyName };         var provider = new RSACryptoServiceProvider(2048, parameters);          _key = new RsaSecurityKey(provider);          _algorithm = SecurityAlgorithms.RsaSha256Signature;         _issuer = issuer;         _audience = audience;     }      public string CreateToken(User user, DateTime expiry)     {         JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();          ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user.UserName, "jwt"));          // TODO: Add whatever claims the user may have...          SecurityToken token = tokenHandler.CreateJwtSecurityToken(new SecurityTokenDescriptor         {             Audience = _audience,             Issuer = _issuer,             SigningCredentials = new SigningCredentials(_key, _algorithm),             Expires = expiry.ToUniversalTime(),             Subject = identity         });          return tokenHandler.WriteToken(token);     }      public TokenValidationParameters GetValidationParameters()     {         return new TokenValidationParameters         {             IssuerSigningKey = _key,             ValidAudience = _audience,             ValidIssuer = _issuer,             ValidateLifetime = true,             ClockSkew = TimeSpan.FromSeconds(0) // Identity and resource servers are the same.         };     } } 

So you're now generating tokens. Time to actually validate them and wire it up. Go to your Startup.cs.

In ConfigureServices()

var tokenProvider = new RsaJwtTokenProvider("issuer", "audience", "mykeyname"); services.AddSingleton<ITokenProvider>(tokenProvider);  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)     .AddJwtBearer(options => {         options.RequireHttpsMetadata = false;         options.TokenValidationParameters = tokenProvider.GetValidationParameters();     });  // This is for the [Authorize] attributes. services.AddAuthorization(auth => {     auth.DefaultPolicy = new AuthorizationPolicyBuilder()         .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)         .RequireAuthenticatedUser()         .Build(); }); 

Then Configure()

public void Configure(IApplicationBuilder app) {     app.UseAuthentication();      // Whatever else you're putting in here...      app.UseMvc(); } 

That should be about all you need. Hopefully I haven't missed anything.

The happy result is...

[Authorize] // Yay! [Route("api/values")] public class ValuesController : Controller {     // ... } 
like image 133
Mitch Avatar answered Sep 23 '22 12:09

Mitch


Following on @Mitch answer: Auth stack changed quite a bit moving to .NET Core 2.0. Answer below is just using the new implementation.

using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens;  namespace JwtWithoutIdentity {     public class Startup     {         public Startup(IConfiguration configuration)         {             Configuration = configuration;         }          public IConfiguration Configuration { get; }          // This method gets called by the runtime. Use this method to add services to the container.         public void ConfigureServices(IServiceCollection services)         {             services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)                 .AddJwtBearer(cfg =>                 {                     cfg.RequireHttpsMetadata = false;                     cfg.SaveToken = true;                      cfg.TokenValidationParameters = new TokenValidationParameters()                     {                         ValidIssuer = "me",                         ValidAudience = "you",                         IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")) //Secret                     };                  });              services.AddMvc();         }          // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.         public void Configure(IApplicationBuilder app, IHostingEnvironment env)         {             if (env.IsDevelopment())             {                 app.UseDeveloperExceptionPage();             }              app.UseAuthentication();              app.UseMvc();         }     } } 

Token Controller

using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using System.Threading.Tasks; using JwtWithoutIdentity.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens;  namespace JwtWithoutIdentity.Controllers {     public class TokenController : Controller     {          [AllowAnonymous]         [Route("api/token")]         [HttpPost]         public async Task<IActionResult> Token(LoginViewModel model)         {              if (!ModelState.IsValid) return BadRequest("Token failed to generate");              var user = (model.Password == "password" && model.Username == "username");              if (!user) return Unauthorized();              //Add Claims             var claims = new[]             {                 new Claim(JwtRegisteredClaimNames.UniqueName, "data"),                 new Claim(JwtRegisteredClaimNames.Sub, "data"),                 new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),             };              var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")); //Secret             var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);              var token = new JwtSecurityToken("me",                 "you",                 claims,                 expires: DateTime.Now.AddMinutes(30),                 signingCredentials: creds);              return Ok(new JsonWebToken()             {                 access_token = new JwtSecurityTokenHandler().WriteToken(token),                 expires_in = 600000,                 token_type = "bearer"             });         }     } } 

Values Controller

using System.Collections.Generic; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc;  namespace JwtWithoutIdentity.Controllers {     [Route("api/[controller]")]     public class ValuesController : Controller     {         // GET api/values         [Authorize]         [HttpGet]         public IEnumerable<string> Get()         {             var name = User.Identity.Name;             var claims = User.Claims;              return new string[] { "value1", "value2" };         }     } } 

Hope this helps!

like image 26
Sethles Avatar answered Sep 24 '22 12:09

Sethles