206 lines
8.2 KiB
C#
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;
|
|
}
|
|
} |