diff --git a/README.md b/README.md index a03445fd6b5..24077a5a105 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 a6ac88d9e45..4d4dcc73936 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 dd34dbe536e..6a45bbc6fcb 100644 --- a/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs +++ b/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs @@ -22,5 +22,10 @@ public sealed class OAuthProviderInfo /// [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 00000000000..0d8b5775d7a --- /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 b55e94be643..0558bbfaa02 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 9e0602d07e1..c98863d01c7 100644 --- a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs @@ -61,9 +61,10 @@ sealed class LoginAuthority : AuthorityBase, ILoginAuthority /// /// 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 @@ public async ValueTask> AttemptLogin(Cancellation { 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,14 @@ public async ValueTask> AttemptLogin(Cancellation if (oAuthLogin) { var oAuthProvider = headers.OAuthProvider!.Value; - string? externalUserId; - try - { - var validator = oAuthProviders - .GetValidator(oAuthProvider); - - if (validator == null) - return BadRequest(ErrorCode.OAuthProviderDisabled); - - externalUserId = await validator - .ValidateResponseCode(headers.OAuthCode!, cancellationToken); - - Logger.LogTrace("External {oAuthProvider} UID: {externalUserId}", oAuthProvider, externalUserId); - } - catch (Octokit.RateLimitExceededException ex) - { - return RateLimit(ex); - } - - if (externalUserId == null) - return Unauthorized(); + var (errorResponse, oauthResult) = await TryOAuthenticate(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 @@ public async ValueTask> AttemptLogin(Cancellation } } + /// + 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 (errorResponse, oAuthResult) = await TryOAuthenticate(headers, oAuthProvider.Value, false, cancellationToken); + if (errorResponse != null) + return errorResponse; + + Logger.LogDebug("Generated {provider} OAuth AccessCode", oAuthProvider.Value); + + return new AuthorityResponse( + new OAuthGatewayLoginResult + { + AccessCode = oAuthResult!.Value.AccessCode, + }); + } + /// /// Add a given to the . /// @@ -296,5 +303,40 @@ private async ValueTask CacheSystemIdentity(ISystemIdentity systemIdentity, User identExpiry += TimeSpan.FromSeconds(15); await identityCache.CacheSystemIdentity(user, systemIdentity!, identExpiry); } + + /// + /// Attempt OAuth authentication. + /// + /// The to use for errored s. + /// The current . + /// The to use. + /// If this is for a server login. + /// The for the operation. + /// A resulting in an errored on failure or the result of the call to on success. + async ValueTask<(AuthorityResponse? ErrorResponse, (string? UserID, string AccessCode)? OAuthResult)> TryOAuthenticate(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(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(ex), null); + } + + if (!oauthResult.HasValue) + return (Unauthorized(), null); + + return (null, OAuthResult: oauthResult); + } } } diff --git a/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs b/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs index a170cd235f3..1236b698344 100644 --- a/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs +++ b/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs @@ -17,6 +17,11 @@ public abstract class OAuthConfigurationBase /// 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 @@ public OAuthConfigurationBase(OAuthConfigurationBase oAuthConfiguration) 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 00000000000..8abf7d4afcf --- /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 433ec5716f5..49f3e89c2bc 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs @@ -188,5 +188,19 @@ public ValueTask CreateToken(CancellationToken cancellationToken) 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. + /// OAuth authentication failed due to rate limiting. + [HttpPost("oauth_gateway")] + [ProducesResponseType(typeof(OAuthGatewayResponse), 200)] + [ProducesResponseType(typeof(ErrorMessageResponse), 429)] + 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 00000000000..0faad835b29 --- /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 c30790791ca..286fe00619d 100644 --- a/src/Tgstation.Server.Host/GraphQL/Mutation.cs +++ b/src/Tgstation.Server.Host/GraphQL/Mutation.cs @@ -23,11 +23,11 @@ public sealed class Mutation 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 @@ public ValueTask Login( 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 cf30206115b..a28589c21d3 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 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 00000000000..12b369cc95d --- /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/Instance.cs b/src/Tgstation.Server.Host/GraphQL/Types/Instance.cs index 192a5b377b1..26223a455eb 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/Instance.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/Instance.cs @@ -14,5 +14,12 @@ public sealed class Instance : Entity /// Queryable s. public IQueryable QueryableInstancePermissionSets() => throw new NotImplementedException(); + + /// + /// Gets the callers effective on the . + /// + /// The callers effective if it exists or . + public InstancePermissionSet? EffectivePermissionSet() + => throw new NotImplementedException(); } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/InstancePermissionSet.cs b/src/Tgstation.Server.Host/GraphQL/Types/InstancePermissionSet.cs index 76bcabcac68..ebe30ac0728 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/InstancePermissionSet.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/InstancePermissionSet.cs @@ -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 /// public sealed class InstancePermissionSet { + /// + /// Gets the the belongs to. + /// + /// The owning . + public PermissionSet PermissionSet() + => throw new NotImplementedException(); + /// /// The of the . /// diff --git a/src/Tgstation.Server.Host/GraphQL/Types/OAuth/BasicOAuthProviderInfo.cs b/src/Tgstation.Server.Host/GraphQL/Types/OAuth/BasicOAuthProviderInfo.cs index f503d7adc8d..3eac5890128 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 @@ public class BasicOAuthProviderInfo /// 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 @@ public BasicOAuthProviderInfo(OAuthProviderInfo providerInfo) 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 5963023b0fc..b68813be3cb 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.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 @@ abstract class GenericOAuthValidator : IOAuthValidator /// public abstract OAuthProvider Provider { get; } + /// + public OAuthGatewayStatus GatewayStatus => OAuthConfiguration.Gateway ?? default; + /// /// The for the . /// @@ -80,7 +84,7 @@ public GenericOAuthValidator( } /// - 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 @@ public GenericOAuthValidator( return null; } + if (!requireUserID) + return (null, AccessCode: accessToken); + Logger.LogTrace("Getting user details..."); var userInfoUrl = OAuthConfiguration?.UserInformationUrlOverride ?? UserInformationUrl; @@ -126,7 +133,7 @@ public GenericOAuthValidator( var userInformationJson = JObject.Parse(userInformationPayload); - return DecodeUserInformationPayload(userInformationJson); + return (DecodeUserInformationPayload(userInformationJson), AccessCode: accessToken); } catch (Exception ex) { @@ -146,6 +153,7 @@ public OAuthProviderInfo GetProviderInfo() 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 d535d978843..3bb7164f63c 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs @@ -8,6 +8,7 @@ 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 @@ sealed class GitHubOAuthValidator : IOAuthValidator /// public OAuthProvider Provider => OAuthProvider.GitHub; + /// + public OAuthGatewayStatus GatewayStatus => oAuthConfiguration.Gateway ?? default; + /// /// The for the . /// @@ -52,7 +56,7 @@ public GitHubOAuthValidator( } /// - 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 @@ public GitHubOAuthValidator( 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 @@ public OAuthProviderInfo GetProviderInfo() { 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 2da6651c041..3b72cd45bef 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs @@ -13,8 +13,9 @@ public interface IOAuthProviders /// 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 472a7ff091d..95f51e354d3 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs @@ -15,6 +15,11 @@ public interface IOAuthValidator /// OAuthProvider Provider { get; } + /// + /// The for the . + /// + OAuthGatewayStatus GatewayStatus { get; } + /// /// Gets the of validator. /// @@ -25,8 +30,9 @@ public interface IOAuthValidator /// 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 9d28ab1c824..5b5af32ba95 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -80,7 +80,10 @@ public OAuthProviders( } /// - 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 b965a326ed3..ac08b78c499 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