using System;
using System.Net;
using VNLib.Utils.Memory;
using VNLib.Plugins.Essentials.Oauth;
using VNLib.Plugins.Essentials.Extensions;
using VNLib.Plugins.Extensions.Validation;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Sql;
namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints
{
///
/// Grants authorization to OAuth2 clients to protected resources
/// with access tokens
///
internal sealed class AccessTokenEndpoint : ResourceEndpointBase
{
private readonly Lazy TokenStore;
private readonly Applications Applications;
//override protection settings to allow most connections to authenticate
protected override ProtectionSettings EndpointProtectionSettings { get; } = new()
{
BrowsersOnly = false,
SessionsRequired = false,
VerifySessionCors = false
};
public AccessTokenEndpoint(string path, PluginBase pbase, Lazy tokenStore)
{
InitPathAndLog(path, pbase.Log);
TokenStore = tokenStore;
Applications = new(pbase.GetContextOptions(), pbase.GetPasswords());
}
protected override async ValueTask PostAsync(HttpEntity entity)
{
//Check for refresh token
if (entity.RequestArgs.IsArgumentSet("grant_type", "refresh_token"))
{
//process a refresh token
}
//Check for grant_type parameter from the request body
if (!entity.RequestArgs.IsArgumentSet("grant_type", "client_credentials"))
{
entity.CloseResponseError(HttpStatusCode.BadRequest, ErrorType.InvalidClient, "Invalid grant type");
//Default to bad request
return VfReturnType.VirtualSkip;
}
//Get client id and secret (and make sure theyre not empty
if (entity.RequestArgs.TryGetNonEmptyValue("client_id", out string? clientId) &&
entity.RequestArgs.TryGetNonEmptyValue("client_secret", out string? secret))
{
if (!ValidatorExtensions.OnlyAlphaNumRegx.IsMatch(clientId))
{
//respond with error message
entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidRequest, "Invalid client_id");
return VfReturnType.VirtualSkip;
}
if (!ValidatorExtensions.OnlyAlphaNumRegx.IsMatch(secret))
{
//respond with error message
entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidRequest, "Invalid client_secret");
return VfReturnType.VirtualSkip;
}
//Convert the clientid and secret to lowercase
clientId = clientId.ToLower();
secret = secret.ToLower();
//Convert secret to private string
PrivateString secretPv = new(secret, false);
//Get the application from apps store
UserApplication? app = await Applications.VerifyAppAsync(clientId, secretPv);
if (app == null)
{
//App was not found or the credentials do not match
entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidClient, "The client credentials are invalid");
return VfReturnType.VirtualSkip;
}
//Create a new session
IOAuth2TokenResult? result = await TokenStore.Value.CreateAccessTokenAsync(entity.Entity, app, entity.EventCancellation);
if (result == null)
{
entity.CloseResponseError(HttpStatusCode.ServiceUnavailable, ErrorType.TemporarilyUnabavailable, "You have reached the maximum number of valid tokens for this application");
return VfReturnType.VirtualSkip;
}
//Create the new response message
OauthTokenResponseMessage tokenMessage = new()
{
AccessToken = result.AccessToken,
//set expired as seconds in int form
Expires = result.ExpiresSeconds,
RefreshToken = result.RefreshToken,
TokenType = result.TokenType
};
//Respond with the token message
entity.CloseResponseJson(HttpStatusCode.OK, tokenMessage);
return VfReturnType.VirtualSkip;
}
//respond with error message
entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidClient, "The request was missing required arguments");
return VfReturnType.VirtualSkip;
}
}
}