From 266e9867d8a3209be16fca30c5c567390267629e Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sat, 2 Nov 2024 11:57:16 -0400 Subject: [PATCH] Setup OAuth Gateways Closes #1996 --- README.md | 7 ++- build/Version.props | 12 ++--- .../Models/OAuthProviderInfo.cs | 5 ++ .../Models/Response/OAuthGatewayResponse.cs | 13 +++++ .../Authority/ILoginAuthority.cs | 11 +++- .../Authority/LoginAuthority.cs | 51 +++++++++++++++---- .../Configuration/OAuthConfigurationBase.cs | 6 +++ .../Configuration/OAuthGatewayStatus.cs | 23 +++++++++ .../Controllers/ApiRootController.cs | 12 +++++ .../OAuthGatewayStatusExtensions.cs | 24 +++++++++ src/Tgstation.Server.Host/GraphQL/Mutation.cs | 21 +++++++- .../GraphQL/Mutations/Payloads/LoginResult.cs | 2 +- .../Payloads/OAuthGatewayLoginResult.cs | 26 ++++++++++ .../Types/OAuth/BasicOAuthProviderInfo.cs | 6 +++ .../Security/OAuth/GenericOAuthValidator.cs | 12 ++++- .../Security/OAuth/GitHubOAuthValidator.cs | 12 ++++- .../Security/OAuth/IOAuthProviders.cs | 3 +- .../Security/OAuth/IOAuthValidator.cs | 10 +++- .../Security/OAuth/OAuthProviders.cs | 5 +- src/Tgstation.Server.Host/appsettings.yml | 1 + 20 files changed, 232 insertions(+), 30 deletions(-) create mode 100644 src/Tgstation.Server.Api/Models/Response/OAuthGatewayResponse.cs create mode 100644 src/Tgstation.Server.Host/Configuration/OAuthGatewayStatus.cs create mode 100644 src/Tgstation.Server.Host/Extensions/OAuthGatewayStatusExtensions.cs create mode 100644 src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/OAuthGatewayLoginResult.cs diff --git a/README.md b/README.md index a03445fd6b..24077a5a10 100644 --- a/README.md +++ b/README.md @@ -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:`: Sets the OAuth client ID and secret for a given ``. 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:`: Sets the OAuth client ID and secret for a given ``. 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. diff --git a/build/Version.props b/build/Version.props index a6ac88d9e4..4d4dcc7393 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,13 +3,13 @@ - 6.11.4 - 5.3.0 - 10.10.0 - 0.4.0 + 6.12.0 + 5.4.0 + 10.11.0 + 0.5.0 7.0.0 - 16.1.0 - 19.1.0 + 16.2.0 + 19.2.0 7.3.0 5.10.0 1.5.0 diff --git a/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs b/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs index dd34dbe536..6a45bbc6fc 100644 --- a/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs +++ b/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs @@ -22,5 +22,10 @@ namespace Tgstation.Server.Api.Models /// [ResponseOptions] public Uri? ServerUrl { get; set; } + + /// + /// If the OAuth provider may only be used for gateway authentication. If the OAuth provider may be used for server logins or gateway authentication. If the OAuth provider may only be used for server logins. + /// + public bool? GatewayOnly { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Response/OAuthGatewayResponse.cs b/src/Tgstation.Server.Api/Models/Response/OAuthGatewayResponse.cs new file mode 100644 index 0000000000..0d8b5775d7 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Response/OAuthGatewayResponse.cs @@ -0,0 +1,13 @@ +namespace Tgstation.Server.Api.Models.Response +{ + /// + /// Success result for an OAuth gateway login attempt. + /// + public sealed class OAuthGatewayResponse + { + /// + /// The user's access token for the requested OAuth service. + /// + public string? AccessCode { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs b/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs index b55e94be64..0558bbfaa0 100644 --- a/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs @@ -12,10 +12,17 @@ namespace Tgstation.Server.Host.Authority public interface ILoginAuthority : IAuthority { /// - /// Attempt to login to the server with the current crentials. + /// Attempt to login to the server with the current Basic or OAuth credentials. /// /// The for the operation. - /// A resulting in a and . + /// A resulting in a . ValueTask> AttemptLogin(CancellationToken cancellationToken); + + /// + /// Attempt to login to an OAuth service with the current OAuth credentials. + /// + /// The for the operation. + /// A resulting in an . + ValueTask> AttemptOAuthGatewayLogin(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs index 9e0602d07e..e0ae9ab433 100644 --- a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs @@ -61,9 +61,10 @@ namespace Tgstation.Server.Host.Authority /// /// Generate an for a given . /// + /// The of to generate. /// The to generate a response for. /// A new, errored . - static AuthorityResponse GenerateHeadersExceptionResponse(HeadersException headersException) + static AuthorityResponse GenerateHeadersExceptionResponse(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(apiHeadersProvider.HeadersException!); if (headers.IsTokenAuthentication) return BadRequest(ErrorCode.TokenWithToken); @@ -161,32 +162,32 @@ namespace Tgstation.Server.Host.Authority if (oAuthLogin) { var oAuthProvider = headers.OAuthProvider!.Value; - string? externalUserId; + (string? UserID, string AccessCode)? oauthResult; try { var validator = oAuthProviders - .GetValidator(oAuthProvider); + .GetValidator(oAuthProvider, true); if (validator == null) return BadRequest(ErrorCode.OAuthProviderDisabled); - externalUserId = await validator - .ValidateResponseCode(headers.OAuthCode!, cancellationToken); + oauthResult = await validator + .ValidateResponseCode(headers.OAuthCode!, true, cancellationToken); - Logger.LogTrace("External {oAuthProvider} UID: {externalUserId}", oAuthProvider, externalUserId); + Logger.LogTrace("External {oAuthProvider} UID: {externalUserId}", oAuthProvider, oauthResult); } catch (Octokit.RateLimitExceededException ex) { return RateLimit(ex); } - if (externalUserId == null) + if (!oauthResult.HasValue) return Unauthorized(); query = query.Where( x => x.OAuthConnections!.Any( y => y.Provider == oAuthProvider - && y.ExternalUserId == externalUserId)); + && y.ExternalUserId == oauthResult.Value.UserID)); } else { @@ -281,6 +282,38 @@ namespace Tgstation.Server.Host.Authority } } + /// + public async ValueTask> AttemptOAuthGatewayLogin(CancellationToken cancellationToken) + { + var headers = apiHeadersProvider.ApiHeaders; + if (headers == null) + return GenerateHeadersExceptionResponse(apiHeadersProvider.HeadersException!); + + var oAuthProvider = headers.OAuthProvider; + if (!oAuthProvider.HasValue) + return BadRequest(ErrorCode.BadHeaders); + + var validator = oAuthProviders + .GetValidator(oAuthProvider.Value, false); + + if (validator == null) + return BadRequest(ErrorCode.OAuthProviderDisabled); + + var result = await validator + .ValidateResponseCode(headers.OAuthCode!, false, cancellationToken); + + if (!result.HasValue) + return Unauthorized(); + + Logger.LogDebug("Generated {provider} OAuth AccessCode", oAuthProvider.Value); + + return new AuthorityResponse( + new OAuthGatewayLoginResult + { + AccessCode = result.Value.AccessCode, + }); + } + /// /// Add a given to the . /// diff --git a/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs b/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs index a170cd235f..1236b69834 100644 --- a/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs +++ b/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs @@ -17,6 +17,11 @@ namespace Tgstation.Server.Host.Configuration /// public string? ClientSecret { get; set; } + /// + /// If the OAuth setup is only to be used for passing the user's OAuth token to clients. + /// + public OAuthGatewayStatus? Gateway { get; set; } + /// /// Initializes a new instance of the class. /// @@ -33,6 +38,7 @@ namespace Tgstation.Server.Host.Configuration ArgumentNullException.ThrowIfNull(oAuthConfiguration); ClientId = oAuthConfiguration.ClientId; ClientSecret = oAuthConfiguration.ClientSecret; + Gateway = oAuthConfiguration.Gateway; } } } diff --git a/src/Tgstation.Server.Host/Configuration/OAuthGatewayStatus.cs b/src/Tgstation.Server.Host/Configuration/OAuthGatewayStatus.cs new file mode 100644 index 0000000000..8abf7d4afc --- /dev/null +++ b/src/Tgstation.Server.Host/Configuration/OAuthGatewayStatus.cs @@ -0,0 +1,23 @@ +namespace Tgstation.Server.Host +{ + /// + /// Status of the OAuth gateway for a provider. + /// + public enum OAuthGatewayStatus + { + /// + /// The OAuth Gateway is disabled. + /// + Disabled, + + /// + /// The OAuth Gateway is enabled. + /// + Enabled, + + /// + /// The provider may ONLY be used as an OAuth Gateway. + /// + Only, + } +} diff --git a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs index 433ec5716f..652c98d445 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs @@ -188,5 +188,17 @@ namespace Tgstation.Server.Host.Controllers return loginAuthority.InvokeTransformable(this, authority => authority.AttemptLogin(cancellationToken)); } + + /// + /// Attempt to authenticate a using . + /// + /// The for the operation. + /// A resulting in the of the operation. + /// generated successfully. + /// OAuth authentication failed. + [HttpPost("oauth_gateway")] + [ProducesResponseType(typeof(OAuthGatewayResponse), 200)] + public ValueTask CreateOAuthGatewayToken(CancellationToken cancellationToken) + => loginAuthority.InvokeTransformable(this, authority => authority.AttemptOAuthGatewayLogin(cancellationToken)); } } diff --git a/src/Tgstation.Server.Host/Extensions/OAuthGatewayStatusExtensions.cs b/src/Tgstation.Server.Host/Extensions/OAuthGatewayStatusExtensions.cs new file mode 100644 index 0000000000..0faad835b2 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/OAuthGatewayStatusExtensions.cs @@ -0,0 +1,24 @@ +using System; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extension methods for . + /// + static class OAuthGatewayStatusExtensions + { + /// + /// Convert a given to a for API usage. + /// + /// The to convert. + /// The form of the . + 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}"), + }; + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/Mutation.cs b/src/Tgstation.Server.Host/GraphQL/Mutation.cs index c30790791c..286fe00619 100644 --- a/src/Tgstation.Server.Host/GraphQL/Mutation.cs +++ b/src/Tgstation.Server.Host/GraphQL/Mutation.cs @@ -23,11 +23,11 @@ namespace Tgstation.Server.Host.GraphQL public const string GraphQLDescription = "Root Mutation type"; /// - /// 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. /// /// The for the . /// The for the operation. - /// A Bearer token to be used with further communication with the server. + /// A . [Error(typeof(ErrorMessageException))] public ValueTask Login( [Service] IGraphQLAuthorityInvoker loginAuthority, @@ -38,5 +38,22 @@ namespace Tgstation.Server.Host.GraphQL return loginAuthority.Invoke( authority => authority.AttemptLogin(cancellationToken)); } + + /// + /// Generate an OAuth user token for the requested service. This requires the OAuth authentication scheme. + /// + /// The for the . + /// The for the operation. + /// An . + [Error(typeof(ErrorMessageException))] + public ValueTask OAuthGateway( + [Service] IGraphQLAuthorityInvoker loginAuthority, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(loginAuthority); + + return loginAuthority.Invoke( + authority => authority.AttemptOAuthGatewayLogin(cancellationToken)); + } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/LoginResult.cs b/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/LoginResult.cs index cf30206115..7694d59202 100644 --- a/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/LoginResult.cs +++ b/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/LoginResult.cs @@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.GraphQL.Mutations.Payloads public sealed class LoginResult : ILegacyApiTransformable { /// - /// 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 and non-transfer endpoints. Contains an expiry time. /// [GraphQLType] [GraphQLNonNullType] diff --git a/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/OAuthGatewayLoginResult.cs b/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/OAuthGatewayLoginResult.cs new file mode 100644 index 0000000000..12b369cc95 --- /dev/null +++ b/src/Tgstation.Server.Host/GraphQL/Mutations/Payloads/OAuthGatewayLoginResult.cs @@ -0,0 +1,26 @@ +using HotChocolate; + +using Tgstation.Server.Api.Models.Response; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.GraphQL.Mutations.Payloads +{ + /// + /// Success result for an OAuth gateway login attempt. + /// + public sealed class OAuthGatewayLoginResult : ILegacyApiTransformable + { + /// + /// The user's access token for the requested OAuth service. + /// + public required string AccessCode { get; init; } + + /// + [GraphQLIgnore] + public OAuthGatewayResponse ToApi() + => new() + { + AccessCode = AccessCode, + }; + } +} diff --git a/src/Tgstation.Server.Host/GraphQL/Types/OAuth/BasicOAuthProviderInfo.cs b/src/Tgstation.Server.Host/GraphQL/Types/OAuth/BasicOAuthProviderInfo.cs index f503d7adc8..3eac589012 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/OAuth/BasicOAuthProviderInfo.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/OAuth/BasicOAuthProviderInfo.cs @@ -14,6 +14,11 @@ namespace Tgstation.Server.Host.GraphQL.Types.OAuth /// public string ClientID { get; } + /// + /// If the OAuth provider can only be used for gateway authentication. If the OAuth provider may be used for server logins or gateway authentication. If the OAuth provider may only be used for server logins. + /// + public bool? GatewayOnly { get; } + /// /// Initializes a new instance of the class. /// @@ -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; } } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs index 5963023b0f..d14c461a4d 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs @@ -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 /// public abstract OAuthProvider Provider { get; } + /// + public OAuthGatewayStatus GatewayStatus => OAuthConfiguration.Gateway!.Value; + /// /// The for the . /// @@ -80,7 +84,7 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public async ValueTask 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(), }; /// diff --git a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs index d535d97884..e07ff4b7ed 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs @@ -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 /// public OAuthProvider Provider => OAuthProvider.GitHub; + /// + public OAuthGatewayStatus GatewayStatus => oAuthConfiguration.Gateway!.Value; + /// /// The for the . /// @@ -52,7 +56,7 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public async ValueTask 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(), }; } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs index 2da6651c04..3b72cd45be 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs @@ -13,8 +13,9 @@ namespace Tgstation.Server.Host.Security.OAuth /// Gets the for a given . /// /// The to get the validator for. + /// If the resulting will be used to authenticate a server login. /// The for . - IOAuthValidator? GetValidator(OAuthProvider oAuthProvider); + IOAuthValidator? GetValidator(OAuthProvider oAuthProvider, bool forLogin); /// /// Gets a of the provider client IDs. diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs index 472a7ff091..95f51e354d 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs @@ -15,6 +15,11 @@ namespace Tgstation.Server.Host.Security.OAuth /// OAuthProvider Provider { get; } + /// + /// The for the . + /// + OAuthGatewayStatus GatewayStatus { get; } + /// /// Gets the of validator. /// @@ -25,8 +30,9 @@ namespace Tgstation.Server.Host.Security.OAuth /// Validate a given OAuth response . /// /// The OAuth response string from web application. + /// If the resulting user ID should be retrieved. /// The for the operation. - /// A resulting in if authentication failed, if a rate limit occurred, and the validated otherwise. - ValueTask ValidateResponseCode(string code, CancellationToken cancellationToken); + /// A resulting in if authentication failed or the validated and OAuth access code otherwise. + ValueTask<(string? UserID, string AccessCode)?> ValidateResponseCode(string code, bool requireUserID, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs index 9d28ab1c82..5b5af32ba9 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -80,7 +80,10 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - 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))); /// public Dictionary ProviderInfos() diff --git a/src/Tgstation.Server.Host/appsettings.yml b/src/Tgstation.Server.Host/appsettings.yml index b965a326ed..ac08b78c49 100644 --- a/src/Tgstation.Server.Host/appsettings.yml +++ b/src/Tgstation.Server.Host/appsettings.yml @@ -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