Files
Swarm/DysonNetwork.Pass/Features/Auth/Services/OidcService.cs

145 lines
4.8 KiB
C#

using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Json;
using System.Text.Json;
using DysonNetwork.Common.Models;
using DysonNetwork.Pass.Data;
using DysonNetwork.Pass.Features.Auth.Interfaces;
using DysonNetwork.Pass.Features.Auth.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
namespace DysonNetwork.Pass.Features.Auth.Services;
public class OidcService : IOidcService
{
protected readonly IConfiguration _configuration;
protected readonly IHttpClientFactory _httpClientFactory;
protected readonly PassDatabase _db;
protected readonly IAuthenticationService _authService;
protected readonly ILogger<OidcService> _logger;
public OidcService(
IConfiguration configuration,
IHttpClientFactory httpClientFactory,
PassDatabase db,
IAuthenticationService authService,
ILogger<OidcService> logger)
{
_configuration = configuration;
_httpClientFactory = httpClientFactory;
_db = db;
_authService = authService;
_logger = logger;
}
public virtual string GetAuthorizationUrl(string state, string nonce)
{
throw new NotImplementedException("This method should be implemented by derived classes");
}
public virtual async Task<OidcUserInfo> ProcessCallbackAsync(OidcCallbackData callbackData)
{
throw new NotImplementedException("This method should be implemented by derived classes");
}
public virtual async Task<AuthResult> AuthenticateAsync(string provider, string code, string state)
{
try
{
var userInfo = await ProcessCallbackAsync(new OidcCallbackData
{
Code = code,
State = state
});
// Find or create user based on the OIDC subject and provider
var account = await FindOrCreateUser(userInfo, provider);
// Create authentication result
return await _authService.AuthenticateWithOidcAsync(provider, code, state);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during OIDC authentication");
return new AuthResult
{
Success = false,
Error = "Authentication failed. Please try again."
};
}
}
public virtual IEnumerable<string> GetSupportedProviders()
{
var section = _configuration.GetSection("Oidc");
return section.GetChildren().Select(x => x.Key);
}
protected virtual async Task<Account> FindOrCreateUser(OidcUserInfo userInfo, string provider)
{
// Check if user exists with this provider and subject
var user = await _db.Accounts
.FirstOrDefaultAsync(u => u.ExternalLogins.Any(ul =>
ul.Provider == provider &&
ul.ProviderSubjectId == userInfo.Subject));
if (user != null)
return user;
// If user doesn't exist, create a new one
user = new Account
{
Id = Guid.NewGuid(),
Username = userInfo.PreferredUsername ?? userInfo.Email?.Split('@')[0] ?? Guid.NewGuid().ToString(),
Email = userInfo.Email,
EmailVerified = userInfo.EmailVerified ?? false,
CreatedAt = SystemClock.Instance.GetCurrentInstant()
};
// Add external login
user.ExternalLogins.Add(new ExternalLogin
{
Id = Guid.NewGuid(),
Provider = provider,
ProviderSubjectId = userInfo.Subject,
CreatedAt = SystemClock.Instance.GetCurrentInstant()
});
await _db.Accounts.AddAsync(user);
await _db.SaveChangesAsync();
return user;
}
protected virtual async Task<JwtSecurityToken> ValidateIdToken(string token, string issuer, string audience, string signingKey)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey));
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = issuer,
ValidateAudience = true,
ValidAudience = audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}, out var validatedToken);
return (JwtSecurityToken)validatedToken;
}
protected virtual async Task<T?> GetFromDiscoveryDocumentAsync<T>(string url)
{
var client = _httpClientFactory.CreateClient();
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(content);
}
}