POC oidc login
This commit is contained in:
parent
6288d89651
commit
df9d970a42
48 changed files with 5009 additions and 96 deletions
|
@ -1,20 +1,34 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using MessageReceivedContext = Microsoft.AspNetCore.Authentication.JwtBearer.MessageReceivedContext;
|
||||
using TokenValidatedContext = Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext;
|
||||
|
||||
namespace API.Extensions;
|
||||
#nullable enable
|
||||
|
||||
public static class IdentityServiceExtensions
|
||||
{
|
||||
private const string DynamicJwt = nameof(DynamicJwt);
|
||||
private const string OpenIdConnect = nameof(OpenIdConnect);
|
||||
private const string LocalIdentity = nameof(LocalIdentity);
|
||||
|
||||
public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
|
@ -47,42 +61,139 @@ public static class IdentityServiceExtensions
|
|||
.AddRoleValidator<RoleValidator<AppRole>>()
|
||||
.AddEntityFrameworkStores<DataContext>();
|
||||
|
||||
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
var auth = services.AddAuthentication(DynamicJwt)
|
||||
.AddPolicyScheme(DynamicJwt, JwtBearerDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters()
|
||||
var iss = Configuration.OidcAuthority;
|
||||
var enabled = Configuration.OidcEnabled;
|
||||
options.ForwardDefaultSelector = context =>
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"]!)),
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidIssuer = "Kavita"
|
||||
};
|
||||
if (!enabled)
|
||||
return LocalIdentity;
|
||||
|
||||
options.Events = new JwtBearerEvents()
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
var fullAuth =
|
||||
context.Request.Headers["Authorization"].FirstOrDefault() ??
|
||||
context.Request.Query["access_token"].FirstOrDefault();
|
||||
|
||||
var token = fullAuth?.TrimPrefix("Bearer ");
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
return LocalIdentity;
|
||||
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
try
|
||||
{
|
||||
var accessToken = context.Request.Query["access_token"];
|
||||
var path = context.HttpContext.Request.Path;
|
||||
// Only use query string based token on SignalR hubs
|
||||
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
|
||||
{
|
||||
context.Token = accessToken;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
var jwt = handler.ReadJwtToken(token);
|
||||
if (jwt.Issuer == iss) return OpenIdConnect;
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* Swallow */
|
||||
}
|
||||
|
||||
return LocalIdentity;
|
||||
};
|
||||
});
|
||||
|
||||
if (Configuration.OidcEnabled)
|
||||
{
|
||||
services.AddScoped<IClaimsTransformation, RolesClaimsTransformation>();
|
||||
|
||||
// TODO: Investigate on how to make this not hardcoded at startup
|
||||
auth.AddJwtBearer(OpenIdConnect, options =>
|
||||
{
|
||||
options.Authority = Configuration.OidcAuthority;
|
||||
options.Audience = Configuration.OidcClientId;
|
||||
options.RequireHttpsMetadata = options.Authority.StartsWith("https://");
|
||||
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidAudience = Configuration.OidcClientId,
|
||||
ValidIssuer = Configuration.OidcAuthority,
|
||||
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
|
||||
ValidateIssuerSigningKey = true,
|
||||
RequireExpirationTime = true,
|
||||
ValidateLifetime = true,
|
||||
RequireSignedTokens = true
|
||||
};
|
||||
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = SetTokenFromQuery,
|
||||
OnTokenValidated = OidcClaimsPrincipalConverter
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
auth.AddJwtBearer(LocalIdentity, options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"]!)),
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidIssuer = "Kavita"
|
||||
};
|
||||
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = SetTokenFromQuery
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
services.AddAuthorization(opt =>
|
||||
{
|
||||
opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole(PolicyConstants.AdminRole));
|
||||
opt.AddPolicy("RequireDownloadRole", policy => policy.RequireRole(PolicyConstants.DownloadRole, PolicyConstants.AdminRole));
|
||||
opt.AddPolicy("RequireChangePasswordRole", policy => policy.RequireRole(PolicyConstants.ChangePasswordRole, PolicyConstants.AdminRole));
|
||||
opt.AddPolicy("RequireDownloadRole",
|
||||
policy => policy.RequireRole(PolicyConstants.DownloadRole, PolicyConstants.AdminRole));
|
||||
opt.AddPolicy("RequireChangePasswordRole",
|
||||
policy => policy.RequireRole(PolicyConstants.ChangePasswordRole, PolicyConstants.AdminRole));
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static async Task OidcClaimsPrincipalConverter(TokenValidatedContext ctx)
|
||||
{
|
||||
var oidcService = ctx.HttpContext.RequestServices.GetRequiredService<IOidcService>();
|
||||
if (ctx.Principal == null) return;
|
||||
|
||||
var user = await oidcService.LoginOrCreate(ctx.Principal);
|
||||
if (user == null) return;
|
||||
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||
new(JwtRegisteredClaimNames.Name, user.UserName ?? string.Empty),
|
||||
new(ClaimTypes.Name, user.UserName ?? string.Empty)
|
||||
};
|
||||
|
||||
var userManager = ctx.HttpContext.RequestServices.GetRequiredService<UserManager<AppUser>>();
|
||||
var roles = await userManager.GetRolesAsync(user);
|
||||
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
|
||||
claims.AddRange(ctx.Principal.Claims);
|
||||
|
||||
var identity = new ClaimsIdentity(claims, ctx.Scheme.Name);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
ctx.HttpContext.User = principal;
|
||||
ctx.Principal = principal;
|
||||
|
||||
ctx.Success();
|
||||
}
|
||||
|
||||
private static Task SetTokenFromQuery(MessageReceivedContext context)
|
||||
{
|
||||
var accessToken = context.Request.Query["access_token"];
|
||||
var path = context.HttpContext.Request.Path;
|
||||
// Only use query string based token on SignalR hubs
|
||||
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs")) context.Token = accessToken;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue