diff --git a/docs/API.dox b/docs/API.dox
index bb0167c9d1..49779e1f2d 100644
--- a/docs/API.dox
+++ b/docs/API.dox
@@ -146,7 +146,7 @@ You will be granted a bearer token as in basic auth. This will have an extended
- 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
+- TGForums: https://tgstation13.org/phpBB/viewtopic.php?f=45&t=30155
- Keycloak: https://plugins.miniorange.com/keycloak-single-sign-on-wordpress-sso-oauth-openid-connect
@section api_perms Permissions
diff --git a/src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs
deleted file mode 100644
index 9040b6dd5a..0000000000
--- a/src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-using System;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Net.Mime;
-using System.Threading;
-using System.Threading.Tasks;
-
-using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Serialization;
-
-using Tgstation.Server.Api.Models;
-using Tgstation.Server.Host.Configuration;
-using Tgstation.Server.Host.System;
-
-namespace Tgstation.Server.Host.Security.OAuth
-{
- ///
- /// Base for s.
- ///
- abstract class BaseOAuthValidator : IOAuthValidator
- {
- ///
- public abstract OAuthProvider Provider { get; }
-
- ///
- /// The for the .
- ///
- protected ILogger Logger { get; }
-
- ///
- /// The for the .
- ///
- protected OAuthConfiguration OAuthConfiguration { get; }
-
- ///
- /// The for the .
- ///
- readonly IHttpClientFactory httpClientFactory;
-
- ///
- /// The for the .
- ///
- readonly IAssemblyInformationProvider assemblyInformationProvider;
-
- ///
- /// Gets that should be used.
- ///
- /// A new .
- protected static JsonSerializerSettings SerializerSettings() => new JsonSerializerSettings
- {
- ContractResolver = new DefaultContractResolver
- {
- NamingStrategy = new SnakeCaseNamingStrategy(),
- },
- };
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The value of .
- /// The value of .
- /// The value of .
- /// The value of .
- public BaseOAuthValidator(
- IHttpClientFactory httpClientFactory,
- IAssemblyInformationProvider assemblyInformationProvider,
- ILogger logger,
- OAuthConfiguration oAuthConfiguration)
- {
- this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
- this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
- Logger = logger ?? throw new ArgumentNullException(nameof(logger));
- OAuthConfiguration = oAuthConfiguration ?? throw new ArgumentNullException(nameof(oAuthConfiguration));
- }
-
- ///
- public abstract Task GetProviderInfo(CancellationToken cancellationToken);
-
- ///
- public abstract Task ValidateResponseCode(string code, CancellationToken cancellationToken);
-
- ///
- /// Create a new configured .
- ///
- /// A new configured .
- protected HttpClient CreateHttpClient()
- {
- var httpClient = httpClientFactory.CreateClient();
- try
- {
- httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
- httpClient.DefaultRequestHeaders.UserAgent.Add(assemblyInformationProvider.ProductInfoHeaderValue);
- return httpClient;
- }
- catch
- {
- httpClient.Dispose();
- throw;
- }
- }
- }
-}
diff --git a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs
index d28bc9d04f..e74a6c9d4b 100644
--- a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs
+++ b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs
@@ -14,6 +14,15 @@ namespace Tgstation.Server.Host.Security.OAuth
///
sealed class DiscordOAuthValidator : GenericOAuthValidator
{
+ ///
+ public override OAuthProvider Provider => OAuthProvider.Discord;
+
+ ///
+ protected override Uri TokenUrl => new Uri("https://discord.com/api/oauth2/token");
+
+ ///
+ protected override Uri UserInformationUrl => new Uri("https://discord.com/api/users/@me");
+
///
/// Initializes a new instance of the class.
///
@@ -30,15 +39,6 @@ namespace Tgstation.Server.Host.Security.OAuth
{
}
- ///
- public override OAuthProvider Provider => OAuthProvider.Discord;
-
- ///
- protected override Uri TokenUrl => new Uri("https://discord.com/api/oauth2/token");
-
- ///
- protected override Uri UserInformationUrl => new Uri("https://discord.com/api/users/@me");
-
///
protected override OAuthTokenRequest CreateTokenRequest(string code) => new OAuthTokenRequest(OAuthConfiguration, code, "identify");
diff --git a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs
index aa834e38a7..9582d915f3 100644
--- a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs
+++ b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs
@@ -2,12 +2,14 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
+using Newtonsoft.Json.Serialization;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
@@ -19,8 +21,21 @@ namespace Tgstation.Server.Host.Security.OAuth
///
/// for generic OAuth2 endpoints.
///
- abstract class GenericOAuthValidator : BaseOAuthValidator
+ abstract class GenericOAuthValidator : IOAuthValidator
{
+ ///
+ public abstract OAuthProvider Provider { get; }
+
+ ///
+ /// The for the .
+ ///
+ protected ILogger Logger { get; }
+
+ ///
+ /// The for the .
+ ///
+ protected OAuthConfiguration OAuthConfiguration { get; }
+
///
/// to to to get the access token.
///
@@ -31,28 +46,49 @@ namespace Tgstation.Server.Host.Security.OAuth
///
protected abstract Uri UserInformationUrl { get; }
+ ///
+ /// The for the .
+ ///
+ readonly IHttpClientFactory httpClientFactory;
+
+ ///
+ /// The for the .
+ ///
+ readonly IAssemblyInformationProvider assemblyInformationProvider;
+
+ ///
+ /// Gets that should be used.
+ ///
+ /// A new .
+ protected static JsonSerializerSettings SerializerSettings() => new JsonSerializerSettings
+ {
+ ContractResolver = new DefaultContractResolver
+ {
+ NamingStrategy = new SnakeCaseNamingStrategy(),
+ },
+ };
+
///
/// Initializes a new instance of the class.
///
- /// The for the .
- /// The for the .
- /// The for the .
- /// The for the .
+ /// The value of .
+ /// The value of .
+ /// The value of .
+ /// The value of .
public GenericOAuthValidator(
IHttpClientFactory httpClientFactory,
IAssemblyInformationProvider assemblyInformationProvider,
ILogger logger,
OAuthConfiguration oAuthConfiguration)
- : base(
- httpClientFactory,
- assemblyInformationProvider,
- logger,
- oAuthConfiguration)
{
+ this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
+ this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
+ Logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ OAuthConfiguration = oAuthConfiguration ?? throw new ArgumentNullException(nameof(oAuthConfiguration));
}
///
- public override async Task ValidateResponseCode(string code, CancellationToken cancellationToken)
+ public async Task ValidateResponseCode(string code, CancellationToken cancellationToken)
{
using var httpClient = CreateHttpClient();
string tokenResponsePayload = null;
@@ -110,7 +146,7 @@ namespace Tgstation.Server.Host.Security.OAuth
}
///
- public override Task GetProviderInfo(CancellationToken cancellationToken) => Task.FromResult(
+ public Task GetProviderInfo(CancellationToken cancellationToken) => Task.FromResult(
new OAuthProviderInfo
{
ClientId = OAuthConfiguration.ClientId,
@@ -138,5 +174,25 @@ namespace Tgstation.Server.Host.Security.OAuth
/// The OAuth code from the browser.
/// The to send to .
protected abstract OAuthTokenRequest CreateTokenRequest(string code);
+
+ ///
+ /// Create a new configured .
+ ///
+ /// A new configured .
+ HttpClient CreateHttpClient()
+ {
+ var httpClient = httpClientFactory.CreateClient();
+ try
+ {
+ httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
+ httpClient.DefaultRequestHeaders.UserAgent.Add(assemblyInformationProvider.ProductInfoHeaderValue);
+ return httpClient;
+ }
+ catch
+ {
+ httpClient.Dispose();
+ throw;
+ }
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGBaseResponse.cs b/src/Tgstation.Server.Host/Security/OAuth/TGBaseResponse.cs
deleted file mode 100644
index 0dd50cf814..0000000000
--- a/src/Tgstation.Server.Host/Security/OAuth/TGBaseResponse.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace Tgstation.Server.Host.Security.OAuth
-{
- ///
- /// Base for tgstation forum responses.
- ///
- abstract class TGBaseResponse
- {
- ///
- /// Expected value of .
- ///
- public const string OkStatus = "OK";
-
- ///
- /// The response status.
- ///
- public string Status { get; set; }
-
- ///
- /// The response error, if any.
- ///
- public string Error { get; set; }
- }
-}
diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGCreateSessionResponse.cs b/src/Tgstation.Server.Host/Security/OAuth/TGCreateSessionResponse.cs
deleted file mode 100644
index a012ada04f..0000000000
--- a/src/Tgstation.Server.Host/Security/OAuth/TGCreateSessionResponse.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace Tgstation.Server.Host.Security.OAuth
-{
- ///
- /// Response when creating a tgstation forums session.
- ///
- sealed class TGCreateSessionResponse : TGBaseResponse
- {
- ///
- /// The session's private token. Similar to OAuth authorization response code.
- ///
- public string SessionPrivateToken { get; set; }
-
- ///
- /// The session's public token. Barely similar to OAuth client ID.
- ///
- public string SessionPublicToken { get; set; }
- }
-}
diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs
index 309c479a07..1783a03993 100644
--- a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs
+++ b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs
@@ -1,14 +1,7 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Net.Http;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Web;
using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Configuration;
@@ -19,28 +12,24 @@ namespace Tgstation.Server.Host.Security.OAuth
///
/// for /tg/ forums.
///
- sealed class TGForumsOAuthValidator : BaseOAuthValidator
+ sealed class TGForumsOAuthValidator : GenericOAuthValidator
{
- ///
- /// Amount of minutes until unused sessions that were created are forgotten.
- ///
- const uint SessionRetentionMinutes = 10;
-
///
public override OAuthProvider Provider => OAuthProvider.TGForums;
- ///
- /// The active session.
- ///
- readonly List> sessions;
+ ///
+ protected override Uri TokenUrl => new Uri("https://tgstation13.org/phpBB/app.php/tgapi/oauth/token");
+
+ ///
+ protected override Uri UserInformationUrl => new Uri("https://tgstation13.org/phpBB/app.php/tgapi/user/me");
///
/// Initializes a new instance of the class.
///
- /// The for the .
- /// The for the .
- /// The for the .
- /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
public TGForumsOAuthValidator(
IHttpClientFactory httpClientFactory,
IAssemblyInformationProvider assemblyInformationProvider,
@@ -52,98 +41,15 @@ namespace Tgstation.Server.Host.Security.OAuth
logger,
oAuthConfiguration)
{
- sessions = new List>();
}
///
- public override async Task GetProviderInfo(CancellationToken cancellationToken)
- {
- var expiredSessions = sessions.RemoveAll(x => x.Item2.AddMinutes(SessionRetentionMinutes) < DateTimeOffset.UtcNow);
- if (expiredSessions > 0)
- Logger.LogTrace("Expired {0} sessions", expiredSessions);
-
- Logger.LogTrace("Creating new session...");
- try
- {
- 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.RedirectUrl.ToString())}",
- };
-
- using var request = new HttpRequestMessage(HttpMethod.Get, builder.Uri);
- using var httpClient = CreateHttpClient();
-
- using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
-
- var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
- var newSession = JsonConvert.DeserializeObject(json, SerializerSettings());
-
- if (newSession.Status != TGBaseResponse.OkStatus)
- {
- Logger.LogWarning("Invalid status from /tg/ API! Status: {0}, Error: {1}", newSession.Status, newSession.Error);
- return null;
- }
-
- sessions.Add(
- Tuple.Create(
- newSession,
- DateTimeOffset.UtcNow));
- return new OAuthProviderInfo
- {
- ClientId = newSession.SessionPublicToken,
- RedirectUri = OAuthConfiguration.RedirectUrl,
- };
- }
- catch (Exception ex)
- {
- Logger.LogWarning(ex, "Failed to create TG Forums session!");
- return null;
- }
- }
+ protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token;
///
- public override async Task ValidateResponseCode(string code, CancellationToken cancellationToken)
- {
- try
- {
- var sessionTuple = sessions.FirstOrDefault(x => x.Item1.SessionPublicToken == code);
- if (sessionTuple == null)
- {
- Logger.LogWarning("No known session with this code active!");
- return null;
- }
+ protected override string DecodeUserInformationPayload(dynamic responseJson) => responseJson.phpbb_username;
- Logger.LogTrace("Validating session...");
-
- UriBuilder builder = new UriBuilder("https://tgstation13.org/phpBB/oauth_get_session_info.php")
- {
- Query = $"site_private_token={HttpUtility.UrlEncode(Convert.ToBase64String(Encoding.UTF8.GetBytes(OAuthConfiguration.ClientSecret)))}&session_private_token={HttpUtility.UrlEncode(sessionTuple.Item1.SessionPrivateToken)}",
- };
-
- using var request = new HttpRequestMessage(HttpMethod.Get, builder.Uri);
- using var httpClient = CreateHttpClient();
-
- using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
-
- var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
- var sessionInfo = JsonConvert.DeserializeObject(json, SerializerSettings());
-
- if (sessionInfo.Status != TGBaseResponse.OkStatus)
- {
- Logger.LogWarning("Invalid status from /tg/ API! Status: {0}, Error: {1}", sessionInfo.Status, sessionInfo.Error);
- return null;
- }
-
- sessions.Remove(sessionTuple);
- return sessionInfo.PhpbbUsername;
- }
- catch (Exception ex)
- {
- Logger.LogWarning(ex, "Failed to create TG Forums session!");
- return null;
- }
- }
+ ///
+ protected override OAuthTokenRequest CreateTokenRequest(string code) => new OAuthTokenRequest(OAuthConfiguration, code, "user");
}
}
diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGGetSessionInfoResponse.cs b/src/Tgstation.Server.Host/Security/OAuth/TGGetSessionInfoResponse.cs
deleted file mode 100644
index 62a630a38b..0000000000
--- a/src/Tgstation.Server.Host/Security/OAuth/TGGetSessionInfoResponse.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace Tgstation.Server.Host.Security.OAuth
-{
- ///
- /// Response when getting tgstation forum user's info.
- ///
- sealed class TGGetSessionInfoResponse : TGBaseResponse
- {
- ///
- /// The user's forum account name.
- ///
- public string PhpbbUsername { get; set; }
- }
-}