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:
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?
Did an edit to make it compatible with ASP.NET Core 2.0.
Firstly, some Nuget packages:
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 { // ... }
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With