initial commit
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
using System.Reflection;
|
||||
using DigitalOps.Authority.Attributes;
|
||||
using OpenHarbor.CQRS.Abstractions.Discovery;
|
||||
using OpenHarbor.CQRS.Abstractions.Security;
|
||||
using OpenHarbor.CQRS.DynamicQuery.Discover;
|
||||
|
||||
namespace DigitalOps.Authority.Services;
|
||||
|
||||
public class CQAuthorizationService(IQueryDiscovery queryDiscovery, UserIdentityService userIdentityService) : IQueryAuthorizationService, ICommandAuthorizationService
|
||||
{
|
||||
public async Task<AuthorizationResult> IsAllowedAsync(bool isQuery, Type queryOrCommandType, CancellationToken cancellationToken)
|
||||
{
|
||||
// determine subject type.
|
||||
Type subjectType = queryOrCommandType;
|
||||
if (isQuery)
|
||||
{
|
||||
var queryMeta = queryDiscovery.FindQuery(queryOrCommandType);
|
||||
if (queryMeta != null && queryMeta is DynamicQueryMeta dqMeta)
|
||||
{
|
||||
subjectType = dqMeta.DestinationType;
|
||||
}
|
||||
}
|
||||
|
||||
// allow guest calls.
|
||||
var allowGuestAttributes = subjectType.GetCustomAttribute<AllowGuestAttribute>(true);
|
||||
if (null != allowGuestAttributes)
|
||||
return AuthorizationResult.Allowed;
|
||||
|
||||
if (false == userIdentityService.IsAuthenticated())
|
||||
return AuthorizationResult.Unauthorized;
|
||||
|
||||
|
||||
var isAllowed = await userIdentityService.IsAuthorizedAsync(cancellationToken);
|
||||
return isAllowed ? AuthorizationResult.Allowed : AuthorizationResult.Forbidden;
|
||||
}
|
||||
|
||||
Task<AuthorizationResult> IQueryAuthorizationService.IsAllowedAsync(Type queryType,
|
||||
CancellationToken cancellationToken)
|
||||
=> IsAllowedAsync(true, queryType, cancellationToken);
|
||||
|
||||
Task<AuthorizationResult> ICommandAuthorizationService.IsAllowedAsync(Type commandType, CancellationToken cancellationToken)
|
||||
=> IsAllowedAsync(false, commandType, cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
using System.Security.Claims;
|
||||
using DigitalOps.Dal;
|
||||
using DigitalOps.Dal.DbEntity;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace DigitalOps.Authority.Services;
|
||||
|
||||
public class UserIdentityService
|
||||
{
|
||||
private User? _user;
|
||||
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly MainDbContext _dbContext;
|
||||
|
||||
public string? SubjectId { get; }
|
||||
public string? Email { get; }
|
||||
public string? FirstName { get; }
|
||||
public string? LastName { get; }
|
||||
public bool? EmailVerified { get; }
|
||||
public string? AuthIssuer { get; }
|
||||
|
||||
public UserIdentityService(MainDbContext dbContext, IHttpContextAccessor httpContextAccessor,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_configuration = configuration;
|
||||
|
||||
// microsoft you twats! NameIdentifier = Sub
|
||||
SubjectId = ResolveClaim(ClaimTypes.NameIdentifier);
|
||||
Email = ResolveClaim(ClaimTypes.Email);
|
||||
FirstName = ResolveClaim(ClaimTypes.GivenName);
|
||||
LastName = ResolveClaim(ClaimTypes.Surname);
|
||||
var emailVerifiedString = ResolveClaim("email_verified");
|
||||
if (null != emailVerifiedString && bool.TryParse(emailVerifiedString, out var emailVerified))
|
||||
{
|
||||
EmailVerified = emailVerified;
|
||||
}
|
||||
|
||||
AuthIssuer = ResolveClaim("iss");
|
||||
}
|
||||
|
||||
public bool IsAuthenticated()
|
||||
{
|
||||
var isAuthenticated = _httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
|
||||
|
||||
if (!isAuthenticated || null == EmailVerified)
|
||||
return false;
|
||||
|
||||
return EmailVerified ?? false;
|
||||
}
|
||||
|
||||
private string? ResolveClaim(params string[] name)
|
||||
{
|
||||
var isAuthenticated = _httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
|
||||
if (false == isAuthenticated)
|
||||
return null;
|
||||
|
||||
var claim = _httpContextAccessor.HttpContext.User.Claims.FirstOrDefault(claim => name.Contains(claim.Type));
|
||||
return claim?.Value;
|
||||
}
|
||||
|
||||
public async Task<User?> GetUserOrDefaultAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (false == IsAuthenticated())
|
||||
return null;
|
||||
|
||||
if (null != _user)
|
||||
return _user;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Email))
|
||||
return null;
|
||||
|
||||
var userOidc = await _dbContext.UserOidcs
|
||||
.AsNoTracking()
|
||||
.Include(oidc => oidc.User)
|
||||
.FirstOrDefaultAsync(oidc => oidc.Issuer == AuthIssuer && oidc.Subject == SubjectId, cancellationToken);
|
||||
|
||||
if (userOidc != null)
|
||||
{
|
||||
_user = userOidc.User;
|
||||
return _user;
|
||||
}
|
||||
|
||||
// otherwise search for the user by email
|
||||
var email = Email?.ToLower() ?? "";
|
||||
|
||||
_user = await _dbContext.Users
|
||||
.Include(user => user.UserOidcs)
|
||||
.FirstOrDefaultAsync(user => user.Email.ToLower() == email, cancellationToken);
|
||||
|
||||
// create user if it doesn't exist
|
||||
if (null == _user)
|
||||
{
|
||||
_user = await CreateUserAsync(cancellationToken);
|
||||
return _user;
|
||||
}
|
||||
|
||||
await CreateUserOidcAsync(_user, true, cancellationToken);
|
||||
return _user;
|
||||
}
|
||||
|
||||
public async Task<bool> IsAuthorizedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await GetUserOrDefaultAsync(cancellationToken);
|
||||
return null != user;
|
||||
}
|
||||
|
||||
private async Task<UserOidc> CreateUserOidcAsync(User user, bool save, CancellationToken cancellationToken)
|
||||
{
|
||||
// if user exist but oidc doesn't, we need to link the account
|
||||
// todo: security concerns: a thirdparty oidc could be use to fake email verification to hijack an account, this has to be addressed before public release
|
||||
|
||||
var mainIssuer = _configuration["JwtBearer:Authority"];
|
||||
var userOidc = new UserOidc
|
||||
{
|
||||
User = user,
|
||||
Issuer = AuthIssuer,
|
||||
Subject = SubjectId,
|
||||
VerifiedAt = (mainIssuer == AuthIssuer) ? DateTime.UtcNow : null
|
||||
};
|
||||
|
||||
_dbContext.UserOidcs.Add(userOidc);
|
||||
|
||||
if (save)
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return userOidc;
|
||||
}
|
||||
|
||||
private async Task<User> CreateUserAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Email = Email,
|
||||
FirstName = FirstName,
|
||||
LastName = LastName,
|
||||
};
|
||||
|
||||
await CreateUserOidcAsync(user, false, cancellationToken);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user