feat: Implement API client and token models with hashing and JWT authentication
- 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.
This commit is contained in:
162
Services/ApiTokenService.cs
Normal file
162
Services/ApiTokenService.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user