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) public static void AddAuthorisationHandlers(this IServiceCollection services)
{ {
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
services.AddScoped<IAuthorizationHandler, WatcherIsOwnerAuthHandler>(); services.AddScoped<IAuthorizationHandler, WatcherIsOwnerAuthHandler>();
services.AddSingleton<IAuthorizationHandler, WatcherIsAdminAuthHandler>(); 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 bool Enabled { get; set; } = false;
public string ConnectionString { get; set; } 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" Version="5.1.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.1" /> <PackageReference Include="NLog.Extensions.Logging" Version="5.2.1" />
<PackageReference Include="NLog.Web.AspNetCore" 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>
<ItemGroup> <ItemGroup>
<Folder Include="wwwroot\" /> <Folder Include="wwwroot\" />
<Folder Include="NowPlaying\" /> <Folder Include="NowPlaying\" />
<Folder Include="Past\" /> <Folder Include="Past\" />
<Folder Include="Auth\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="NowPlaying\" /> <None Remove="NowPlaying\" />
<None Remove="Past\" /> <None Remove="Past\" />
<None Remove="Microsoft.AspNetCore.Authentication.JwtBearer" />
<None Remove="Auth\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="appsettings.Development.json" Condition="Exists('appsettings.Development.json')"> <None Update="appsettings.Development.json" Condition="Exists('appsettings.Development.json')">

View File

@ -1,24 +1,23 @@
using System; using System;
using System.Collections.Generic; using System.Text;
using System.Linq; using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Identity; using Selector.Cache.Extensions;
using Microsoft.EntityFrameworkCore;
using Selector.Events; using Selector.Events;
using Selector.Web.Hubs;
using Selector.Web.Extensions;
using Selector.Extensions; using Selector.Extensions;
using Selector.Model; using Selector.Model;
using Selector.Model.Extensions; using Selector.Model.Extensions;
using Selector.Cache; using Selector.Web.Auth;
using Selector.Cache.Extensions; using Selector.Web.Extensions;
using Selector.Web.Hubs;
namespace Selector.Web namespace Selector.Web
{ {
@ -46,6 +45,10 @@ namespace Selector.Web
{ {
Configuration.GetSection(string.Join(':', RootOptions.Key, NowPlayingOptions.Key)).Bind(options); 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); var config = OptionsHelper.ConfigureOptions(Configuration);
@ -60,6 +63,20 @@ namespace Selector.Web
services.AddSignalR(o => o.EnableDetailedErrors = true); services.AddSignalR(o => o.EnableDetailedErrors = true);
services.AddHttpClient(); 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 => services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("Default")) options.UseNpgsql(Configuration.GetConnectionString("Default"))
); );
@ -69,7 +86,10 @@ namespace Selector.Web
services.AddTransient<IListenRepository, MetaListenRepository>(); services.AddTransient<IListenRepository, MetaListenRepository>();
//services.AddTransient<IListenRepository, SpotifyListenRepository>(); //services.AddTransient<IListenRepository, SpotifyListenRepository>();
}
public void ConfigureIdentity(IServiceCollection services, RootOptions config)
{
services.AddIdentity<ApplicationUser, IdentityRole>() services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>() .AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI() .AddDefaultUI()
@ -96,7 +116,10 @@ namespace Selector.Web
options.User.RequireUniqueEmail = false; options.User.RequireUniqueEmail = false;
options.SignIn.RequireConfirmedEmail = false; options.SignIn.RequireConfirmedEmail = false;
}); });
}
public void ConfigureAuth(IServiceCollection services, RootOptions config)
{
services.ConfigureApplicationCookie(options => services.ConfigureApplicationCookie(options =>
{ {
// Cookie settings // Cookie settings
@ -108,13 +131,41 @@ namespace Selector.Web
options.SlidingExpiration = true; 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.AddAuthorisationHandlers();
}
services.AddEvents(); public void ConfigureRedis(IServiceCollection services, RootOptions config)
{
services.AddSpotify();
ConfigureLastFm(config, services);
if (config.RedisOptions.Enabled) if (config.RedisOptions.Enabled)
{ {
Console.WriteLine("> Adding Redis..."); Console.WriteLine("> Adding Redis...");

View File

@ -11,10 +11,14 @@
"enabled": true "enabled": true
}, },
"Now": { "Now": {
"ArtistResampleWindow": "14.00:00:00", "ArtistResampleWindow": "14.00:00:00",
"AlbumResampleWindow": "14.00:00:00", "AlbumResampleWindow": "14.00:00:00",
"TrackResampleWindow": "14.00:00:00" "TrackResampleWindow": "14.00:00:00"
} }
}, },
"Jwt": {
"Issuer": "https://selector.sarsoo.xyz",
"Audience": "selector.sarsoo.xyz"
},
"AllowedHosts": "*" "AllowedHosts": "*"
} }