Merge pull request #1997 from tgstation/GitHubGateway [APIDeploy][GQLDeploy][NugetDeploy]

OAuth Gateways
This commit is contained in:
Jordan Dominion
2024-11-02 16:15:31 -04:00
committed by GitHub
22 changed files with 275 additions and 46 deletions
+5 -2
View File
@@ -292,7 +292,7 @@ Create an `appsettings.Production.yml` file next to `appsettings.yml`. This will
- `Swarm:UpdateRequiredNodeCount`: Should be set to the total number of servers in your swarm minus 1. Prevents updates from occurring unless the non-controller server count in the swarm is greater than or equal to this value.
- `Security:OAuth:<Provider Name>`: Sets the OAuth client ID and secret for a given `<Provider Name>`. The currently supported providers are `Keycloak`, `GitHub`, `Discord`, `InvisionCommunity` and `TGForums`. Setting these fields to `null` disables logins with the provider, but does not stop users from associating their accounts using the API. Sample Entry:
- `Security:OAuth:<Provider Name>`: Sets the OAuth client ID and secret for a given `<Provider Name>`. The currently supported providers are `Keycloak`, `GitHub`, `Discord`, `InvisionCommunity` and `TGForums`. Setting these fields to `null` disables logins AND gateway auth with the provider, but does not stop users from associating their accounts using the API. Sample Entry:
```yml
Security:
OAuth:
@@ -301,6 +301,7 @@ Security:
ClientSecret: "..."
RedirectUrl: "..."
ServerUrl: "..."
Gateway: Disabled # Can be one of `Disabled` disallowing gateway auth (default), `Enabled` allowing gateway auth, or `Only` allowing gateway auth and disabling OAuth logins with this provider
UserInformationUrlOverride: "..." # For power users, leave out of configuration for most cases. Not supported by GitHub provider.
```
The following providers use the `RedirectUrl` setting:
@@ -315,6 +316,8 @@ The following providers use the `ServerUrl` setting:
- Keycloak
- InvisionCommunity
Gateway auth simply allows the users to authenticate with the service using the configuration you provide and have their impersonation token passed back to the client. An example of this is using GitHub gateway auth to allow clients to enumerate pull requests without getting rate limited.
- `Telemetry:DisableVersionReporting`: Prevents you installation and the version you're using from being reported on the source repository's deployments list
- `Telemetry:ServerFriendlyName`: Prevents anonymous TGS version usage statistics from being sent to be displayed on the repository.
@@ -637,7 +640,7 @@ This functionality has the following prerequisites:
Here are tools for interacting with the TGS web API
- [tgstation-server-webpanel](https://github.com/tgstation/tgstation-server-webpanel): Official client and included with the server. A react web app for using tgstation-server.
- [Tgstation.Server.ControlPanel](https://github.com/tgstation/Tgstation.Server.ControlPanel): Official client. A cross platform GUI for using tgstation-server. Feature complete but lacks OAuth login options.
- [Tgstation.Server.ControlPanel](https://github.com/tgstation/Tgstation.Server.ControlPanel): Deprecated client. A cross platform GUI for using tgstation-server.
- [Tgstation.Server.Client](https://www.nuget.org/packages/Tgstation.Server.Client): A nuget .NET Standard 2.0 TAP based library for communicating with tgstation-server. Feature complete.
- [Tgstation.Server.Api](https://www.nuget.org/packages/Tgstation.Server.Api): A nuget .NET Standard 2.0 library containing API definitions for tgstation-server. Feature complete.
+6 -6
View File
@@ -3,13 +3,13 @@
<!-- Integration tests will ensure they match across the board -->
<Import Project="WebpanelVersion.props" />
<PropertyGroup>
<TgsCoreVersion>6.11.4</TgsCoreVersion>
<TgsConfigVersion>5.3.0</TgsConfigVersion>
<TgsRestVersion>10.10.0</TgsRestVersion>
<TgsGraphQLVersion>0.4.0</TgsGraphQLVersion>
<TgsCoreVersion>6.12.0</TgsCoreVersion>
<TgsConfigVersion>5.4.0</TgsConfigVersion>
<TgsRestVersion>10.11.0</TgsRestVersion>
<TgsGraphQLVersion>0.5.0</TgsGraphQLVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
<TgsApiLibraryVersion>16.1.0</TgsApiLibraryVersion>
<TgsClientVersion>19.1.0</TgsClientVersion>
<TgsApiLibraryVersion>16.2.0</TgsApiLibraryVersion>
<TgsClientVersion>19.2.0</TgsClientVersion>
<TgsDmapiVersion>7.3.0</TgsDmapiVersion>
<TgsInteropVersion>5.10.0</TgsInteropVersion>
<TgsHostWatchdogVersion>1.5.0</TgsHostWatchdogVersion>
@@ -22,5 +22,10 @@ namespace Tgstation.Server.Api.Models
/// </summary>
[ResponseOptions]
public Uri? ServerUrl { get; set; }
/// <summary>
/// If <see langword="true"/> the OAuth provider may only be used for gateway authentication. If <see langword="false"/> the OAuth provider may be used for server logins or gateway authentication. If <see langword="null"/> the OAuth provider may only be used for server logins.
/// </summary>
public bool? GatewayOnly { get; set; }
}
}
@@ -0,0 +1,13 @@
namespace Tgstation.Server.Api.Models.Response
{
/// <summary>
/// Success result for an OAuth gateway login attempt.
/// </summary>
public sealed class OAuthGatewayResponse
{
/// <summary>
/// The user's access token for the requested OAuth service.
/// </summary>
public string? AccessCode { get; set; }
}
}
@@ -12,10 +12,17 @@ namespace Tgstation.Server.Host.Authority
public interface ILoginAuthority : IAuthority
{
/// <summary>
/// Attempt to login to the server with the current crentials.
/// Attempt to login to the server with the current Basic or OAuth credentials.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="LoginResult"/> and <see cref="Models.User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="LoginResult"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
ValueTask<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken);
/// <summary>
/// Attempt to login to an OAuth service with the current OAuth credentials.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in an <see cref="OAuthGatewayLoginResult"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
ValueTask<AuthorityResponse<OAuthGatewayLoginResult>> AttemptOAuthGatewayLogin(CancellationToken cancellationToken);
}
}
@@ -61,9 +61,10 @@ namespace Tgstation.Server.Host.Authority
/// <summary>
/// Generate an <see cref="AuthorityResponse{TResult}"/> for a given <paramref name="headersException"/>.
/// </summary>
/// <typeparam name="TResult">The <see cref="Type"/> of <see cref="AuthorityResponse{TResult}"/> to generate.</typeparam>
/// <param name="headersException">The <see cref="HeadersException"/> to generate a response for.</param>
/// <returns>A new, errored <see cref="LoginResult"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
static AuthorityResponse<LoginResult> GenerateHeadersExceptionResponse(HeadersException headersException)
static AuthorityResponse<TResult> GenerateHeadersExceptionResponse<TResult>(HeadersException headersException)
=> new(
new ErrorMessageResponse(ErrorCode.BadHeaders)
{
@@ -135,7 +136,7 @@ namespace Tgstation.Server.Host.Authority
{
var headers = apiHeadersProvider.ApiHeaders;
if (headers == null)
return GenerateHeadersExceptionResponse(apiHeadersProvider.HeadersException!);
return GenerateHeadersExceptionResponse<LoginResult>(apiHeadersProvider.HeadersException!);
if (headers.IsTokenAuthentication)
return BadRequest<LoginResult>(ErrorCode.TokenWithToken);
@@ -161,32 +162,14 @@ namespace Tgstation.Server.Host.Authority
if (oAuthLogin)
{
var oAuthProvider = headers.OAuthProvider!.Value;
string? externalUserId;
try
{
var validator = oAuthProviders
.GetValidator(oAuthProvider);
if (validator == null)
return BadRequest<LoginResult>(ErrorCode.OAuthProviderDisabled);
externalUserId = await validator
.ValidateResponseCode(headers.OAuthCode!, cancellationToken);
Logger.LogTrace("External {oAuthProvider} UID: {externalUserId}", oAuthProvider, externalUserId);
}
catch (Octokit.RateLimitExceededException ex)
{
return RateLimit<LoginResult>(ex);
}
if (externalUserId == null)
return Unauthorized<LoginResult>();
var (errorResponse, oauthResult) = await TryOAuthenticate<LoginResult>(headers, oAuthProvider, true, cancellationToken);
if (errorResponse != null)
return errorResponse;
query = query.Where(
x => x.OAuthConnections!.Any(
y => y.Provider == oAuthProvider
&& y.ExternalUserId == externalUserId));
&& y.ExternalUserId == oauthResult!.Value.UserID));
}
else
{
@@ -281,6 +264,30 @@ namespace Tgstation.Server.Host.Authority
}
}
/// <inheritdoc />
public async ValueTask<AuthorityResponse<OAuthGatewayLoginResult>> AttemptOAuthGatewayLogin(CancellationToken cancellationToken)
{
var headers = apiHeadersProvider.ApiHeaders;
if (headers == null)
return GenerateHeadersExceptionResponse<OAuthGatewayLoginResult>(apiHeadersProvider.HeadersException!);
var oAuthProvider = headers.OAuthProvider;
if (!oAuthProvider.HasValue)
return BadRequest<OAuthGatewayLoginResult>(ErrorCode.BadHeaders);
var (errorResponse, oAuthResult) = await TryOAuthenticate<OAuthGatewayLoginResult>(headers, oAuthProvider.Value, false, cancellationToken);
if (errorResponse != null)
return errorResponse;
Logger.LogDebug("Generated {provider} OAuth AccessCode", oAuthProvider.Value);
return new AuthorityResponse<OAuthGatewayLoginResult>(
new OAuthGatewayLoginResult
{
AccessCode = oAuthResult!.Value.AccessCode,
});
}
/// <summary>
/// Add a given <paramref name="systemIdentity"/> to the <see cref="identityCache"/>.
/// </summary>
@@ -296,5 +303,40 @@ namespace Tgstation.Server.Host.Authority
identExpiry += TimeSpan.FromSeconds(15);
await identityCache.CacheSystemIdentity(user, systemIdentity!, identExpiry);
}
/// <summary>
/// Attempt OAuth authentication.
/// </summary>
/// <typeparam name="TResult">The <see cref="Type"/> to use for errored <see cref="AuthorityResponse{TResult}"/>s.</typeparam>
/// <param name="headers">The current <see cref="ApiHeaders"/>.</param>
/// <param name="oAuthProvider">The <see cref="OAuthProvider"/> to use.</param>
/// <param name="forLogin">If this is for a server login.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in an errored <see cref="AuthorityResponse{TResult}"/> on failure or the result of the call to <see cref="IOAuthValidator.ValidateResponseCode(string, bool, CancellationToken)"/> on success.</returns>
async ValueTask<(AuthorityResponse<TResult>? ErrorResponse, (string? UserID, string AccessCode)? OAuthResult)> TryOAuthenticate<TResult>(ApiHeaders headers, OAuthProvider oAuthProvider, bool forLogin, CancellationToken cancellationToken)
{
(string? UserID, string AccessCode)? oauthResult;
try
{
var validator = oAuthProviders
.GetValidator(oAuthProvider, forLogin);
if (validator == null)
return (BadRequest<TResult>(ErrorCode.OAuthProviderDisabled), null);
oauthResult = await validator
.ValidateResponseCode(headers.OAuthCode!, forLogin, cancellationToken);
Logger.LogTrace("External {oAuthProvider} UID: {externalUserId}", oAuthProvider, oauthResult);
}
catch (Octokit.RateLimitExceededException ex)
{
return (RateLimit<TResult>(ex), null);
}
if (!oauthResult.HasValue)
return (Unauthorized<TResult>(), null);
return (null, OAuthResult: oauthResult);
}
}
}
@@ -17,6 +17,11 @@ namespace Tgstation.Server.Host.Configuration
/// </summary>
public string? ClientSecret { get; set; }
/// <summary>
/// If the OAuth setup is only to be used for passing the user's OAuth token to clients.
/// </summary>
public OAuthGatewayStatus? Gateway { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="OAuthConfigurationBase"/> class.
/// </summary>
@@ -33,6 +38,7 @@ namespace Tgstation.Server.Host.Configuration
ArgumentNullException.ThrowIfNull(oAuthConfiguration);
ClientId = oAuthConfiguration.ClientId;
ClientSecret = oAuthConfiguration.ClientSecret;
Gateway = oAuthConfiguration.Gateway;
}
}
}
@@ -0,0 +1,23 @@
namespace Tgstation.Server.Host
{
/// <summary>
/// Status of the OAuth gateway for a provider.
/// </summary>
public enum OAuthGatewayStatus
{
/// <summary>
/// The OAuth Gateway is disabled.
/// </summary>
Disabled,
/// <summary>
/// The OAuth Gateway is enabled.
/// </summary>
Enabled,
/// <summary>
/// The provider may ONLY be used as an OAuth Gateway.
/// </summary>
Only,
}
}
@@ -188,5 +188,19 @@ namespace Tgstation.Server.Host.Controllers
return loginAuthority.InvokeTransformable<LoginResult, TokenResponse>(this, authority => authority.AttemptLogin(cancellationToken));
}
/// <summary>
/// Attempt to authenticate a <see cref="User"/> using <see cref="ApiController.ApiHeaders"/>.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation.</returns>
/// <response code="200"><see cref="OAuthGatewayResponse"/> generated successfully.</response>
/// <response code="401">OAuth authentication failed.</response>
/// <response code="429">OAuth authentication failed due to rate limiting.</response>
[HttpPost("oauth_gateway")]
[ProducesResponseType(typeof(OAuthGatewayResponse), 200)]
[ProducesResponseType(typeof(ErrorMessageResponse), 429)]
public ValueTask<IActionResult> CreateOAuthGatewayToken(CancellationToken cancellationToken)
=> loginAuthority.InvokeTransformable<OAuthGatewayLoginResult, OAuthGatewayResponse>(this, authority => authority.AttemptOAuthGatewayLogin(cancellationToken));
}
}
@@ -0,0 +1,24 @@
using System;
namespace Tgstation.Server.Host.Extensions
{
/// <summary>
/// Extension methods for <see cref="OAuthGatewayStatus"/>.
/// </summary>
static class OAuthGatewayStatusExtensions
{
/// <summary>
/// Convert a given <paramref name="oAuthGatewayStatus"/> to a <see cref="Nullable{T}"/> <see cref="bool"/> for API usage.
/// </summary>
/// <param name="oAuthGatewayStatus">The <see cref="OAuthGatewayStatus"/> to convert.</param>
/// <returns>The <see cref="Nullable{T}"/> <see cref="bool"/> form of the <paramref name="oAuthGatewayStatus"/>.</returns>
public static bool? ToBoolean(this OAuthGatewayStatus oAuthGatewayStatus)
=> oAuthGatewayStatus switch
{
OAuthGatewayStatus.Disabled => null,
OAuthGatewayStatus.Enabled => false,
OAuthGatewayStatus.Only => true,
_ => throw new InvalidOperationException($"Invalid {nameof(OAuthGatewayStatus)}: {oAuthGatewayStatus}"),
};
}
}
+19 -2
View File
@@ -23,11 +23,11 @@ namespace Tgstation.Server.Host.GraphQL
public const string GraphQLDescription = "Root Mutation type";
/// <summary>
/// Generate a JWT for authenticating with server. This is the only operation that accepts and required basic authentication.
/// Generate a JWT for authenticating with server. This requires either the Basic authentication or OAuth authentication schemes.
/// </summary>
/// <param name="loginAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="ILoginAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A Bearer token to be used with further communication with the server.</returns>
/// <returns>A <see cref="LoginResult"/>.</returns>
[Error(typeof(ErrorMessageException))]
public ValueTask<LoginResult> Login(
[Service] IGraphQLAuthorityInvoker<ILoginAuthority> loginAuthority,
@@ -38,5 +38,22 @@ namespace Tgstation.Server.Host.GraphQL
return loginAuthority.Invoke<LoginResult, LoginResult>(
authority => authority.AttemptLogin(cancellationToken));
}
/// <summary>
/// Generate an OAuth user token for the requested service. This requires the OAuth authentication scheme.
/// </summary>
/// <param name="loginAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="ILoginAuthority"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>An <see cref="OAuthGatewayLoginResult"/>.</returns>
[Error(typeof(ErrorMessageException))]
public ValueTask<OAuthGatewayLoginResult> OAuthGateway(
[Service] IGraphQLAuthorityInvoker<ILoginAuthority> loginAuthority,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(loginAuthority);
return loginAuthority.Invoke<OAuthGatewayLoginResult, OAuthGatewayLoginResult>(
authority => authority.AttemptOAuthGatewayLogin(cancellationToken));
}
}
}
@@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations.Payloads
public sealed class LoginResult : ILegacyApiTransformable<TokenResponse>
{
/// <summary>
/// The JSON Web Token (JWT) to use as a Bearer token for accessing the server. Contains an expiry time.
/// The JSON Web Token (JWT) to use as a Bearer token for accessing the server at non-login endpoints. Contains an expiry time.
/// </summary>
[GraphQLType<JwtType>]
[GraphQLNonNullType]
@@ -0,0 +1,26 @@
using HotChocolate;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.GraphQL.Mutations.Payloads
{
/// <summary>
/// Success result for an OAuth gateway login attempt.
/// </summary>
public sealed class OAuthGatewayLoginResult : ILegacyApiTransformable<OAuthGatewayResponse>
{
/// <summary>
/// The user's access token for the requested OAuth service.
/// </summary>
public required string AccessCode { get; init; }
/// <inheritdoc />
[GraphQLIgnore]
public OAuthGatewayResponse ToApi()
=> new()
{
AccessCode = AccessCode,
};
}
}
@@ -14,5 +14,12 @@ namespace Tgstation.Server.Host.GraphQL.Types
/// <returns>Queryable <see cref="InstancePermissionSet"/>s.</returns>
public IQueryable<InstancePermissionSet> QueryableInstancePermissionSets()
=> throw new NotImplementedException();
/// <summary>
/// Gets the callers effective <see cref="InstancePermissionSet"/> on the <see cref="Instance"/>.
/// </summary>
/// <returns>The callers effective <see cref="InstancePermissionSet"/> if it exists or <see langword="null"/>.</returns>
public InstancePermissionSet? EffectivePermissionSet()
=> throw new NotImplementedException();
}
}
@@ -1,4 +1,6 @@
using Tgstation.Server.Api.Rights;
using System;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Host.GraphQL.Types
{
@@ -7,6 +9,13 @@ namespace Tgstation.Server.Host.GraphQL.Types
/// </summary>
public sealed class InstancePermissionSet
{
/// <summary>
/// Gets the <see cref="Types.PermissionSet"/> the <see cref="InstancePermissionSet"/> belongs to.
/// </summary>
/// <returns>The owning <see cref="Types.PermissionSet"/>.</returns>
public PermissionSet PermissionSet()
=> throw new NotImplementedException();
/// <summary>
/// The <see cref="Api.Rights.InstancePermissionSetRights"/> of the <see cref="InstancePermissionSet"/>.
/// </summary>
@@ -14,6 +14,11 @@ namespace Tgstation.Server.Host.GraphQL.Types.OAuth
/// </summary>
public string ClientID { get; }
/// <summary>
/// If <see langword="true"/> the OAuth provider can only be used for gateway authentication. If <see langword="false"/> the OAuth provider may be used for server logins or gateway authentication. If <see langword="null"/> the OAuth provider may only be used for server logins.
/// </summary>
public bool? GatewayOnly { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BasicOAuthProviderInfo"/> class.
/// </summary>
@@ -23,6 +28,7 @@ namespace Tgstation.Server.Host.GraphQL.Types.OAuth
ArgumentNullException.ThrowIfNull(providerInfo);
ClientID = providerInfo.ClientId ?? throw new InvalidOperationException("ClientID not set!");
GatewayOnly = providerInfo.GatewayOnly;
}
}
}
@@ -15,6 +15,7 @@ using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Extensions;
namespace Tgstation.Server.Host.Security.OAuth
{
@@ -26,6 +27,9 @@ namespace Tgstation.Server.Host.Security.OAuth
/// <inheritdoc />
public abstract OAuthProvider Provider { get; }
/// <inheritdoc />
public OAuthGatewayStatus GatewayStatus => OAuthConfiguration.Gateway ?? default;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="GenericOAuthValidator"/>.
/// </summary>
@@ -80,7 +84,7 @@ namespace Tgstation.Server.Host.Security.OAuth
}
/// <inheritdoc />
public async ValueTask<string?> ValidateResponseCode(string code, CancellationToken cancellationToken)
public async ValueTask<(string? UserID, string AccessCode)?> ValidateResponseCode(string code, bool requireUserID, CancellationToken cancellationToken)
{
using var httpClient = CreateHttpClient();
string? tokenResponsePayload = null;
@@ -112,6 +116,9 @@ namespace Tgstation.Server.Host.Security.OAuth
return null;
}
if (!requireUserID)
return (null, AccessCode: accessToken);
Logger.LogTrace("Getting user details...");
var userInfoUrl = OAuthConfiguration?.UserInformationUrlOverride ?? UserInformationUrl;
@@ -126,7 +133,7 @@ namespace Tgstation.Server.Host.Security.OAuth
var userInformationJson = JObject.Parse(userInformationPayload);
return DecodeUserInformationPayload(userInformationJson);
return (DecodeUserInformationPayload(userInformationJson), AccessCode: accessToken);
}
catch (Exception ex)
{
@@ -146,6 +153,7 @@ namespace Tgstation.Server.Host.Security.OAuth
ClientId = OAuthConfiguration.ClientId,
RedirectUri = OAuthConfiguration.RedirectUrl,
ServerUrl = OAuthConfiguration.ServerUrl,
GatewayOnly = GatewayStatus.ToBoolean(),
};
/// <summary>
@@ -8,6 +8,7 @@ using Octokit;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Utils.GitHub;
namespace Tgstation.Server.Host.Security.OAuth
@@ -20,6 +21,9 @@ namespace Tgstation.Server.Host.Security.OAuth
/// <inheritdoc />
public OAuthProvider Provider => OAuthProvider.GitHub;
/// <inheritdoc />
public OAuthGatewayStatus GatewayStatus => oAuthConfiguration.Gateway ?? default;
/// <summary>
/// The <see cref="IGitHubServiceFactory"/> for the <see cref="GitHubOAuthValidator"/>.
/// </summary>
@@ -52,7 +56,7 @@ namespace Tgstation.Server.Host.Security.OAuth
}
/// <inheritdoc />
public async ValueTask<string?> ValidateResponseCode(string code, CancellationToken cancellationToken)
public async ValueTask<(string? UserID, string AccessCode)?> ValidateResponseCode(string code, bool requireUserID, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(code);
@@ -65,12 +69,15 @@ namespace Tgstation.Server.Host.Security.OAuth
if (token == null)
return null;
if (!requireUserID)
return (null, AccessCode: token);
var authenticatedClient = await gitHubServiceFactory.CreateService(token, cancellationToken);
logger.LogTrace("Getting user details...");
var userId = await authenticatedClient.GetCurrentUserId(cancellationToken);
return userId.ToString(CultureInfo.InvariantCulture);
return (userId.ToString(CultureInfo.InvariantCulture), AccessCode: token);
}
catch (RateLimitExceededException)
{
@@ -89,6 +96,7 @@ namespace Tgstation.Server.Host.Security.OAuth
{
ClientId = oAuthConfiguration.ClientId,
RedirectUri = oAuthConfiguration.RedirectUrl,
GatewayOnly = GatewayStatus.ToBoolean(),
};
}
}
@@ -13,8 +13,9 @@ namespace Tgstation.Server.Host.Security.OAuth
/// Gets the <see cref="IOAuthValidator"/> for a given <paramref name="oAuthProvider"/>.
/// </summary>
/// <param name="oAuthProvider">The <see cref="OAuthProvider"/> to get the validator for.</param>
/// <param name="forLogin">If the resulting <see cref="IOAuthValidator"/> will be used to authenticate a server login.</param>
/// <returns>The <see cref="IOAuthValidator"/> for <paramref name="oAuthProvider"/>.</returns>
IOAuthValidator? GetValidator(OAuthProvider oAuthProvider);
IOAuthValidator? GetValidator(OAuthProvider oAuthProvider, bool forLogin);
/// <summary>
/// Gets a <see cref="Dictionary{TKey, TValue}"/> of the provider client IDs.
@@ -15,6 +15,11 @@ namespace Tgstation.Server.Host.Security.OAuth
/// </summary>
OAuthProvider Provider { get; }
/// <summary>
/// The <see cref="OAuthGatewayStatus"/> for the <see cref="IOAuthValidator"/>.
/// </summary>
OAuthGatewayStatus GatewayStatus { get; }
/// <summary>
/// Gets the <see cref="OAuthProvider"/> of validator.
/// </summary>
@@ -25,8 +30,9 @@ namespace Tgstation.Server.Host.Security.OAuth
/// Validate a given OAuth response <paramref name="code"/>.
/// </summary>
/// <param name="code">The OAuth response string from web application.</param>
/// <param name="requireUserID">If the resulting user ID should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="null"/> if authentication failed, <see cref="global::System.UInt64.MaxValue"/> if a rate limit occurred, and the validated <see cref="OAuthConnection.ExternalUserId"/> otherwise.</returns>
ValueTask<string?> ValidateResponseCode(string code, CancellationToken cancellationToken);
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="null"/> if authentication failed or the validated <see cref="OAuthConnection.ExternalUserId"/> and OAuth access code otherwise.</returns>
ValueTask<(string? UserID, string AccessCode)?> ValidateResponseCode(string code, bool requireUserID, CancellationToken cancellationToken);
}
}
@@ -80,7 +80,10 @@ namespace Tgstation.Server.Host.Security.OAuth
}
/// <inheritdoc />
public IOAuthValidator? GetValidator(OAuthProvider oAuthProvider) => validators.FirstOrDefault(x => x.Provider == oAuthProvider);
public IOAuthValidator? GetValidator(OAuthProvider oAuthProvider, bool forLogin)
=> validators.FirstOrDefault(
x => x.Provider == oAuthProvider
&& ((forLogin && x.GatewayStatus != OAuthGatewayStatus.Only) || (!forLogin && x.GatewayStatus != OAuthGatewayStatus.Disabled)));
/// <inheritdoc />
public Dictionary<OAuthProvider, OAuthProviderInfo> ProviderInfos()
@@ -65,6 +65,7 @@ Security:
# ClientId: # OAuth client ID
# ClientSecret: # OAuth client secret
# ServerUrl: # Only used by Keycloak and InvisionCommunity. Server URL (Includes Keycloak realm)
# Gateway: # Can be one of `Disabled` disallowing gateway auth (default), `Enabled` allowing gateway auth, or `Only` allowing gateway auth and disabling OAuth logins with this provider
# UserInformationUrlOverride: # Not supported by GitHub. Overrides the URL TGS uses to retrieve a user's information
GitHub: # https://github.com OAuth configuration
Discord: # https://discord.com OAuth configuration