Oauth cleanups

This commit is contained in:
Jordan Brown
2020-12-07 14:06:43 -05:00
parent 36044c826c
commit 3147ffd2bf
17 changed files with 102 additions and 58 deletions
+9 -4
View File
@@ -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
View File
@@ -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)
{