diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 15c75bb15d..474aabc0f1 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -203,7 +203,7 @@ jobs: - name: Set TGS4_GITHUB_REF for PR if: ${{ github.event_name == 'pull_request' }} - run: echo "TGS4_GITHUB_REF=${{ github.event.base_ref }}" >> $env:GITHUB_ENV + run: echo "TGS4_GITHUB_REF=${{ github.base_ref }}" >> $env:GITHUB_ENV - name: Set TGS4_GITHUB_REF for push if: ${{ github.event_name == 'push' }} @@ -345,7 +345,7 @@ jobs: - name: Set TGS4_GITHUB_REF for PR if: ${{ github.event_name == 'pull_request' }} - run: echo "TGS4_GITHUB_REF=${{ github.event.base_ref }}" >> $GITHUB_ENV + run: echo "TGS4_GITHUB_REF=${{ github.base_ref }}" >> $GITHUB_ENV - name: Set TGS4_GITHUB_REF for push if: ${{ github.event_name == 'push' }} diff --git a/README.md b/README.md index 922fbab65d..841fdb4e12 100644 --- a/README.md +++ b/README.md @@ -133,10 +133,21 @@ Create an `appsettings.Production.json` file next to `appsettings.json`. This wi - `Security:OAuth`: Sets the OAuth client ID and secret for a given ``. The currently supported providers are `GitHub`, `Discord`, 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: ```json "GitHubOAuth":{ - "ClientId": "... (Note for `TGForums`, this is the redirect_uri used)", - "ClientSecret": "..." + "ClientId": "...", + "ClientSecret": "...", + "RedirectUrl": "...", (Used with certain providers) + "ServerUrl": "...", (Used with certain providers) } ``` +The following providers use the `RedirectUrl` setting: + +- GitHub +- TGForums +- Keycloak + +The following providers use the `ServerUrl` setting: + +- Keycloak ### Database Configuration diff --git a/docs/API.dox b/docs/API.dox index f33f901cb2..0a27032d8e 100644 --- a/docs/API.dox +++ b/docs/API.dox @@ -63,7 +63,7 @@ TGS will only every return the response codes listed here - 204: No Content. Identical to 200 with no response body. - 400: Bad Request. The response body will contain an @ref Tgstation.Server.Api.Models.ErrorMessage model detailing the error - 401: Unauthorized. Invalid or expired credentials were provided. Check rights APIs for updates. See @ref api_auth for details -- 403: Forbidden. User tried to make a request they were not allowed to perform. +- 403: Forbidden. User tried to make a request they were not allowed to perform. - 404: Not found. A resource was requested that had never existed. In the case of retrieving a resource by ID, it could potentially exist in the future - 406: Not Acceptable. Consequence of failing to provide an Accept header - 408: Request Timeout. The client took to long to continue a request @@ -97,7 +97,7 @@ Other fields may be present in the Version model but should be ignored. See a de @section api_auth Authentication -Every request made to TGS requires authentication. It is provided in the form of the Authorization header. +Every request made to TGS requires authentication. It is provided in the form of the Authorization header. The first request made to TGS must be to login the user @@ -128,7 +128,7 @@ TGS4 supports OAuth 2.0 with select providers for authentication. The flow for this is as follows: -- Retrieve the @ref api_ver to find out available OAuth providers and their respective client IDs. +- Retrieve the @ref api_ver to find out available OAuth providers and their respective client ID and redirect URIs. - Send the user to the Authorization Request endpoint for the provider using the client ID from above. See https://tools.ietf.org/html/rfc6749#section-4.1.1. DO NOT specify a redirect URI, this should be configured in the provider. - Retrieve the authorization response code after successfully completing the authorize step above. - Perform the following request: @@ -144,9 +144,10 @@ You will be granted a bearer token as in basic auth. This will have an extended @subsubsection api_auth_o_providers Supported Providers -- ID: 0, Name: GitHub, Documentation: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/ -- ID: 1, Name: Discord, Documentation: https://discord.com/developers/docs/topics/oauth2 -- ID: 2, Name: TGForums, Documentation: https://tgstation13.org/phpBB/viewtopic.php?f=45&t=9922 +- GitHub: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps +- Discord: https://discord.com/developers/docs/topics/oauth2 +- TGForums: https://tgstation13.org/phpBB/viewtopic.php?f=45&t=9922 +- Keycloak: https://plugins.miniorange.com/keycloak-single-sign-on-wordpress-sso-oauth-openid-connect @section api_perms Permissions @@ -391,7 +392,7 @@ If the server detects a set of @ref Tgstation.Server.Api.Models.TestMergeParamet @subsubsection api_repounsetauth Unsetting Authentication -The repository uses the @ref Tgstation.Server.Api.Models.Repository.AccessUser and @ref Tgstation.Server.Api.Models.Repository.AccessToken credentials to access the remote repository if these fields are set. To unset them you must set both of them to an empty string like so +The repository uses the @ref Tgstation.Server.Api.Models.Repository.AccessUser and @ref Tgstation.Server.Api.Models.Repository.AccessToken credentials to access the remote repository if these fields are set. To unset them you must set both of them to an empty string like so @code{.json} { diff --git a/src/Tgstation.Server.Api/Models/OAuthConnection.cs b/src/Tgstation.Server.Api/Models/OAuthConnection.cs index d684a04e2e..f68f7d2f16 100644 --- a/src/Tgstation.Server.Api/Models/OAuthConnection.cs +++ b/src/Tgstation.Server.Api/Models/OAuthConnection.cs @@ -1,3 +1,5 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Api.Models @@ -10,6 +12,7 @@ namespace Tgstation.Server.Api.Models /// /// The of the . /// ] + [JsonConverter(typeof(StringEnumConverter))] [EnumDataType(typeof(OAuthProvider))] public OAuthProvider Provider { get; set; } diff --git a/src/Tgstation.Server.Api/Models/OAuthProvider.cs b/src/Tgstation.Server.Api/Models/OAuthProvider.cs index 8d5a9eb354..0031ed0f79 100644 --- a/src/Tgstation.Server.Api/Models/OAuthProvider.cs +++ b/src/Tgstation.Server.Api/Models/OAuthProvider.cs @@ -19,5 +19,10 @@ namespace Tgstation.Server.Api.Models /// https://tgstation13.org /// TGForums, + + /// + /// https://www.keycloak.org + /// + Keycloak, } } diff --git a/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs b/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs new file mode 100644 index 0000000000..10466c3204 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/OAuthProviderInfo.cs @@ -0,0 +1,20 @@ +using System; + +namespace Tgstation.Server.Api.Models +{ + /// + /// Public information about a given . + /// + public sealed class OAuthProviderInfo + { + /// + /// The client ID. + /// + public string? ClientId { get; set; } + + /// + /// The redirect URL. + /// + public Uri? RedirectUri { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/ServerInformation.cs b/src/Tgstation.Server.Api/Models/ServerInformation.cs index a2d8958845..2111302237 100644 --- a/src/Tgstation.Server.Api/Models/ServerInformation.cs +++ b/src/Tgstation.Server.Api/Models/ServerInformation.cs @@ -24,8 +24,8 @@ namespace Tgstation.Server.Api.Models public Version? DMApiVersion { get; set; } /// - /// Map of to the server's associated client IDs for them. + /// Map of to the for them. /// - public IDictionary? OAuthProviderClientIds { get; set; } + public IDictionary? OAuthProviderInfos { get; set; } } } diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj index 7b61d94fec..f971e5b548 100644 --- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj +++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj @@ -37,6 +37,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj index 3070b75011..4ceb1f0844 100644 --- a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj +++ b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj @@ -31,7 +31,6 @@ - all runtime; build; native; contentfiles; analyzers diff --git a/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs b/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs index b0dc12fc21..b12e239911 100644 --- a/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs @@ -3,35 +3,18 @@ using System; namespace Tgstation.Server.Host.Configuration { /// - /// OAuth options. + /// OAuth configuration options. /// - class OAuthConfiguration + sealed class OAuthConfiguration : OAuthConfigurationBase { /// - /// The client ID. + /// The client redirect URL. Not used by all providers. /// - public string ClientId { get; set; } + public Uri ServerUrl { get; set; } /// - /// The client secret. + /// The authentication server URL. Not used by all providers. /// - public string ClientSecret { get; set; } - - /// - /// Initializes a new instance of the . - /// - public OAuthConfiguration() { } - - /// - /// Initializes a new instance of the . - /// - /// The to copy settings from. - public OAuthConfiguration(OAuthConfiguration oAuthConfiguration) - { - if (oAuthConfiguration == null) - throw new ArgumentNullException(nameof(oAuthConfiguration)); - ClientId = oAuthConfiguration.ClientId; - ClientSecret = oAuthConfiguration.ClientSecret; - } + public Uri RedirectUrl { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs b/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs new file mode 100644 index 0000000000..6fb4fda5fb --- /dev/null +++ b/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs @@ -0,0 +1,37 @@ +using System; + +namespace Tgstation.Server.Host.Configuration +{ + /// + /// Base OAuth options. + /// + abstract class OAuthConfigurationBase + { + /// + /// The client ID. + /// + public string ClientId { get; set; } + + /// + /// The client secret. + /// + public string ClientSecret { get; set; } + + /// + /// Initializes a new instance of the . + /// + public OAuthConfigurationBase() { } + + /// + /// Initializes a new instance of the . + /// + /// The to copy settings from. + public OAuthConfigurationBase(OAuthConfigurationBase oAuthConfiguration) + { + if (oAuthConfiguration == null) + throw new ArgumentNullException(nameof(oAuthConfiguration)); + ClientId = oAuthConfiguration.ClientId; + ClientSecret = oAuthConfiguration.ClientSecret; + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index 9d09033105..fd4dd4d7f8 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -167,7 +167,7 @@ namespace Tgstation.Server.Host.Controllers InstanceLimit = generalConfiguration.InstanceLimit, UserLimit = generalConfiguration.UserLimit, ValidInstancePaths = generalConfiguration.ValidInstancePaths, - OAuthProviderClientIds = await oAuthProviders.ClientIds(cancellationToken).ConfigureAwait(false) + OAuthProviderInfos = await oAuthProviders.ProviderInfos(cancellationToken).ConfigureAwait(false) }); } diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index 5d4783dcf0..547df857df 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; namespace Tgstation.Server.Host.Models { @@ -75,7 +76,8 @@ namespace Tgstation.Server.Host.Models Id = Id, InstanceManagerRights = showDetails ? InstanceManagerRights : null, Name = Name, - SystemIdentifier = showDetails ? SystemIdentifier : null + SystemIdentifier = showDetails ? SystemIdentifier : null, + OAuthConnections = OAuthConnections?.Select(x => x.ToApi()).ToList(), }; /// diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs index d74ef23d4a..04bf2e2062 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Linq; @@ -60,6 +60,7 @@ namespace Tgstation.Server.Host.Security .AsQueryable() .Where(x => x.Id == userId) .Include(x => x.CreatedBy) + .Include(x => x.OAuthConnections) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (user == default) diff --git a/src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs index 68cc17cf23..4cb1995f59 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs @@ -73,7 +73,7 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public abstract Task GetClientId(CancellationToken cancellationToken); + public abstract Task GetProviderInfo(CancellationToken cancellationToken); /// public abstract Task ValidateResponseCode(string code, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs index f0fe8591f5..37d055d08b 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs @@ -38,7 +38,7 @@ namespace Tgstation.Server.Host.Security.OAuth protected override Uri UserInformationUrl => new Uri("https://discord.com/api/users/@me"); /// - protected override OAuthTokenRequest CreateTokenRequest(string code) => new DiscordTokenRequest(OAuthConfiguration, code); + protected override OAuthTokenRequest CreateTokenRequest(string code) => new OAuthTokenRequest(OAuthConfiguration, code, "identify"); /// protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token; diff --git a/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs b/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs deleted file mode 100644 index fdd6dcc393..0000000000 --- a/src/Tgstation.Server.Host/Security/OAuth/DiscordTokenRequest.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Tgstation.Server.Host.Configuration; - -namespace Tgstation.Server.Host.Security.OAuth -{ - /// - /// for Discord. - /// - /// See https://discord.com/developers/docs/topics/oauth2 - sealed class DiscordTokenRequest : OAuthTokenRequest - { - /// - /// The 'grant_type' field. - /// - public string GrantType { get; } - - /// - /// The 'scope' field. - /// - public string Scope { get; } - - /// - /// Initializes a new instance of the . - /// - /// The for the . - /// The OAuth code for the . - public DiscordTokenRequest(OAuthConfiguration oAuthConfiguration, string code) - : base(oAuthConfiguration, code) - { - GrantType = "authorization_code"; - Scope = "identify"; - } - } -} diff --git a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs index 8deff22537..ad0de1e980 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs @@ -8,6 +8,7 @@ using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.System; @@ -95,7 +96,10 @@ namespace Tgstation.Server.Host.Security.OAuth var accessToken = DecodeTokenPayload(tokenResponseJson); if (accessToken == null) + { + Logger.LogTrace("No token from DecodeTokenPayload!"); return null; + } Logger.LogTrace("Getting user details..."); using var userInformationRequest = new HttpRequestMessage(HttpMethod.Get, UserInformationUrl); @@ -119,6 +123,11 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public override Task GetClientId(CancellationToken cancellationToken) => Task.FromResult(OAuthConfiguration.ClientId); + public override Task GetProviderInfo(CancellationToken cancellationToken) => Task.FromResult( + new OAuthProviderInfo + { + ClientId = OAuthConfiguration.ClientId, + RedirectUri = OAuthConfiguration.RedirectUrl + }); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs index b4ee1354e6..c678568ebe 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs @@ -65,7 +65,10 @@ namespace Tgstation.Server.Host.Security.OAuth new OauthTokenRequest( oAuthConfiguration.ClientId, oAuthConfiguration.ClientSecret, - code)) + code) + { + RedirectUri = oAuthConfiguration.RedirectUrl + }) .ConfigureAwait(false); var token = response.AccessToken; @@ -94,6 +97,11 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public Task GetClientId(CancellationToken cancellationToken) => Task.FromResult(oAuthConfiguration.ClientId); + public Task GetProviderInfo(CancellationToken cancellationToken) => Task.FromResult( + new OAuthProviderInfo + { + ClientId = oAuthConfiguration.ClientId, + RedirectUri = oAuthConfiguration.RedirectUrl + }); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs index 66af75b5af..06cae4e96e 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs @@ -21,7 +21,7 @@ namespace Tgstation.Server.Host.Security.OAuth /// Gets a of the provider client IDs. /// /// The for the operation. - /// A resulting in a anew of the active provider client IDs. - Task> ClientIds(CancellationToken cancellationToken); + /// A resulting in a anew of the active s. + Task> ProviderInfos(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs index be33dcb992..be8df49788 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs @@ -15,11 +15,11 @@ namespace Tgstation.Server.Host.Security.OAuth OAuthProvider Provider { get; } /// - /// Gets the OAuth client ID of validator. + /// Gets the of validator. /// /// The for the operation. /// A resulting in the client ID of the validator on success, on failure. - Task GetClientId(CancellationToken cancellationToken); + Task GetProviderInfo(CancellationToken cancellationToken); /// /// Validate a given OAuth response . diff --git a/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs new file mode 100644 index 0000000000..61e08a56ab --- /dev/null +++ b/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs @@ -0,0 +1,54 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Net.Http; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Security.OAuth +{ + /// + /// OAuth validator for Keycloak. + /// + sealed class KeycloakOAuthValidator : GenericOAuthValidator + { + /// + public override OAuthProvider Provider => OAuthProvider.Keycloak; + + /// + protected override Uri TokenUrl => new Uri($"{BaseProtocolPath}/token"); + + /// + protected override Uri UserInformationUrl => new Uri($"{BaseProtocolPath}/userinfo"); + + /// + /// Base path to the server's OAuth endpoint. + /// + string BaseProtocolPath => $"{OAuthConfiguration.ServerUrl}/protocol/openid-connect"; + + /// + /// Initializes a new instance of the . + /// + /// The for the . + /// The for the . + /// The for the . + /// The for the . + public KeycloakOAuthValidator( + IHttpClientFactory httpClientFactory, + IAssemblyInformationProvider assemblyInformationProvider, + ILogger logger, + OAuthConfiguration oAuthConfiguration) + : base(httpClientFactory, assemblyInformationProvider, logger, oAuthConfiguration) + { + } + + /// + protected override OAuthTokenRequest CreateTokenRequest(string code) => new OAuthTokenRequest(OAuthConfiguration, code, "openid"); + + /// + protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token; + + /// + protected override string DecodeUserInformationPayload(dynamic responseJson) => responseJson.sub; + } +} diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs index 91be97f0e2..e8133706b2 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -58,7 +58,7 @@ namespace Tgstation.Server.Host.Security.OAuth loggerFactory.CreateLogger(), discordConfig)); - if(securityConfiguration.OAuth.TryGetValue(OAuthProvider.TGForums, out var tgConfig)) + if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.TGForums, out var tgConfig)) validatorsBuilder.Add( new TGForumsOAuthValidator( httpClientFactory, @@ -66,6 +66,14 @@ namespace Tgstation.Server.Host.Security.OAuth loggerFactory.CreateLogger(), tgConfig)); + if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.Keycloak, out var keyCloakConfig)) + validatorsBuilder.Add( + new KeycloakOAuthValidator( + httpClientFactory, + assemblyInformationProvider, + loggerFactory.CreateLogger(), + keyCloakConfig)); + validators = validatorsBuilder; } @@ -73,11 +81,11 @@ namespace Tgstation.Server.Host.Security.OAuth public IOAuthValidator GetValidator(OAuthProvider oAuthProvider) => validators.FirstOrDefault(x => x.Provider == oAuthProvider); /// - public async Task> ClientIds(CancellationToken cancellationToken) + public async Task> ProviderInfos(CancellationToken cancellationToken) { var providersAndTasks = validators.ToDictionary( x => x.Provider, - x => x.GetClientId(cancellationToken)); + x => x.GetProviderInfo(cancellationToken)); await Task.WhenAll(providersAndTasks.Values).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs index 7f03023625..eb715953c6 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs @@ -6,22 +6,42 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// Generic OAuth token request. /// - class OAuthTokenRequest : OAuthConfiguration + sealed class OAuthTokenRequest : OAuthConfigurationBase { /// - /// The OAuth code. + /// The OAuth code received from the browser. /// public string Code { get; } /// - /// Initializes a new instance of the + /// The scopes being requested. + /// + public string Scope { get; } + + /// + /// The OAuth redirect URI. + /// + public Uri RedirectUri { get; } + + /// + /// The OAuth grant type. + /// + public string GrantType { get; } + + /// + /// Initializes a new instance of the . /// /// The to build from. - /// The OAuth code received from the browser. - public OAuthTokenRequest(OAuthConfiguration oAuthConfiguration, string code) + /// The value of . + /// The value of + public OAuthTokenRequest(OAuthConfiguration oAuthConfiguration, string code, string scope) : base(oAuthConfiguration) { Code = code ?? throw new ArgumentNullException(nameof(code)); + Scope = scope ?? throw new ArgumentNullException(nameof(scope)); + + RedirectUri = oAuthConfiguration.RedirectUrl; + GrantType = "authorization_code"; } } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs index afdd635974..4e6028987e 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs @@ -54,7 +54,7 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public override async Task GetClientId(CancellationToken cancellationToken) + public override async Task GetProviderInfo(CancellationToken cancellationToken) { var expiredSessions = sessions.RemoveAll(x => x.Item2.AddMinutes(SessionRetentionMinutes) < DateTimeOffset.Now); if (expiredSessions > 0) @@ -65,7 +65,7 @@ namespace Tgstation.Server.Host.Security.OAuth { UriBuilder builder = new UriBuilder("https://tgstation13.org/phpBB/oauth_create_session.php") { - Query = $"site_private_token={HttpUtility.UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(OAuthConfiguration.ClientSecret)))}&return_uri={HttpUtility.UrlEncode(OAuthConfiguration.ClientId)}" + Query = $"site_private_token={HttpUtility.UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(OAuthConfiguration.ClientSecret)))}&return_uri={HttpUtility.UrlEncode(OAuthConfiguration.RedirectUrl.ToString())}" }; using var request = new HttpRequestMessage(HttpMethod.Get, builder.Uri); @@ -83,8 +83,15 @@ namespace Tgstation.Server.Host.Security.OAuth return null; } - sessions.Add(Tuple.Create(newSession, DateTimeOffset.Now)); - return newSession.SessionPublicToken; + sessions.Add( + Tuple.Create( + newSession, + DateTimeOffset.Now)); + return new OAuthProviderInfo + { + ClientId = newSession.SessionPublicToken, + RedirectUri = OAuthConfiguration.RedirectUrl + }; } catch (Exception ex) { diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json index 00506b5762..b0df570838 100644 --- a/src/Tgstation.Server.Host/appsettings.json +++ b/src/Tgstation.Server.Host/appsettings.json @@ -58,7 +58,8 @@ "OAuth": { "GitHub": null, "Discord": null, - "TGForums": null + "TGForums": null, + "Keycloak": null } } } diff --git a/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs b/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs index 3bf72f06e1..32c94dc8ae 100644 --- a/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs @@ -22,7 +22,6 @@ namespace Tgstation.Server.Tests.Instance public async Task Run(Task repositoryTask, CancellationToken cancellationToken) { - Assert.IsFalse(repositoryTask.IsCompleted); var deployJob = await dreamMakerClient.Compile(cancellationToken); deployJob = await WaitForJob(deployJob, 30, true, null, cancellationToken); Assert.IsTrue(deployJob.ErrorCode == ErrorCode.RepoCloning || deployJob.ErrorCode == ErrorCode.RepoMissing); diff --git a/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs b/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs index 02b3251622..1bdb2bb7fb 100644 --- a/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/RepositoryTest.cs @@ -22,28 +22,13 @@ namespace Tgstation.Server.Tests.Instance public async Task RunPreWatchdog(CancellationToken cancellationToken) { - const string GitHubRef = "TGS4_GITHUB_REF"; - var branchSourceEnvVars = new List - { - "TGS4_TEST_BRANCH", - "APPVEYOR_REPO_BRANCH", - "TRAVIS_BRANCH", - GitHubRef - }; - + const string TestRefEnvVar = "TGS4_GITHUB_REF"; + var envVar = Environment.GetEnvironmentVariable(TestRefEnvVar); string workingBranch = null; - foreach (var envVarName in branchSourceEnvVars) + if (!String.IsNullOrWhiteSpace(envVar)) { - var envVar = Environment.GetEnvironmentVariable(envVarName); - if (!String.IsNullOrWhiteSpace(envVar)) - { - if(envVarName == GitHubRef) - envVar = envVar.Substring("refs/heads/".Length); - - workingBranch = envVar; - Console.WriteLine($"TEST: Set working branch to '{workingBranch}' from env var '{envVarName}'"); - break; - } + workingBranch = envVar; + Console.WriteLine($"TEST: Set working branch to '{workingBranch}' from env var '{TestRefEnvVar}'"); } if (workingBranch == null) diff --git a/tests/Tgstation.Server.Tests/TestingServer.cs b/tests/Tgstation.Server.Tests/TestingServer.cs index 7eee0642a1..3459c2a11a 100644 --- a/tests/Tgstation.Server.Tests/TestingServer.cs +++ b/tests/Tgstation.Server.Tests/TestingServer.cs @@ -93,6 +93,7 @@ namespace Tgstation.Server.Tests { args.Add($"Security:OAuth:{I}:ClientId=Fake"); args.Add($"Security:OAuth:{I}:ClientSecret=Faker"); + args.Add($"Security:OAuth:{I}:Url=https://fakest.com"); } // SPECIFICALLY DELETE THE DEV APPSETTINGS, WE DON'T WANT IT IN THE WAY