134 lines
4.3 KiB
C#
134 lines
4.3 KiB
C#
using ReverseLlama.Protocol;
|
|
|
|
namespace ReverseLlama.Server;
|
|
|
|
internal static class TokenAuthentication
|
|
{
|
|
private static readonly PathString PathTokenPrefix = new("/token");
|
|
|
|
public static bool IsAuthorized(
|
|
HttpRequest request,
|
|
ServerSettings settings,
|
|
ManagementStore managementStore,
|
|
bool allowQueryToken,
|
|
bool allowPathToken = false)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(settings.Token) && !managementStore.HasApiKeys)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (request.Headers.TryGetValue(ProtocolConstants.TokenHeader, out var headerValues)
|
|
&& headerValues.Any(value => IsTokenAuthorized(value, settings, managementStore, updateApiKeyLastUsed: true)))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Bearer form for OpenAI-compatible clients (e.g. n8n's OpenAI nodes pointed
|
|
// at /clients/{id}/v1) that can send an API key but no custom headers.
|
|
if (request.Headers.TryGetValue("Authorization", out var authorizationValues)
|
|
&& authorizationValues.Any(value => TryGetBearerToken(value, out var bearerToken)
|
|
&& IsTokenAuthorized(bearerToken, settings, managementStore, updateApiKeyLastUsed: true)))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (allowPathToken
|
|
&& TryGetPathToken(request.Path, out var pathToken, out _)
|
|
&& IsTokenAuthorized(pathToken, settings, managementStore, updateApiKeyLastUsed: true))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return allowQueryToken
|
|
&& request.Query.TryGetValue("token", out var queryValues)
|
|
&& queryValues.Any(value => IsTokenAuthorized(value, settings, managementStore, updateApiKeyLastUsed: true));
|
|
}
|
|
|
|
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);
|
|
|
|
public static bool IsTokenAuthorized(
|
|
string? token,
|
|
ServerSettings settings,
|
|
ManagementStore managementStore,
|
|
bool updateApiKeyLastUsed)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(token))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return !string.IsNullOrWhiteSpace(settings.Token)
|
|
&& string.Equals(token, settings.Token, StringComparison.Ordinal)
|
|
|| managementStore.IsApiKeyValid(token, updateApiKeyLastUsed);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|