Backend/e-suite.API/eSuite.API/SingleSignOn/SingleSignOn.cs
2026-01-20 21:50:10 +00:00

206 lines
8.2 KiB
C#

using System.IdentityModel.Tokens.Jwt;
using System.Text;
using e_suite.API.Common.exceptions;
using e_suite.API.Common.extensions;
using e_suite.Database.Core.Tables.UserManager;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
namespace eSuite.API.SingleSignOn;
/// <summary>
/// Internal class used to manage the single sign on process
/// </summary>
public class SingleSignOn : ISingleSignOn
{
private readonly e_suite.API.Common.IUserManager _userManager;
private readonly IConfiguration _configuration;
private readonly IHttpClientFacade _httpClientFacade;
/// <summary>
/// Default constructor used for DI
/// </summary>
/// <param name="userManager"></param>
/// <param name="configuration"></param>
/// <param name="httpClientFacade"></param>
public SingleSignOn(e_suite.API.Common.IUserManager userManager, IConfiguration configuration, IHttpClientFacade httpClientFacade)
{
_userManager = userManager;
_configuration = configuration;
_httpClientFacade = httpClientFacade;
}
private readonly string _responseType = "code";
private readonly string _scope = "openid";
private string GetRedirectUri(SsoProvider ssoProvider)
{
var redirectUri = _configuration.GetConfigValue("BASE_URL", "baseUrl", "http://localhost:3000");
return $"{redirectUri!.TrimEnd('/')}/account/auth/{ssoProvider.Id}";
}
/// <summary>
/// Use an e-mail address to find the appropriate sso provider and start the single sign on process.
/// </summary>
/// <param name="loginEmail"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<string> StartSingleSignOn(string loginEmail, CancellationToken cancellationToken)
{
SsoProvider? ssoProvider;
try
{
ssoProvider = await GetSsoProviderByEmail(loginEmail, cancellationToken);
}
catch (NotFoundException)
{
ssoProvider = null;
}
return BuildAuthorizationUrl(ssoProvider);
}
/// <summary>
/// Use a known sso provider id to start the single sign on process.
/// </summary>
/// <param name="ssoProviderId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="NotFoundException"></exception>
public async Task<string> StartSingleSignOn(long ssoProviderId, CancellationToken cancellationToken)
{
var ssoProvider = await _userManager.GetSsoProviderById(ssoProviderId, cancellationToken) ??
throw new NotFoundException("SSO Provider not found");
return BuildAuthorizationUrl(ssoProvider);
}
private string BuildAuthorizationUrl(SsoProvider? ssoProvider)
{
if (ssoProvider == null)
return string.Empty;
var authorizationUrl =
$"{ssoProvider.AuthorizationEndpoint}?response_type={_responseType}&client_id={ssoProvider.ClientId}&redirect_uri={GetRedirectUri(ssoProvider)}&scope={_scope}";
return authorizationUrl;
}
private async Task<SsoProvider?> GetSsoProviderByEmail(string loginEmail, CancellationToken cancellationToken)
{
var ssoProvider = await _userManager.GetSsoProviderForEmail(loginEmail, cancellationToken) ?? throw new NotFoundException("SSO Provider Not Found");
return ssoProvider;
}
/// <summary>
/// Exchange the Authorisation Token for a Certified JWT and extract the Subject
/// </summary>
/// <param name="ssoProviderId"></param>
/// <param name="code"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="NotFoundException"></exception>
public async Task<string> ExchangeAuthorisationToken(long ssoProviderId, string code, CancellationToken cancellationToken)
{
var ssoProvider = await _userManager.GetSsoProviderById(ssoProviderId, cancellationToken) ?? throw new NotFoundException("SSO Provider Not Found");
var formFields = new Dictionary<string, string>
{
{ "grant_type", "authorization_code" },
{ "code", code },
{ "redirect_uri", GetRedirectUri(ssoProvider) },
{ "client_id", ssoProvider.ClientId },
{ "client_secret", ssoProvider.ClientSecret }
};
using var request = new HttpRequestMessage(HttpMethod.Post, ssoProvider.TokenEndpoint);
request.Content = new FormUrlEncodedContent(formFields);
using var response = await _httpClientFacade.SendAsync(request, cancellationToken);
var responseString = await response.Content.ReadAsStringAsync(cancellationToken);
var token = ExtractAccessTokenFromResponse(responseString);
var ssoUserId = await ExtractSubjectFromJwt(ssoProvider, token, cancellationToken);
return ssoUserId;
}
private async Task<string> ExtractSubjectFromJwt(SsoProvider ssoProvider, string token, CancellationToken cancellationToken)
{
var validatedToken = await ValidateToken(ssoProvider, token, cancellationToken);
return validatedToken.Subject;
}
/// <summary>
///
/// </summary>
public string? ManualKey { get; set; } = null;
private async Task<JwtSecurityToken> ValidateToken(SsoProvider ssoProvider, string token, CancellationToken cancellationToken)
{
var securityToken = new JwtSecurityToken(token);
SecurityKey? key = await GetSecurityKey(ssoProvider, securityToken, cancellationToken);
if (key == null)
{
if (ManualKey != null)
key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(ManualKey));
}
var tvp = new TokenValidationParameters
{
ValidateActor = false, // check the profile ID
ValidateAudience = !string.IsNullOrWhiteSpace(ssoProvider.ClientId),
ValidAudience = ssoProvider.ClientId,
ValidateIssuer = true,
ValidIssuer = ssoProvider.ValidIssuer,
ValidateIssuerSigningKey = key != null,
IssuerSigningKey = key,
RequireSignedTokens = key != null,
ValidateLifetime = true,
RequireExpirationTime = true
};
var handler = new JwtSecurityTokenHandler();
handler.ValidateToken(token, tvp, out var validatedToken);
if (validatedToken is JwtSecurityToken jwtSecurityToken)
{
return jwtSecurityToken;
}
throw new ArgumentOutOfRangeException( nameof(token), "Invalid security token.");
}
private async Task<JsonWebKey> GetSecurityKey(SsoProvider ssoProvider, JwtSecurityToken securityToken, CancellationToken cancellationToken)
{
var certificates = await GetCertificateDictionary(ssoProvider, cancellationToken);
if (certificates.Count == 0)
return null!;
var cert = certificates[securityToken.Header.Kid];
return cert;
}
private async Task<Dictionary<string, JsonWebKey>> GetCertificateDictionary(SsoProvider ssoProvider, CancellationToken cancellationToken)
{
var openConfigurationUri =
new Uri($"{ssoProvider.ValidIssuer.TrimEnd('/')}/.well-known/openid-configuration");
var configurationJson = await _httpClientFacade.GetStringAsync(openConfigurationUri, cancellationToken);
var openIdConfiguration = JsonConvert.DeserializeObject<OpenIdConfiguration>(configurationJson) ??
throw new NullReferenceException();
var jwks = await _httpClientFacade.GetStringAsync(new Uri(openIdConfiguration.JwksUri), cancellationToken);
var openIdKeys = JsonConvert.DeserializeObject<OpenIdKeys>(jwks)!;
return openIdKeys.Keys.ToDictionary(key => key.Kid);
}
private static string ExtractAccessTokenFromResponse(string responseString)
{
var responseJson = JsonConvert.DeserializeObject<OpenIdResponse>(responseString) ?? throw new NullReferenceException();
return responseJson.IdToken;
}
}