| | | 1 | | using System.Security.Cryptography; |
| | | 2 | | using System.Text; |
| | | 3 | | using Elsa.Common; |
| | | 4 | | using Elsa.Common.Multitenancy; |
| | | 5 | | using Elsa.Extensions; |
| | | 6 | | using Elsa.ExternalAuthentication.Contracts; |
| | | 7 | | using Elsa.ExternalAuthentication.Models; |
| | | 8 | | using Elsa.Identity.Contracts; |
| | | 9 | | using Elsa.Identity.Models; |
| | | 10 | | |
| | | 11 | | namespace Elsa.ExternalAuthentication.Services; |
| | | 12 | | |
| | 2 | 13 | | public sealed class DefaultExternalAuthenticationTokenIssuer( |
| | 2 | 14 | | IExternalAuthenticationSessionStore sessionStore, |
| | 2 | 15 | | IIdentityProviderConnectionRegistry connectionRegistry, |
| | 2 | 16 | | IEnumerable<ISecretBindingResolver> secretBindingResolvers, |
| | 2 | 17 | | IUserProvider userProvider, |
| | 2 | 18 | | IRoleProvider roleProvider, |
| | 2 | 19 | | IElsaTokenService tokenService, |
| | 2 | 20 | | ITenantAccessor tenantAccessor, |
| | 2 | 21 | | ISystemClock clock) : IExternalAuthenticationTokenIssuer |
| | | 22 | | { |
| | | 23 | | public async ValueTask<ExternalTokenResponse> IssueAsync(ExternalAuthenticationSession session, CancellationToken ca |
| | | 24 | | { |
| | 2 | 25 | | var refreshToken = CreateRefreshToken(session.Id); |
| | 2 | 26 | | session.CurrentRefreshTokenHash = Hash(refreshToken); |
| | 2 | 27 | | await sessionStore.SaveAsync(session, cancellationToken); |
| | 2 | 28 | | return await IssueResponseAsync(session, refreshToken, cancellationToken); |
| | 2 | 29 | | } |
| | | 30 | | |
| | | 31 | | public async ValueTask<ExternalTokenResponse> RefreshAsync(string clientId, SensitiveString refreshToken, Cancellati |
| | | 32 | | { |
| | 3 | 33 | | var rawToken = refreshToken.Reveal(); |
| | 3 | 34 | | var separator = rawToken.IndexOf('.', StringComparison.Ordinal); |
| | 3 | 35 | | if (separator <= 0 || separator == rawToken.Length - 1) |
| | 0 | 36 | | throw new InvalidOperationException("The external refresh token is invalid."); |
| | 3 | 37 | | var sessionId = rawToken[..separator]; |
| | 3 | 38 | | var currentHash = Hash(rawToken); |
| | 3 | 39 | | var session = await sessionStore.FindByIdAsync(sessionId, cancellationToken); |
| | 3 | 40 | | if (session is null || !string.Equals(session.AuthenticationClientId, clientId, StringComparison.Ordinal)) |
| | 0 | 41 | | throw new InvalidOperationException("The external refresh token is invalid."); |
| | 3 | 42 | | var connection = await connectionRegistry.FindByKeyAsync(session.TenantId, session.ConnectionKey, cancellationTo |
| | 3 | 43 | | if (session.RevokedAt != null || session.ExpiresAt <= clock.UtcNow || connection is null || connection.IsShadowe |
| | 0 | 44 | | throw new InvalidOperationException("The external authentication session is no longer valid."); |
| | 3 | 45 | | if (!string.Equals(session.SecretGenerationFingerprint, await GetSecretFingerprintAsync(connection.Connection.Se |
| | 0 | 46 | | throw new InvalidOperationException("The external authentication session secrets changed."); |
| | | 47 | | |
| | 3 | 48 | | var nextToken = CreateRefreshToken(session.Id); |
| | 3 | 49 | | var rotation = await sessionStore.TryRotateRefreshTokenAsync(session.Id, currentHash, session.RefreshGeneration, |
| | 3 | 50 | | if (rotation is not ExternalAuthenticationSessionRotationResult.Rotated { Session: var rotated }) |
| | 1 | 51 | | throw new InvalidOperationException("The external refresh token cannot be used."); |
| | | 52 | | |
| | 2 | 53 | | return await IssueResponseAsync(rotated, nextToken, cancellationToken); |
| | 2 | 54 | | } |
| | | 55 | | |
| | | 56 | | private async ValueTask<ExternalTokenResponse> IssueResponseAsync(ExternalAuthenticationSession session, string refr |
| | | 57 | | { |
| | 4 | 58 | | using var tenantContext = tenantAccessor.PushContext(new Tenant { Id = session.TenantId, Name = session.TenantId |
| | 4 | 59 | | var user = await userProvider.FindAsync(new UserFilter { Id = session.UserId }, cancellationToken) |
| | 4 | 60 | | ?? throw new InvalidOperationException("The external authentication session user no longer exists."); |
| | 4 | 61 | | var roles = (await roleProvider.FindByIdsAsync(user.Roles, cancellationToken)).ToArray(); |
| | 8 | 62 | | var permissions = roles.SelectMany(x => x.Permissions).Concat(session.ExternalGrants.Select(x => x.Permission)). |
| | 6 | 63 | | var accessToken = await tokenService.IssueAccessTokenAsync(new TokenIssuanceContext(user, roles.Select(x => x.Na |
| | 4 | 64 | | var now = clock.UtcNow; |
| | 4 | 65 | | return new ExternalTokenResponse( |
| | 4 | 66 | | accessToken.Token, |
| | 4 | 67 | | "Bearer", |
| | 4 | 68 | | Math.Max(0, (long)(accessToken.ExpiresAt - now).TotalSeconds), |
| | 4 | 69 | | refreshToken, |
| | 4 | 70 | | Math.Max(0, (long)(session.RefreshExpiresAt - now).TotalSeconds), |
| | 4 | 71 | | Math.Max(0, (long)(session.ExpiresAt - now).TotalSeconds)); |
| | 4 | 72 | | } |
| | | 73 | | |
| | 5 | 74 | | private static string CreateRefreshToken(string sessionId) => $"{sessionId}.{Convert.ToBase64String(RandomNumberGene |
| | 8 | 75 | | private static string Hash(string token) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token))); |
| | | 76 | | |
| | | 77 | | private async ValueTask<string> GetSecretFingerprintAsync(IDictionary<string, SecretBinding> bindings, CancellationT |
| | | 78 | | { |
| | 3 | 79 | | var fingerprints = new List<string>(); |
| | 6 | 80 | | foreach (var (name, binding) in bindings) |
| | | 81 | | { |
| | 0 | 82 | | var resolver = secretBindingResolvers.FirstOrDefault(x => string.Equals(x.Type, binding.ResolverType, String |
| | 0 | 83 | | ?? throw new InvalidOperationException("A required secret binding resolver is unavailable."); |
| | 0 | 84 | | var resolved = await resolver.ResolveAsync(binding, cancellationToken); |
| | 0 | 85 | | fingerprints.Add($"{name}:{resolved.GenerationFingerprint}"); |
| | 0 | 86 | | resolved.Value.Dispose(); |
| | 0 | 87 | | } |
| | 3 | 88 | | return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(string.Join("\n", fingerprints.OrderBy(x => x, |
| | 3 | 89 | | } |
| | | 90 | | } |