From e512372c4cc8b3b1e70bf6a864c1a950e9ffe7fa Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 31 Oct 2021 01:29:14 -0400 Subject: [PATCH 1/5] Updates /tg/ forums OAuth support --- .../Security/OAuth/TGForumsOAuthValidator.cs | 114 ++---------------- 1 file changed, 10 insertions(+), 104 deletions(-) diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs index 309c479a07..d594f65476 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,20 +12,16 @@ 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. @@ -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"); } } From bccc3da82e441accb9ec5e9bd53f1b3b77b9b2ef Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 31 Oct 2021 01:35:54 -0400 Subject: [PATCH 2/5] Update documentation link for /tg/ OAuth --- docs/API.dox | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 4e5eb576a62692967931b008c442f5aae3262a98 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 31 Oct 2021 01:37:58 -0400 Subject: [PATCH 3/5] Removes unused classes --- .../Security/OAuth/TGBaseResponse.cs | 23 ------------------- .../Security/OAuth/TGCreateSessionResponse.cs | 18 --------------- .../OAuth/TGGetSessionInfoResponse.cs | 13 ----------- 3 files changed, 54 deletions(-) delete mode 100644 src/Tgstation.Server.Host/Security/OAuth/TGBaseResponse.cs delete mode 100644 src/Tgstation.Server.Host/Security/OAuth/TGCreateSessionResponse.cs delete mode 100644 src/Tgstation.Server.Host/Security/OAuth/TGGetSessionInfoResponse.cs 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/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; } - } -} From 44205afcaf9fe38a803cf86b0fd356c54b1b0007 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 31 Oct 2021 01:44:07 -0400 Subject: [PATCH 4/5] Fold BaseOAuthValidator into GenericOAuthValidator --- .../Security/OAuth/BaseOAuthValidator.cs | 103 ------------------ .../Security/OAuth/GenericOAuthValidator.cs | 80 ++++++++++++-- .../Security/OAuth/TGForumsOAuthValidator.cs | 8 +- 3 files changed, 72 insertions(+), 119 deletions(-) delete mode 100644 src/Tgstation.Server.Host/Security/OAuth/BaseOAuthValidator.cs 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/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/TGForumsOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs index d594f65476..1783a03993 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs @@ -26,10 +26,10 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// 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, From 464650a94a43a4c7eade3781c03afdb1fff1e243 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 31 Oct 2021 01:44:50 -0400 Subject: [PATCH 5/5] Reorganize DiscordOAuthValidator --- .../Security/OAuth/DiscordOAuthValidator.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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");