Decouple SGU authentication from profile pages
This commit is contained in:
@@ -15,7 +15,9 @@ public sealed class NtlmCredentialValidator(BrokerOptions options) : INtlmCreden
|
||||
string password,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Uri current = GetProfileUri(identity.Role);
|
||||
Uri authenticationUri = new(
|
||||
new Uri(options.Endpoint, UriKind.Absolute),
|
||||
options.AuthenticationPath);
|
||||
HashSet<string> allowedHosts = new(
|
||||
this.options.AllowedRedirectHosts,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
@@ -50,79 +52,28 @@ public sealed class NtlmCredentialValidator(BrokerOptions options) : INtlmCreden
|
||||
|
||||
try
|
||||
{
|
||||
for (int hop = 0; hop <= this.options.MaxRedirects; hop++)
|
||||
NtlmValidationResult? authenticationFailure = await ValidateCredentialsAsync(
|
||||
client,
|
||||
authenticationUri,
|
||||
allowedHosts,
|
||||
credentialCache,
|
||||
credentialedAuthorities,
|
||||
credential,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (authenticationFailure is not null)
|
||||
{
|
||||
if (!IsAllowedHttpsUri(current, allowedHosts))
|
||||
{
|
||||
return NtlmValidationResult.Unavailable("NTLM_REDIRECT_REJECTED");
|
||||
}
|
||||
|
||||
string authority = current.GetLeftPart(UriPartial.Authority);
|
||||
if (credentialedAuthorities.Add(authority))
|
||||
{
|
||||
credentialCache.Add(new Uri(authority + "/"), "NTLM", credential);
|
||||
}
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, current);
|
||||
using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeout.CancelAfter(TimeSpan.FromSeconds(this.options.TimeoutSeconds));
|
||||
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
{
|
||||
response = await client
|
||||
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeout.Token)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return NtlmValidationResult.Unavailable("NTLM_TIMEOUT");
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return NtlmValidationResult.Unavailable();
|
||||
}
|
||||
|
||||
using (response)
|
||||
{
|
||||
if (response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||
{
|
||||
return NtlmValidationResult.Invalid();
|
||||
}
|
||||
|
||||
int statusCode = (int)response.StatusCode;
|
||||
if (statusCode >= 500)
|
||||
{
|
||||
return NtlmValidationResult.Unavailable("NTLM_UPSTREAM_ERROR");
|
||||
}
|
||||
|
||||
if (statusCode is >= 300 and < 400)
|
||||
{
|
||||
Uri? location = response.Headers.Location;
|
||||
if (location is null)
|
||||
{
|
||||
return NtlmValidationResult.Unavailable("NTLM_INVALID_REDIRECT");
|
||||
}
|
||||
|
||||
current = location.IsAbsoluteUri ? location : new Uri(current, location);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statusCode is >= 200 and < 300)
|
||||
{
|
||||
InstitutionalProfile? profile = await TryReadProfileAsync(
|
||||
response,
|
||||
identity,
|
||||
timeout.Token,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return NtlmValidationResult.Valid(profile);
|
||||
}
|
||||
|
||||
return NtlmValidationResult.Invalid();
|
||||
}
|
||||
return authenticationFailure;
|
||||
}
|
||||
|
||||
return NtlmValidationResult.Unavailable("NTLM_REDIRECT_LIMIT");
|
||||
InstitutionalProfile? profile = await TryFetchProfileAsync(
|
||||
client,
|
||||
identity,
|
||||
allowedHosts,
|
||||
credentialCache,
|
||||
credentialedAuthorities,
|
||||
credential,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return NtlmValidationResult.Valid(profile);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -130,6 +81,154 @@ public sealed class NtlmCredentialValidator(BrokerOptions options) : INtlmCreden
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<NtlmValidationResult?> ValidateCredentialsAsync(
|
||||
HttpClient client,
|
||||
Uri authenticationUri,
|
||||
HashSet<string> allowedHosts,
|
||||
CredentialCache credentialCache,
|
||||
HashSet<string> credentialedAuthorities,
|
||||
NetworkCredential credential,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!IsAllowedHttpsUri(authenticationUri, allowedHosts))
|
||||
{
|
||||
return NtlmValidationResult.Unavailable("NTLM_AUTH_ENDPOINT_REJECTED");
|
||||
}
|
||||
|
||||
AddCredential(
|
||||
authenticationUri,
|
||||
credentialCache,
|
||||
credentialedAuthorities,
|
||||
credential);
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, authenticationUri);
|
||||
using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeout.CancelAfter(TimeSpan.FromSeconds(options.TimeoutSeconds));
|
||||
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
{
|
||||
response = await client
|
||||
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeout.Token)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return NtlmValidationResult.Unavailable("NTLM_TIMEOUT");
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return NtlmValidationResult.Unavailable();
|
||||
}
|
||||
|
||||
using (response)
|
||||
{
|
||||
if (response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||
{
|
||||
return NtlmValidationResult.Invalid();
|
||||
}
|
||||
|
||||
int statusCode = (int)response.StatusCode;
|
||||
if (statusCode is >= 200 and < 300)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (statusCode is >= 300 and < 400)
|
||||
{
|
||||
Uri? location = response.Headers.Location;
|
||||
Uri? redirect = location is null
|
||||
? null
|
||||
: location.IsAbsoluteUri ? location : new Uri(authenticationUri, location);
|
||||
return redirect is not null && IsAllowedHttpsUri(redirect, allowedHosts)
|
||||
? null
|
||||
: NtlmValidationResult.Unavailable("NTLM_AUTH_REDIRECT_REJECTED");
|
||||
}
|
||||
|
||||
return statusCode is 429 or >= 500
|
||||
? NtlmValidationResult.Unavailable("NTLM_UPSTREAM_ERROR")
|
||||
: NtlmValidationResult.Unavailable("NTLM_UNEXPECTED_RESPONSE");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<InstitutionalProfile?> TryFetchProfileAsync(
|
||||
HttpClient client,
|
||||
UserIdentity identity,
|
||||
HashSet<string> allowedHosts,
|
||||
CredentialCache credentialCache,
|
||||
HashSet<string> credentialedAuthorities,
|
||||
NetworkCredential credential,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri current = GetProfileUri(identity.Role);
|
||||
using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeout.CancelAfter(TimeSpan.FromSeconds(options.ProfileTimeoutSeconds));
|
||||
|
||||
for (int hop = 0; hop <= options.MaxRedirects; hop++)
|
||||
{
|
||||
if (!IsAllowedHttpsUri(current, allowedHosts))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
AddCredential(current, credentialCache, credentialedAuthorities, credential);
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, current);
|
||||
using HttpResponseMessage response = await client
|
||||
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeout.Token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
int statusCode = (int)response.StatusCode;
|
||||
if (statusCode is >= 300 and < 400)
|
||||
{
|
||||
Uri? location = response.Headers.Location;
|
||||
if (location is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
current = location.IsAbsoluteUri ? location : new Uri(current, location);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statusCode is >= 200 and < 300)
|
||||
{
|
||||
return await TryReadProfileAsync(
|
||||
response,
|
||||
identity,
|
||||
timeout.Token,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Enrichment is optional once the lightweight NTLM endpoint has
|
||||
// authoritatively accepted the credentials.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void AddCredential(
|
||||
Uri uri,
|
||||
CredentialCache credentialCache,
|
||||
HashSet<string> credentialedAuthorities,
|
||||
NetworkCredential credential)
|
||||
{
|
||||
string authority = uri.GetLeftPart(UriPartial.Authority);
|
||||
if (credentialedAuthorities.Add(authority))
|
||||
{
|
||||
credentialCache.Add(new Uri(authority + "/"), "NTLM", credential);
|
||||
}
|
||||
}
|
||||
|
||||
private Uri GetProfileUri(InstitutionalRole role)
|
||||
{
|
||||
Uri endpoint = new(options.Endpoint, UriKind.Absolute);
|
||||
|
||||
Reference in New Issue
Block a user