mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-07-12 16:43:01 +01:00
Oauth cleanups
This commit is contained in:
@@ -133,15 +133,20 @@ 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":{
|
||||
"Url": "...", (Used with certain providers)
|
||||
"ClientId": "...",
|
||||
"ClientSecret": "..."
|
||||
"ClientSecret": "...",
|
||||
"RedirectUrl": "...", (Used with certain providers)
|
||||
"ServerUrl": "...", (Used with certain providers)
|
||||
}
|
||||
```
|
||||
The following providers use the `RedirectUrl` setting:
|
||||
|
||||
The following providers use the `Url` setting:
|
||||
- GitHub
|
||||
- TGForums
|
||||
|
||||
- `TGForums`: Used as the OAuth redirect url.
|
||||
The following providers use the `ServerUrl` setting:
|
||||
|
||||
- None so far
|
||||
|
||||
### Database Configuration
|
||||
|
||||
|
||||
+4
-4
@@ -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:
|
||||
@@ -391,7 +391,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}
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
@@ -6,8 +8,13 @@ namespace Tgstation.Server.Host.Configuration
|
||||
sealed class OAuthConfiguration : OAuthConfigurationBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The redirect or server URL. Not used by all providers.
|
||||
/// The client redirect URL. Not used by all providers.
|
||||
/// </summary>
|
||||
public string Url { get; set; }
|
||||
public Uri ServerUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The authentication server URL. Not used by all providers.
|
||||
/// </summary>
|
||||
public Uri RedirectUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,27 +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>
|
||||
/// Initializes a new instance of the <see cref="DiscordTokenRequest"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="oAuthConfiguration">The <see cref="OAuthConfigurationBase"/> for the <see cref="OAuthTokenRequest"/>.</param>
|
||||
/// <param name="code">The OAuth code for the <see cref="OAuthTokenRequest"/>.</param>
|
||||
public DiscordTokenRequest(OAuthConfigurationBase oAuthConfiguration, string code)
|
||||
: base(oAuthConfiguration, code, "identify")
|
||||
{
|
||||
GrantType = "authorization_code";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -119,6 +120,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"/>.
|
||||
|
||||
@@ -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,
|
||||
@@ -73,11 +73,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,7 +6,7 @@ namespace Tgstation.Server.Host.Security.OAuth
|
||||
/// <summary>
|
||||
/// Generic OAuth token request.
|
||||
/// </summary>
|
||||
class OAuthTokenRequest : OAuthConfigurationBase
|
||||
sealed class OAuthTokenRequest : OAuthConfigurationBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The OAuth code received from the browser.
|
||||
@@ -18,17 +18,30 @@ namespace Tgstation.Server.Host.Security.OAuth
|
||||
/// </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 value of <see cref="Code"/>.</param>
|
||||
/// <param name="scope">The value of <see cref="Scope"/></param>
|
||||
public OAuthTokenRequest(OAuthConfigurationBase oAuthConfiguration, string code, string scope)
|
||||
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.Url)}"
|
||||
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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user