adding jwt auth

This commit is contained in:
Andy Pack 2023-01-21 16:17:46 +00:00
parent 5650f69876
commit f13f2d9fd3
Signed by: sarsoo
GPG Key ID: A55BA3536A5E0ED7
7 changed files with 221 additions and 25 deletions

View File

@ -9,12 +9,6 @@ namespace Selector.Model.Extensions
{
public static void AddAuthorisationHandlers(this IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
services.AddScoped<IAuthorizationHandler, WatcherIsOwnerAuthHandler>();
services.AddSingleton<IAuthorizationHandler, WatcherIsAdminAuthHandler>();

View File

@ -0,0 +1,61 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Selector.Model;
namespace Selector.Web.Auth;
public class JwtTokenService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IOptions<JwtOptions> _options;
public JwtTokenService(UserManager<ApplicationUser> userManager, IOptions<JwtOptions> options)
{
_userManager = userManager;
_options = options;
}
public async Task<JwtSecurityToken> CreateJwtToken(ApplicationUser user)
{
var userClaims = await _userManager.GetClaimsAsync(user);
var roles = await _userManager.GetRolesAsync(user);
var roleClaims = roles.Select(r => new Claim("roles", r));
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Name, user.UserName),
new Claim("uid", user.Id)
}
.Union(userClaims)
.Union(roleClaims);
var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.Key));
var signingCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256);
var jwtSecurityToken = new JwtSecurityToken(
issuer: _options.Value.Issuer,
audience: _options.Value.Audience,
claims: claims,
expires: DateTime.UtcNow.Add(_options.Value.Expiry),
signingCredentials: signingCredentials);
return jwtSecurityToken;
}
}

View File

@ -0,0 +1,71 @@
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Selector.Model;
using Selector.Web.Auth;
namespace Selector.Web.Controller;
[ApiController]
[AllowAnonymous]
[Route("api/[controller]/token")]
public class AuthController : BaseAuthController
{
private readonly JwtTokenService _tokenService;
public AuthController(ApplicationDbContext context, IAuthorizationService auth, UserManager<ApplicationUser> userManager, ILogger<BaseAuthController> logger, JwtTokenService tokenService) : base(context, auth, userManager, logger)
{
_tokenService = tokenService;
}
public class TokenModel
{
public string Username { get; set; }
public string Password { get; set; }
}
[HttpPost]
public async Task<IActionResult> Token([FromForm] TokenModel model)
{
var user = await UserManager.GetUserAsync(User);
if (user is null) // user isn't logged in, use parameter creds
{
if (model.Username is null)
{
return BadRequest("No username provided");
}
if (model.Password is null)
{
return BadRequest("No password provided");
}
var normalUsername = model.Username.Trim().ToUpperInvariant();
user = await Context.Users.FirstOrDefaultAsync(u => u.NormalizedUserName == normalUsername);
if (user is null)
{
return NotFound("user not found");
}
if (!await UserManager.CheckPasswordAsync(user, model.Password))
{
return Unauthorized("credentials failed");
}
}
var token = await _tokenService.CreateJwtToken(user);
var tokenHandler = new JwtSecurityTokenHandler();
return Ok(new Dictionary<string, string>
{
{"token", tokenHandler.WriteToken(token)}
});
}
}

View File

@ -57,4 +57,15 @@ namespace Selector.Web
public bool Enabled { get; set; } = false;
public string ConnectionString { get; set; }
}
public class JwtOptions
{
public const string _Key = "Jwt";
public string Key { get; set; }
public string Issuer { get; set; }
public string Audience { get; set; }
public TimeSpan Expiry { get; set; } = TimeSpan.FromDays(7);
}
}

View File

@ -32,17 +32,21 @@
<PackageReference Include="NLog" Version="5.1.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.1" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.2.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
<Folder Include="NowPlaying\" />
<Folder Include="Past\" />
<Folder Include="Auth\" />
</ItemGroup>
<ItemGroup>
<None Remove="NowPlaying\" />
<None Remove="Past\" />
<None Remove="Microsoft.AspNetCore.Authentication.JwtBearer" />
<None Remove="Auth\" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.Development.json" Condition="Exists('appsettings.Development.json')">

View File

@ -1,24 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Selector.Cache.Extensions;
using Selector.Events;
using Selector.Web.Hubs;
using Selector.Web.Extensions;
using Selector.Extensions;
using Selector.Model;
using Selector.Model.Extensions;
using Selector.Cache;
using Selector.Cache.Extensions;
using Selector.Web.Auth;
using Selector.Web.Extensions;
using Selector.Web.Hubs;
namespace Selector.Web
{
@ -46,6 +45,10 @@ namespace Selector.Web
{
Configuration.GetSection(string.Join(':', RootOptions.Key, NowPlayingOptions.Key)).Bind(options);
});
services.Configure<JwtOptions>(options =>
{
Configuration.GetSection(JwtOptions._Key).Bind(options);
});
var config = OptionsHelper.ConfigureOptions(Configuration);
@ -60,6 +63,20 @@ namespace Selector.Web
services.AddSignalR(o => o.EnableDetailedErrors = true);
services.AddHttpClient();
ConfigureDB(services, config);
ConfigureIdentity(services, config);
ConfigureAuth(services, config);
services.AddEvents();
services.AddSpotify();
ConfigureLastFm(config, services);
ConfigureRedis(services, config);
}
public void ConfigureDB(IServiceCollection services, RootOptions config)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("Default"))
);
@ -69,7 +86,10 @@ namespace Selector.Web
services.AddTransient<IListenRepository, MetaListenRepository>();
//services.AddTransient<IListenRepository, SpotifyListenRepository>();
}
public void ConfigureIdentity(IServiceCollection services, RootOptions config)
{
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
@ -96,7 +116,10 @@ namespace Selector.Web
options.User.RequireUniqueEmail = false;
options.SignIn.RequireConfirmedEmail = false;
});
}
public void ConfigureAuth(IServiceCollection services, RootOptions config)
{
services.ConfigureApplicationCookie(options =>
{
// Cookie settings
@ -108,13 +131,41 @@ namespace Selector.Web
options.SlidingExpiration = true;
});
var jwtConfig = Configuration.GetSection(JwtOptions._Key).Get<JwtOptions>();
services.AddAuthentication()
.AddJwtBearer(o =>
{
o.RequireHttpsMetadata = false;
o.SaveToken = false;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
ValidIssuer = jwtConfig.Issuer,
ValidAudience = jwtConfig.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.Key))
};
});
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(IdentityConstants.ApplicationScheme, JwtBearerDefaults.AuthenticationScheme)
.Build();
});
services.AddTransient<JwtTokenService>();
services.AddAuthorisationHandlers();
}
services.AddEvents();
services.AddSpotify();
ConfigureLastFm(config, services);
public void ConfigureRedis(IServiceCollection services, RootOptions config)
{
if (config.RedisOptions.Enabled)
{
Console.WriteLine("> Adding Redis...");

View File

@ -16,5 +16,9 @@
"TrackResampleWindow": "14.00:00:00"
}
},
"Jwt": {
"Issuer": "https://selector.sarsoo.xyz",
"Audience": "selector.sarsoo.xyz"
},
"AllowedHosts": "*"
}