- Added ApiClientModel and ApiTokenModel for managing API clients and tokens. - Introduced ConfigurationDefinitionModel and ConfigurationValueModel for configuration management. - Created CredentialSecretModel for storing credential secrets. - Developed DeploymentArtifactModel and DeploymentBatchModel for deployment management. - Enhanced DeploymentTargetModel and DeploymentTemplateSelectionModel to support template revisions. - Added TemplateRevisionModel and TemplateVersionModel for versioning templates. - Implemented ApiClientSecretHasher for secure secret hashing. - Created ApiTokenService for generating and validating JWT tokens. - Updated QueueJobService to handle deployment requests with artifacts. - Configured authentication settings in appsettings.json for JWT and Negotiate authentication.
163 lines
5.7 KiB
C#
163 lines
5.7 KiB
C#
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<int?>("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<string> scopes)
|
|
{
|
|
return CreateAccessToken(client.ClientId, client.ClientId, scopes);
|
|
}
|
|
|
|
public ApiTokenCreateResult CreateAccessToken(string subject, string? clientId, IReadOnlyCollection<string> 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<Claim>
|
|
{
|
|
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<string> ReadClientScopes(ApiClientModel client)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(client.ScopesJson))
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
return JsonSerializer.Deserialize<string[]>(client.ScopesJson) ?? Array.Empty<string>();
|
|
}
|
|
|
|
public IReadOnlyCollection<string> 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<string> 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<string> ReadAllowedTokenScopes()
|
|
{
|
|
var configuredScopes = _configuration.GetSection("Authentication:OnPremClientCredentials:AllowedTokenScopes").Get<string[]>();
|
|
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));
|
|
}
|
|
}
|
|
}
|