ASP.NET Core 2.0 Bearer Auth without Identity
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{ // ...}
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!