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; /// /// Internal class used to manage the single sign on process /// public class SingleSignOn : ISingleSignOn { private readonly e_suite.API.Common.IUserManager _userManager; private readonly IConfiguration _configuration; private readonly IHttpClientFacade _httpClientFacade; /// /// Default constructor used for DI /// /// /// /// 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}"; } /// /// Use an e-mail address to find the appropriate sso provider and start the single sign on process. /// /// /// /// public async Task StartSingleSignOn(string loginEmail, CancellationToken cancellationToken) { SsoProvider? ssoProvider; try { ssoProvider = await GetSsoProviderByEmail(loginEmail, cancellationToken); } catch (NotFoundException) { ssoProvider = null; } return BuildAuthorizationUrl(ssoProvider); } /// /// Use a known sso provider id to start the single sign on process. /// /// /// /// /// public async Task 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 GetSsoProviderByEmail(string loginEmail, CancellationToken cancellationToken) { var ssoProvider = await _userManager.GetSsoProviderForEmail(loginEmail, cancellationToken) ?? throw new NotFoundException("SSO Provider Not Found"); return ssoProvider; } /// /// Exchange the Authorisation Token for a Certified JWT and extract the Subject /// /// /// /// /// /// public async Task 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 { { "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 ExtractSubjectFromJwt(SsoProvider ssoProvider, string token, CancellationToken cancellationToken) { var validatedToken = await ValidateToken(ssoProvider, token, cancellationToken); return validatedToken.Subject; } /// /// /// public string? ManualKey { get; set; } = null; private async Task 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 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> 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(configurationJson) ?? throw new NullReferenceException(); var jwks = await _httpClientFacade.GetStringAsync(new Uri(openIdConfiguration.JwksUri), cancellationToken); var openIdKeys = JsonConvert.DeserializeObject(jwks)!; return openIdKeys.Keys.ToDictionary(key => key.Kid); } private static string ExtractAccessTokenFromResponse(string responseString) { var responseJson = JsonConvert.DeserializeObject(responseString) ?? throw new NullReferenceException(); return responseJson.IdToken; } }