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 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 IsAuthorizedAsync(CancellationToken cancellationToken) { var user = await GetUserOrDefaultAsync(cancellationToken); return null != user; } private async Task 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 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; } }