using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using System.Text.Json; using Microsoft.IdentityModel.Tokens; using Microsoft.SelfService.Portal.Core.API.Models; namespace Microsoft.SelfService.Portal.Core.API.Services { public class ApiTokenService { private readonly IConfiguration _configuration; public ApiTokenService(IConfiguration configuration) { _configuration = configuration; } public int TokenLifetimeSeconds => _configuration.GetValue("Authentication:OnPremClientCredentials:TokenLifetimeSeconds") ?? 3600; public string Issuer => _configuration["Authentication:OnPremClientCredentials:Issuer"] ?? "Microsoft.SelfService.Portal.Core.API"; public string Audience => _configuration["Authentication:OnPremClientCredentials:Audience"] ?? "Microsoft.SelfService.Portal.Core.API"; public ApiTokenCreateResult CreateAccessToken(ApiClientModel client, IReadOnlyCollection scopes) { return CreateAccessToken(client.ClientId, client.ClientId, scopes); } public ApiTokenCreateResult CreateAccessToken(string subject, string? clientId, IReadOnlyCollection scopes) { var signingKey = GetSigningKey(); var credentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); var now = DateTime.UtcNow; var jti = Guid.NewGuid().ToString("N"); var expiresAt = now.AddSeconds(TokenLifetimeSeconds); var claims = new List { new(JwtRegisteredClaimNames.Sub, subject), new(JwtRegisteredClaimNames.Jti, jti) }; if (!string.IsNullOrWhiteSpace(clientId)) { claims.Add(new Claim("client_id", clientId)); } foreach (var scope in scopes) { claims.Add(new Claim("scope", scope)); } var token = new JwtSecurityToken( issuer: Issuer, audience: Audience, claims: claims, notBefore: now, expires: expiresAt, signingCredentials: credentials); return new ApiTokenCreateResult { AccessToken = new JwtSecurityTokenHandler().WriteToken(token), Jti = jti, IssuedAt = now, ExpiresAt = expiresAt }; } public IReadOnlyCollection ReadClientScopes(ApiClientModel client) { if (string.IsNullOrWhiteSpace(client.ScopesJson)) { return Array.Empty(); } return JsonSerializer.Deserialize(client.ScopesJson) ?? Array.Empty(); } public IReadOnlyCollection ResolveRequestedScopes(ApiClientModel client, string? requestedScope) { var allowedScopes = ReadClientScopes(client); if (string.IsNullOrWhiteSpace(requestedScope)) { return allowedScopes; } var requestedScopes = requestedScope .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); if (requestedScopes.Any(scope => !allowedScopes.Contains(scope, StringComparer.OrdinalIgnoreCase))) { throw new InvalidOperationException("Requested scope is not allowed for this client."); } return requestedScopes; } public IReadOnlyCollection ResolveRequestedUserScopes(string? requestedScope) { if (string.IsNullOrWhiteSpace(requestedScope)) { throw new InvalidOperationException("At least one scope is required."); } var allowedScopes = ReadAllowedTokenScopes(); var requestedScopes = requestedScope .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); if (requestedScopes.Length == 0) { throw new InvalidOperationException("At least one scope is required."); } if (requestedScopes.Any(scope => !allowedScopes.Contains(scope, StringComparer.OrdinalIgnoreCase))) { throw new InvalidOperationException("Requested scope is not allowed."); } return requestedScopes; } public IReadOnlyCollection ReadAllowedTokenScopes() { var configuredScopes = _configuration.GetSection("Authentication:OnPremClientCredentials:AllowedTokenScopes").Get(); if (configuredScopes != null && configuredScopes.Length > 0) { return configuredScopes; } return [ "deployment.read", "deployment.write", "queue.process", "template.read", "credential.resolve", "token.manage", "token.admin" ]; } public SymmetricSecurityKey GetSigningKey() { var signingKey = _configuration["Authentication:OnPremClientCredentials:SigningKey"]; if (string.IsNullOrWhiteSpace(signingKey)) { signingKey = "DevelopmentOnly-OnPremClientCredentials-SigningKey-ChangeMe"; } return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)); } } }