| | | 1 | | using ArturRios.Data.Relational.Core.Interfaces; |
| | | 2 | | using ArturRios.Heimdall.Command.Services; |
| | | 3 | | using ArturRios.Heimdall.Domain.Entities; |
| | | 4 | | using ArturRios.Jwt; |
| | | 5 | | using ArturRios.Util.WebApi.Security.Interfaces; |
| | | 6 | | using Microsoft.EntityFrameworkCore; |
| | | 7 | | |
| | | 8 | | namespace ArturRios.Heimdall.WebApi.Security; |
| | | 9 | | |
| | | 10 | | /// <summary> |
| | | 11 | | /// Issues UC-11's authentication token as the application's own ES256-signed JWT (FR-AU-03), |
| | | 12 | | /// using the same <see cref="IAuthenticatedUserMapper" /> the middleware validates incoming |
| | | 13 | | /// tokens with — so a token this class issues is one the API can always read back. |
| | | 14 | | /// </summary> |
| | | 15 | | /// <remarks> |
| | | 16 | | /// The token is signed with the current key, which its <c>kid</c> names by thumbprint |
| | | 17 | | /// (<see cref="TokenSigningKeys" />), as is every other kind Heimdall issues. Expiry comes from |
| | | 18 | | /// the registered <see cref="JwtConfiguration" />, which reads it from the |
| | | 19 | | /// environment (NFR-03), and is reported to the caller so a client need not decode the token to |
| | | 20 | | /// know when to re-authenticate. At issuance the acting scope's permissions whose |
| | | 21 | | /// <see cref="ScopePermission.IncludeAsJwtClaim" /> flag is set are read from the database |
| | | 22 | | /// (UC-31…UC-35, FR-SP) and folded into the token, so a downstream caller can authorize on them. |
| | | 23 | | /// A User claims the permissions of the scope they belong to; a Scope Admin the union over the |
| | | 24 | | /// scopes they own; a System Admin none, since they belong to no scope. Logically deleted scopes |
| | | 25 | | /// and permissions are excluded, as a deleted resource is never claimed. A restricted identity |
| | | 26 | | /// is issued a subject-rights-only token instead (NFR-24), which claims none of this, under |
| | | 27 | | /// <see cref="PersonTokenAudiences.SubjectRights" /> rather than the full token's audience, so a |
| | | 28 | | /// consumer pinning the person audience refuses it (Threat Model TH-35). |
| | | 29 | | /// </remarks> |
| | 310 | 30 | | public class JwtAuthTokenIssuer( |
| | 310 | 31 | | JwtConfiguration configuration, |
| | 310 | 32 | | PersonTokenAudiences audiences, |
| | 310 | 33 | | JwtHandler jwtHandler, |
| | 310 | 34 | | IAuthenticatedUserMapper mapper, |
| | 310 | 35 | | IAsyncReadOnlyRepository<ScopePermission, long> permissionReader) : IAuthTokenIssuer |
| | | 36 | | { |
| | | 37 | | public async Task<AuthToken> IssueAsync(AuthTokenSubject subject) |
| | | 38 | | { |
| | 86 | 39 | | var permissionClaims = await LoadScopePermissionClaimsAsync(subject); |
| | | 40 | | |
| | | 41 | | // The issued token carries this login's claims; everything else — key, issuer, audience, |
| | | 42 | | // lifetime — comes from the configuration the validator also uses. |
| | 86 | 43 | | var user = new IdentityUser( |
| | 86 | 44 | | subject.PersonId, subject.RoleId, subject.ScopeId, subject.OwnedScopeIds) |
| | 86 | 45 | | { |
| | 86 | 46 | | DisplayName = subject.DisplayName, |
| | 86 | 47 | | ScopePermissionClaims = permissionClaims, |
| | 86 | 48 | | SecurityStamp = subject.SecurityStamp |
| | 86 | 49 | | }; |
| | | 50 | | |
| | 86 | 51 | | var tokenConfiguration = configuration with { Claims = mapper.ToClaims(user) }; |
| | | 52 | | |
| | 86 | 53 | | return new AuthToken( |
| | 86 | 54 | | jwtHandler.CreateToken(tokenConfiguration), |
| | 86 | 55 | | DateTime.UtcNow.AddSeconds(configuration.ExpirationInSeconds)); |
| | 86 | 56 | | } |
| | | 57 | | |
| | | 58 | | public Task<AuthToken> IssueSubjectRightsOnlyAsync(Guid subjectId, int roleId, Guid? securityStamp) |
| | | 59 | | { |
| | | 60 | | // Built from the subject and role alone: the token admits nothing a scope, an ownership or |
| | | 61 | | // a permission would matter to, so it claims none — and no display name, since it is not |
| | | 62 | | // a token any downstream service should provision a profile from. The stamp it does carry, |
| | | 63 | | // so a credential change revokes it like any other person token (FR-AU-11). |
| | 13 | 64 | | var user = new IdentityUser(subjectId, roleId) { SubjectRightsOnly = true, SecurityStamp = securityStamp }; |
| | | 65 | | |
| | 13 | 66 | | var tokenConfiguration = configuration with |
| | 13 | 67 | | { |
| | 13 | 68 | | Audience = audiences.SubjectRights, |
| | 13 | 69 | | Claims = mapper.ToClaims(user) |
| | 13 | 70 | | }; |
| | | 71 | | |
| | 13 | 72 | | return Task.FromResult(new AuthToken( |
| | 13 | 73 | | jwtHandler.CreateToken(tokenConfiguration), |
| | 13 | 74 | | DateTime.UtcNow.AddSeconds(configuration.ExpirationInSeconds))); |
| | | 75 | | } |
| | | 76 | | |
| | | 77 | | /// <summary> |
| | | 78 | | /// The distinct names of the non-deleted permissions flagged <see cref="ScopePermission.IncludeAsJwtClaim" /> |
| | | 79 | | /// across the scopes this subject acts within — <see cref="AuthTokenSubject.ScopeId" /> for a |
| | | 80 | | /// User and <see cref="AuthTokenSubject.OwnedScopeIds" /> for a Scope Admin. Empty for a |
| | | 81 | | /// System Admin, who carries no scope. |
| | | 82 | | /// </summary> |
| | | 83 | | private async Task<IReadOnlyCollection<string>> LoadScopePermissionClaimsAsync(AuthTokenSubject subject) |
| | | 84 | | { |
| | 86 | 85 | | var scopePublicIds = subject.OwnedScopeIds.ToList(); |
| | | 86 | | |
| | 86 | 87 | | if (subject.ScopeId is not null) |
| | | 88 | | { |
| | 28 | 89 | | scopePublicIds.Add(subject.ScopeId.Value); |
| | | 90 | | } |
| | | 91 | | |
| | 86 | 92 | | if (scopePublicIds.Count == 0) |
| | | 93 | | { |
| | 55 | 94 | | return []; |
| | | 95 | | } |
| | | 96 | | |
| | 31 | 97 | | return await permissionReader.Query() |
| | 31 | 98 | | .Where(permission => scopePublicIds.Contains(permission.Scope.PublicId) |
| | 31 | 99 | | && !permission.Scope.IsDeleted |
| | 31 | 100 | | && !permission.IsDeleted |
| | 31 | 101 | | && permission.IncludeAsJwtClaim) |
| | 31 | 102 | | .Select(permission => permission.Name) |
| | 31 | 103 | | .Distinct() |
| | 31 | 104 | | .ToListAsync(); |
| | 86 | 105 | | } |
| | | 106 | | } |