- Add ReactivatePipeline to handle subscription reactivation, including scaling Docker services, health verification, status updates, audit logging, and broadcasting status changes. - Introduce RotateCredentialsPipeline for OAuth2 credential rotation, managing the deletion of old apps, creation of new ones, credential storage, access verification, and audit logging. - Create StepRunner to manage job step execution, including lifecycle management and progress broadcasting via SignalR. - Implement SuspendPipeline for subscription suspension, scaling down services, updating statuses, logging audits, and broadcasting changes. - Add UpdateScreenLimitPipeline to update Xibo CMS screen limits and record snapshots. - Introduce XiboFeatureManifests for hardcoded feature ACLs per role. - Add docker-compose.dev.yml for local development with PostgreSQL setup.
103 lines
3.4 KiB
C#
103 lines
3.4 KiB
C#
using System.IdentityModel.Tokens.Jwt;
|
|
using System.Security.Claims;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Options;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using OTSSignsOrchestrator.Server.Data;
|
|
using OTSSignsOrchestrator.Server.Data.Entities;
|
|
|
|
namespace OTSSignsOrchestrator.Server.Auth;
|
|
|
|
public class OperatorAuthService
|
|
{
|
|
private readonly OrchestratorDbContext _db;
|
|
private readonly JwtOptions _jwt;
|
|
private readonly ILogger<OperatorAuthService> _logger;
|
|
|
|
public OperatorAuthService(
|
|
OrchestratorDbContext db,
|
|
IOptions<JwtOptions> jwt,
|
|
ILogger<OperatorAuthService> logger)
|
|
{
|
|
_db = db;
|
|
_jwt = jwt.Value;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<(string Jwt, string RefreshToken)> LoginAsync(string email, string password)
|
|
{
|
|
var op = await _db.Operators.FirstOrDefaultAsync(
|
|
o => o.Email == email.Trim().ToLowerInvariant());
|
|
|
|
if (op is null || !BCrypt.Net.BCrypt.Verify(password, op.PasswordHash))
|
|
{
|
|
_logger.LogWarning("Login failed for {Email}", email);
|
|
throw new UnauthorizedAccessException("Invalid email or password.");
|
|
}
|
|
|
|
_logger.LogInformation("Operator {Email} logged in", op.Email);
|
|
var jwt = GenerateJwt(op);
|
|
var refresh = await CreateRefreshTokenAsync(op.Id);
|
|
return (jwt, refresh);
|
|
}
|
|
|
|
public async Task<string> RefreshAsync(string refreshToken)
|
|
{
|
|
var token = await _db.RefreshTokens
|
|
.Include(r => r.Operator)
|
|
.FirstOrDefaultAsync(r => r.Token == refreshToken);
|
|
|
|
if (token is null || token.RevokedAt is not null || token.ExpiresAt < DateTime.UtcNow)
|
|
throw new UnauthorizedAccessException("Invalid or expired refresh token.");
|
|
|
|
// Revoke the used token (single-use rotation)
|
|
token.RevokedAt = DateTime.UtcNow;
|
|
await _db.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("Refresh token used for operator {Email}", token.Operator.Email);
|
|
return GenerateJwt(token.Operator);
|
|
}
|
|
|
|
private string GenerateJwt(Operator op)
|
|
{
|
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwt.Key));
|
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
|
|
|
var claims = new[]
|
|
{
|
|
new Claim(JwtRegisteredClaimNames.Sub, op.Id.ToString()),
|
|
new Claim(JwtRegisteredClaimNames.Email, op.Email),
|
|
new Claim(ClaimTypes.Name, op.Email),
|
|
new Claim(ClaimTypes.Role, op.Role.ToString()),
|
|
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
|
};
|
|
|
|
var token = new JwtSecurityToken(
|
|
issuer: _jwt.Issuer,
|
|
audience: _jwt.Audience,
|
|
claims: claims,
|
|
expires: DateTime.UtcNow.AddMinutes(15),
|
|
signingCredentials: creds);
|
|
|
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
|
}
|
|
|
|
private async Task<string> CreateRefreshTokenAsync(Guid operatorId)
|
|
{
|
|
var tokenValue = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
|
|
|
|
_db.RefreshTokens.Add(new RefreshToken
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
OperatorId = operatorId,
|
|
Token = tokenValue,
|
|
ExpiresAt = DateTime.UtcNow.AddDays(7),
|
|
});
|
|
|
|
await _db.SaveChangesAsync();
|
|
return tokenValue;
|
|
}
|
|
}
|