Files
Ngino/src/ReverseLlama.Server/TokenAuthentication.cs
T

213 lines
6.6 KiB
C#

using System.Security.Cryptography;
using System.Text;
using ReverseLlama.Protocol;
namespace ReverseLlama.Server;
internal static class TokenAuthentication
{
private static readonly PathString PathTokenPrefix = new("/token");
public static AuthResult Authorize(
HttpRequest request,
ServerSettings settings,
ManagementStore managementStore,
bool allowQueryToken,
bool allowPathToken = false)
{
if (string.IsNullOrWhiteSpace(settings.Token) && !managementStore.HasApiKeys)
{
return AuthResult.Success(null);
}
if (request.Headers.TryGetValue(ProtocolConstants.TokenHeader, out var headerValues))
{
foreach (var value in headerValues)
{
var result = AuthorizeToken(value, settings, managementStore, updateApiKeyLastUsed: true);
if (result.IsAuthorized)
{
return result;
}
}
}
if (request.Headers.TryGetValue("Authorization", out var authorizationValues))
{
foreach (var value in authorizationValues)
{
if (TryGetBearerToken(value, out var bearerToken))
{
var result = AuthorizeToken(bearerToken, settings, managementStore, updateApiKeyLastUsed: true);
if (result.IsAuthorized)
{
return result;
}
}
}
}
// Path-token auth: useful for clients that cannot send headers.
// SECURITY: the token appears in the URL and will be logged by
// web servers, proxies, and browsers. Prefer header auth when possible.
if (allowPathToken
&& TryGetPathToken(request.Path, out var pathToken, out _))
{
var result = AuthorizeToken(pathToken, settings, managementStore, updateApiKeyLastUsed: true);
if (result.IsAuthorized)
{
return result;
}
}
// Query-string auth: needed for clients that cannot send headers
// (e.g. browser address bar, status page).
// SECURITY: same URL-logging risks as path-token auth above.
if (allowQueryToken
&& request.Query.TryGetValue("token", out var queryValues))
{
foreach (var value in queryValues)
{
var result = AuthorizeToken(value, settings, managementStore, updateApiKeyLastUsed: true);
if (result.IsAuthorized)
{
return result;
}
}
}
return AuthResult.Failure;
}
public static bool IsAuthorized(
HttpRequest request,
ServerSettings settings,
ManagementStore managementStore,
bool allowQueryToken,
bool allowPathToken = false) =>
Authorize(request, settings, managementStore, allowQueryToken, allowPathToken).IsAuthorized;
public static bool TryRemovePathToken(
PathString path,
ServerSettings settings,
ManagementStore managementStore,
out PathString remainingPath)
{
remainingPath = path;
if (!TryGetPathToken(path, out var pathToken, out var tokenRemainingPath)
|| !IsTokenAuthorized(pathToken, settings, managementStore, updateApiKeyLastUsed: false))
{
return false;
}
remainingPath = string.IsNullOrEmpty(tokenRemainingPath.Value)
? new PathString("/")
: tokenRemainingPath;
return true;
}
public static bool IsOwnBearerValue(string? value, ServerSettings settings, ManagementStore managementStore) =>
TryGetBearerToken(value, out var token)
&& IsTokenAuthorized(token, settings, managementStore, updateApiKeyLastUsed: false);
private static AuthResult AuthorizeToken(
string? token,
ServerSettings settings,
ManagementStore managementStore,
bool updateApiKeyLastUsed)
{
if (string.IsNullOrWhiteSpace(token))
{
return AuthResult.Failure;
}
if (!string.IsNullOrWhiteSpace(settings.Token)
&& CryptographicOperations.FixedTimeEquals(
SHA256.HashData(Encoding.UTF8.GetBytes(token)),
SHA256.HashData(Encoding.UTF8.GetBytes(settings.Token))))
{
return AuthResult.Success(null);
}
var apiKeyId = managementStore.GetApiKeyId(token);
if (apiKeyId is not null)
{
managementStore.IsApiKeyValid(token, updateApiKeyLastUsed);
return AuthResult.Success(apiKeyId);
}
return AuthResult.Failure;
}
public static bool IsTokenAuthorized(
string? token,
ServerSettings settings,
ManagementStore managementStore,
bool updateApiKeyLastUsed) =>
AuthorizeToken(token, settings, managementStore, updateApiKeyLastUsed).IsAuthorized;
private static bool TryGetBearerToken(string? authorization, out string token)
{
token = "";
if (string.IsNullOrWhiteSpace(authorization)
|| !authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
return false;
}
token = authorization["Bearer ".Length..].Trim();
return token.Length > 0;
}
private static bool TryGetPathToken(PathString path, out string pathToken, out PathString remainingPath)
{
pathToken = "";
remainingPath = PathString.Empty;
if (!path.StartsWithSegments(PathTokenPrefix, out var pathAfterPrefix))
{
return false;
}
var value = pathAfterPrefix.Value ?? "";
if (value.Length <= 1 || value[0] != '/')
{
return false;
}
var nextSlash = value.IndexOf('/', 1);
pathToken = nextSlash < 0
? value[1..]
: value[1..nextSlash];
if (string.IsNullOrEmpty(pathToken))
{
return false;
}
remainingPath = nextSlash < 0
? PathString.Empty
: new PathString(value[nextSlash..]);
return true;
}
}
internal sealed class AuthResult
{
public static AuthResult Failure { get; } = new(false, null);
public static AuthResult Success(string? apiKeyId) => new(true, apiKeyId);
public bool IsAuthorized { get; }
public string? ApiKeyId { get; }
private AuthResult(bool isAuthorized, string? apiKeyId)
{
IsAuthorized = isAuthorized;
ApiKeyId = apiKeyId;
}
}