mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-29 16:11:05 +01:00
Merge pull request #1161 from tgstation/1160-KeyCloak
Adds Keycloak OAuth
This commit is contained in:
@@ -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' }}
|
||||
|
||||
@@ -133,10 +133,21 @@ Create an `appsettings.Production.json` file next to `appsettings.json`. This wi
|
||||
- `Security:<Provider Name>OAuth`: Sets the OAuth client ID and secret for a given `<Provider Name>`. 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
|
||||
|
||||
|
||||
+8
-7
@@ -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}
|
||||
{
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// The <see cref="OAuthProvider"/> of the <see cref="OAuthConnection"/>.
|
||||
/// </summary>]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
[EnumDataType(typeof(OAuthProvider))]
|
||||
public OAuthProvider Provider { get; set; }
|
||||
|
||||
|
||||
@@ -19,5 +19,10 @@ namespace Tgstation.Server.Api.Models
|
||||
/// https://tgstation13.org
|
||||
/// </summary>
|
||||
TGForums,
|
||||
|
||||
/// <summary>
|
||||
/// https://www.keycloak.org
|
||||
/// </summary>
|
||||
Keycloak,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Public information about a given <see cref="OAuthProvider"/>.
|
||||
/// </summary>
|
||||
public sealed class OAuthProviderInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The client ID.
|
||||
/// </summary>
|
||||
public string? ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The redirect URL.
|
||||
/// </summary>
|
||||
public Uri? RedirectUri { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,8 @@ namespace Tgstation.Server.Api.Models
|
||||
public Version? DMApiVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Map of <see cref="OAuthProvider"/> to the server's associated client IDs for them.
|
||||
/// Map of <see cref="OAuthProvider"/> to the <see cref="OAuthProviderInfo"/> for them.
|
||||
/// </summary>
|
||||
public IDictionary<OAuthProvider, string>? OAuthProviderClientIds { get; set; }
|
||||
public IDictionary<OAuthProvider, OAuthProviderInfo>? OAuthProviderInfos { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
|
||||
@@ -3,35 +3,18 @@ using System;
|
||||
namespace Tgstation.Server.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth options.
|
||||
/// OAuth configuration options.
|
||||
/// </summary>
|
||||
class OAuthConfiguration
|
||||
sealed class OAuthConfiguration : OAuthConfigurationBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The client ID.
|
||||
/// The client redirect URL. Not used by all providers.
|
||||
/// </summary>
|
||||
public string ClientId { get; set; }
|
||||
public Uri ServerUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The client secret.
|
||||
/// The authentication server URL. Not used by all providers.
|
||||
/// </summary>
|
||||
public string ClientSecret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OAuthConfiguration"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
public OAuthConfiguration() { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OAuthConfiguration"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="oAuthConfiguration">The <see cref="OAuthConfiguration"/> to copy settings from.</param>
|
||||
public OAuthConfiguration(OAuthConfiguration oAuthConfiguration)
|
||||
{
|
||||
if (oAuthConfiguration == null)
|
||||
throw new ArgumentNullException(nameof(oAuthConfiguration));
|
||||
ClientId = oAuthConfiguration.ClientId;
|
||||
ClientSecret = oAuthConfiguration.ClientSecret;
|
||||
}
|
||||
public Uri RedirectUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Base OAuth options.
|
||||
/// </summary>
|
||||
abstract class OAuthConfigurationBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The client ID.
|
||||
/// </summary>
|
||||
public string ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The client secret.
|
||||
/// </summary>
|
||||
public string ClientSecret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OAuthConfigurationBase"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
public OAuthConfigurationBase() { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OAuthConfigurationBase"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="oAuthConfiguration">The <see cref="OAuthConfigurationBase"/> to copy settings from.</param>
|
||||
public OAuthConfigurationBase(OAuthConfigurationBase oAuthConfiguration)
|
||||
{
|
||||
if (oAuthConfiguration == null)
|
||||
throw new ArgumentNullException(nameof(oAuthConfiguration));
|
||||
ClientId = oAuthConfiguration.ClientId;
|
||||
ClientSecret = oAuthConfiguration.ClientSecret;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace Tgstation.Server.Host.Security.OAuth
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<string> GetClientId(CancellationToken cancellationToken);
|
||||
public abstract Task<OAuthProviderInfo> GetProviderInfo(CancellationToken cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<string> ValidateResponseCode(string code, CancellationToken cancellationToken);
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Tgstation.Server.Host.Security.OAuth
|
||||
protected override Uri UserInformationUrl => new Uri("https://discord.com/api/users/@me");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override OAuthTokenRequest CreateTokenRequest(string code) => new DiscordTokenRequest(OAuthConfiguration, code);
|
||||
protected override OAuthTokenRequest CreateTokenRequest(string code) => new OAuthTokenRequest(OAuthConfiguration, code, "identify");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token;
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="OAuthTokenRequest"/> for Discord.
|
||||
/// </summary>
|
||||
/// <remarks>See https://discord.com/developers/docs/topics/oauth2</remarks>
|
||||
sealed class DiscordTokenRequest : OAuthTokenRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The 'grant_type' field.
|
||||
/// </summary>
|
||||
public string GrantType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The 'scope' field.
|
||||
/// </summary>
|
||||
public string Scope { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DiscordTokenRequest"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="oAuthConfiguration">The <see cref="OAuthConfiguration"/> for the <see cref="OAuthTokenRequest"/>.</param>
|
||||
/// <param name="code">The OAuth code for the <see cref="OAuthTokenRequest"/>.</param>
|
||||
public DiscordTokenRequest(OAuthConfiguration oAuthConfiguration, string code)
|
||||
: base(oAuthConfiguration, code)
|
||||
{
|
||||
GrantType = "authorization_code";
|
||||
Scope = "identify";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> GetClientId(CancellationToken cancellationToken) => Task.FromResult(OAuthConfiguration.ClientId);
|
||||
public override Task<OAuthProviderInfo> GetProviderInfo(CancellationToken cancellationToken) => Task.FromResult(
|
||||
new OAuthProviderInfo
|
||||
{
|
||||
ClientId = OAuthConfiguration.ClientId,
|
||||
RedirectUri = OAuthConfiguration.RedirectUrl
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> GetClientId(CancellationToken cancellationToken) => Task.FromResult(oAuthConfiguration.ClientId);
|
||||
public Task<OAuthProviderInfo> GetProviderInfo(CancellationToken cancellationToken) => Task.FromResult(
|
||||
new OAuthProviderInfo
|
||||
{
|
||||
ClientId = oAuthConfiguration.ClientId,
|
||||
RedirectUri = oAuthConfiguration.RedirectUrl
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Tgstation.Server.Host.Security.OAuth
|
||||
/// Gets a <see cref="Dictionary{TKey, TValue}"/> of the provider client IDs.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a anew <see cref="Dictionary{TKey, TValue}"/> of the active provider client IDs.</returns>
|
||||
Task<Dictionary<OAuthProvider, string>> ClientIds(CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a anew <see cref="Dictionary{TKey, TValue}"/> of the active <see cref="OAuthProviderInfo"/>s.</returns>
|
||||
Task<Dictionary<OAuthProvider, OAuthProviderInfo>> ProviderInfos(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,11 @@ namespace Tgstation.Server.Host.Security.OAuth
|
||||
OAuthProvider Provider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the OAuth client ID of validator.
|
||||
/// Gets the <see cref="OAuthProvider"/> of validator.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the client ID of the validator on success, <see langword="null"/> on failure.</returns>
|
||||
Task<string> GetClientId(CancellationToken cancellationToken);
|
||||
Task<OAuthProviderInfo> GetProviderInfo(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Validate a given OAuth response <paramref name="code"/>.
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// OAuth validator for Keycloak.
|
||||
/// </summary>
|
||||
sealed class KeycloakOAuthValidator : GenericOAuthValidator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override OAuthProvider Provider => OAuthProvider.Keycloak;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Uri TokenUrl => new Uri($"{BaseProtocolPath}/token");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Uri UserInformationUrl => new Uri($"{BaseProtocolPath}/userinfo");
|
||||
|
||||
/// <summary>
|
||||
/// Base path to the server's OAuth endpoint.
|
||||
/// </summary>
|
||||
string BaseProtocolPath => $"{OAuthConfiguration.ServerUrl}/protocol/openid-connect";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="KeycloakOAuthValidator"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for the <see cref="GenericOAuthValidator"/>.</param>
|
||||
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> for the <see cref="GenericOAuthValidator"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="GenericOAuthValidator"/>.</param>
|
||||
/// <param name="oAuthConfiguration">The <see cref="OAuthConfiguration"/> for the <see cref="GenericOAuthValidator"/>.</param>
|
||||
public KeycloakOAuthValidator(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
ILogger<KeycloakOAuthValidator> logger,
|
||||
OAuthConfiguration oAuthConfiguration)
|
||||
: base(httpClientFactory, assemblyInformationProvider, logger, oAuthConfiguration)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override OAuthTokenRequest CreateTokenRequest(string code) => new OAuthTokenRequest(OAuthConfiguration, code, "openid");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string DecodeUserInformationPayload(dynamic responseJson) => responseJson.sub;
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ namespace Tgstation.Server.Host.Security.OAuth
|
||||
loggerFactory.CreateLogger<DiscordOAuthValidator>(),
|
||||
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<TGForumsOAuthValidator>(),
|
||||
tgConfig));
|
||||
|
||||
if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.Keycloak, out var keyCloakConfig))
|
||||
validatorsBuilder.Add(
|
||||
new KeycloakOAuthValidator(
|
||||
httpClientFactory,
|
||||
assemblyInformationProvider,
|
||||
loggerFactory.CreateLogger<KeycloakOAuthValidator>(),
|
||||
keyCloakConfig));
|
||||
|
||||
validators = validatorsBuilder;
|
||||
}
|
||||
|
||||
@@ -73,11 +81,11 @@ namespace Tgstation.Server.Host.Security.OAuth
|
||||
public IOAuthValidator GetValidator(OAuthProvider oAuthProvider) => validators.FirstOrDefault(x => x.Provider == oAuthProvider);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Dictionary<OAuthProvider, string>> ClientIds(CancellationToken cancellationToken)
|
||||
public async Task<Dictionary<OAuthProvider, OAuthProviderInfo>> 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);
|
||||
|
||||
|
||||
@@ -6,22 +6,42 @@ namespace Tgstation.Server.Host.Security.OAuth
|
||||
/// <summary>
|
||||
/// Generic OAuth token request.
|
||||
/// </summary>
|
||||
class OAuthTokenRequest : OAuthConfiguration
|
||||
sealed class OAuthTokenRequest : OAuthConfigurationBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The OAuth code.
|
||||
/// The OAuth code received from the browser.
|
||||
/// </summary>
|
||||
public string Code { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OAuthTokenRequest"/>
|
||||
/// The scopes being requested.
|
||||
/// </summary>
|
||||
public string Scope { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The OAuth redirect URI.
|
||||
/// </summary>
|
||||
public Uri RedirectUri { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The OAuth grant type.
|
||||
/// </summary>
|
||||
public string GrantType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OAuthTokenRequest"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="oAuthConfiguration">The <see cref="OAuthConfiguration"/> to build from.</param>
|
||||
/// <param name="code">The OAuth code received from the browser.</param>
|
||||
public OAuthTokenRequest(OAuthConfiguration oAuthConfiguration, string code)
|
||||
/// <param name="code">The value of <see cref="Code"/>.</param>
|
||||
/// <param name="scope">The value of <see cref="Scope"/></param>
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace Tgstation.Server.Host.Security.OAuth
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<string> GetClientId(CancellationToken cancellationToken)
|
||||
public override async Task<OAuthProviderInfo> 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)
|
||||
{
|
||||
|
||||
@@ -58,7 +58,8 @@
|
||||
"OAuth": {
|
||||
"GitHub": null,
|
||||
"Discord": null,
|
||||
"TGForums": null
|
||||
"TGForums": null,
|
||||
"Keycloak": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string>
|
||||
{
|
||||
"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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user