Merge pull request #2177 from tgstation/OidcQol

OIDC QoL
This commit is contained in:
Jordan Dominion
2025-03-24 21:35:36 -04:00
committed by GitHub
8 changed files with 45 additions and 21 deletions
+6 -5
View File
@@ -357,14 +357,15 @@ Gateway auth simply allows the users to authenticate with the service using the
```yml
Security:
OpenIDConnect:
Example:
Example: # Scheme key: Name of this OIDC scheme (public)
Authority: "..." # This is the root of the URL containing the "/.well-known/openid-configuration" endpoint
ClientId: "..."
ClientSecret: "..."
FriendlyName: "Example Provider" # Friendly name is how this provider is displayed in UIs
ThemeColour: "#ff0000" # (Optional) Hex color code indicating the color used to theme UI elements relating to this provider
ThemeIconUrl: "..." # (Optional) Public URL of an image that can be used to theme UI elements relating to this provider
UsernameClaim: "preferred_username" # (Optional) Name of claim used to set TGS usernames upon registration. By default "preferred_username" is used. Only applies when using OidcStrictMode.
FriendlyName: "Example Provider" # Friendly name is how this provider is displayed in UIs (public)
ThemeColour: "#ff0000" # (Optional) Hex color code indicating the color used to theme UI elements relating to this provider (public)
ThemeIconUrl: "..." # (Optional) Public URL of an image that can be used to theme UI elements relating to this provider (public)
UsernameClaim: "preferred_username" # (Optional) Name of claim used to set TGS usernames upon registration. By default "preferred_username" is used. Only applies when using OidcStrictMode.
GroupIdClaim: "tgstation-server-group-id" # (Options) Name of claim used to set TGS group ID upon logging in. By default "tgstation-server-group-id" is used. Only applies when using OidcStrictMode.
```
- `Security:OidcStrictMode`: Boolean flag that, when `true`, disables password and OAuth logins, password changes, individual permission set assignment, and enables user registration using OpenID Connect providers. The claim name `tgstation-server-group-id` is used to dictate what TGS group users are registered to.
+2 -2
View File
@@ -3,8 +3,8 @@
<!-- Integration tests will ensure they match across the board -->
<Import Project="WebpanelVersion.props" />
<PropertyGroup>
<TgsCoreVersion>6.15.2</TgsCoreVersion>
<TgsConfigVersion>5.6.0</TgsConfigVersion>
<TgsCoreVersion>6.16.0</TgsCoreVersion>
<TgsConfigVersion>5.7.0</TgsConfigVersion>
<TgsRestVersion>10.13.0</TgsRestVersion>
<TgsGraphQLVersion>0.6.0</TgsGraphQLVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
+1 -1
View File
@@ -1,6 +1,6 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!-- This is in it's own file to help incremental building, changing it causes a complete rebuild of the web panel -->
<TgsWebpanelVersion>6.9.3</TgsWebpanelVersion>
<TgsWebpanelVersion>6.9.5</TgsWebpanelVersion>
</PropertyGroup>
</Project>
@@ -9,6 +9,11 @@ namespace Tgstation.Server.Host.Configuration
/// </summary>
public sealed class OidcConfiguration
{
/// <summary>
/// The default value of <see cref="GroupIdClaim"/>.
/// </summary>
private const string DefaultGroupClaimName = "tgstation-server-group-id";
/// <summary>
/// The <see cref="Uri"/> containing the .well-known endpoint for the provider.
/// </summary>
@@ -48,5 +53,10 @@ namespace Tgstation.Server.Host.Configuration
/// The name of the claim used to set the user's name.
/// </summary>
public string UsernameClaim { get; set; } = JwtRegisteredClaimNames.PreferredUsername;
/// <summary>
/// Claim name used to set user groups in OIDC strict mode.
/// </summary>
public string GroupIdClaim { get; set; } = DefaultGroupClaimName;
}
}
@@ -916,6 +916,7 @@ namespace Tgstation.Server.Host.Core
.ValidateOidcToken(
context,
configName,
config.GroupIdClaim,
context
.HttpContext
.RequestAborted);
@@ -923,7 +924,7 @@ namespace Tgstation.Server.Host.Core
if (securityConfiguration.OidcStrictMode)
{
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapUniqueJsonKey(AuthenticationContextFactory.TgsGroupIdClaimName, AuthenticationContextFactory.TgsGroupIdClaimName);
options.ClaimActions.MapUniqueJsonKey(config.GroupIdClaim, config.GroupIdClaim);
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
NameClaimType = config.UsernameClaim,
@@ -32,11 +32,6 @@ namespace Tgstation.Server.Host.Security
/// </summary>
public const string OpenIDConnectAuthenticationSchemePrefix = $"{OpenIdConnectDefaults.AuthenticationScheme}.";
/// <summary>
/// Claim name used to set user groups in OIDC strict mode.
/// </summary>
public const string TgsGroupIdClaimName = "tgstation-server-group-id";
/// <summary>
/// The <see cref="IAuthenticationContext"/> the <see cref="AuthenticationContextFactory"/> created.
/// </summary>
@@ -233,7 +228,9 @@ namespace Tgstation.Server.Host.Security
}
/// <inheritdoc />
public async Task ValidateOidcToken(RemoteAuthenticationContext<OpenIdConnectOptions> tokenValidatedContext, string schemeKey, CancellationToken cancellationToken)
#pragma warning disable CA1506 // TODO: Decomplexify
public async Task ValidateOidcToken(RemoteAuthenticationContext<OpenIdConnectOptions> tokenValidatedContext, string schemeKey, string groupIdClaimName, CancellationToken cancellationToken)
#pragma warning restore CA1506
{
ArgumentNullException.ThrowIfNull(tokenValidatedContext);
@@ -270,7 +267,7 @@ namespace Tgstation.Server.Host.Security
}
else
{
var groupClaim = principal.FindFirst(TgsGroupIdClaimName);
var groupClaim = principal.FindFirst(groupIdClaimName);
long? groupId;
if (groupClaim == default)
groupId = null;
@@ -278,7 +275,7 @@ namespace Tgstation.Server.Host.Security
groupId = groupIdParsed;
else
{
tokenValidatedContext.Fail($"User has non-numeric '{TgsGroupIdClaimName}' claim!");
tokenValidatedContext.Fail($"User has non-numeric '{groupIdClaimName}' claim!");
return;
}
@@ -291,7 +288,7 @@ namespace Tgstation.Server.Host.Security
.FirstOrDefaultAsync(cancellationToken)
: null;
var missingClaimError = $"User missing '{TgsGroupIdClaimName}' claim!";
var missingClaimError = $"User missing '{groupIdClaimName}' claim!";
if (connection == default)
{
var username = principal.Identity?.Name;
@@ -311,11 +308,13 @@ namespace Tgstation.Server.Host.Security
{
tokenValidatedContext.Fail(
groupId.HasValue
? $"'{TgsGroupIdClaimName}' does not point to a valid group!"
? $"'{groupIdClaimName}' does not point to a valid group!"
: missingClaimError);
return;
}
logger.LogInformation("Registering new user '{name}' via OIDC scheme '{scheme}'", username, schemeKey);
var tgsUser = await databaseContext
.Users
.GetTgsUser(
@@ -335,7 +334,7 @@ namespace Tgstation.Server.Host.Security
GroupId = group.Id,
OidcConnections = new List<OidcConnection>
{
new OidcConnection
new()
{
SchemeKey = schemeKey,
ExternalUserId = userId,
@@ -353,6 +352,7 @@ namespace Tgstation.Server.Host.Security
// group update
if (group == null)
{
logger.LogDebug("User {id} attempted to login via OIDC scheme '{scheme}' but had no group ID claim ('{groupClaimName}') and will be disabled", user.Id, schemeKey, groupIdClaimName);
user.PermissionSet = new PermissionSet
{
AdministrationRights = AdministrationRights.None,
@@ -365,6 +365,7 @@ namespace Tgstation.Server.Host.Security
return;
}
logger.LogDebug("User {id} mapped to group {groupId} via OIDC login on scheme '{scheme}'", user.Id, groupId, schemeKey);
user.Group = group;
if (user.PermissionSet != null)
databaseContext.PermissionSets.Remove(user.PermissionSet);
@@ -24,8 +24,9 @@ namespace Tgstation.Server.Host.Security
/// </summary>
/// <param name="tokenValidatedContext">The <see cref="RemoteAuthenticationContext{TOptions}"/> for <see cref="OpenIdConnectOptions"/>.</param>
/// <param name="schemeKey">The scheme key being used to login.</param>
/// <param name="groupIdClaimName">The name of the <see cref="global::System.Security.Claims.Claim"/> used to set the user's group ID.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task ValidateOidcToken(RemoteAuthenticationContext<OpenIdConnectOptions> tokenValidatedContext, string schemeKey, CancellationToken cancellationToken);
Task ValidateOidcToken(RemoteAuthenticationContext<OpenIdConnectOptions> tokenValidatedContext, string schemeKey, string groupIdClaimName, CancellationToken cancellationToken);
}
}
+10
View File
@@ -74,6 +74,16 @@ Security:
TGForums: # https://tgstation13.org OAuth configuration
Keycloak: # Keycloak OAuth configuration.
InvisionCommunity: # Invision Community OAuth configuration.
# OpenIDConnect:
# Example: # Scheme key: Name of this OIDC scheme (public)
# Authority: "..." # This is the root of the URL containing the "/.well-known/openid-configuration" endpoint
# ClientId: "..."
# ClientSecret: "..."
# FriendlyName: "Example Provider" # Friendly name is how this provider is displayed in UIs (public)
# ThemeColour: "#ff0000" # (Optional) Hex color code indicating the color used to theme UI elements relating to this provider (public)
# ThemeIconUrl: "..." # (Optional) Public URL of an image that can be used to theme UI elements relating to this provider (public)
# UsernameClaim: "preferred_username" # (Optional) Name of claim used to set TGS usernames upon registration. By default "preferred_username" is used. Only applies when using OidcStrictMode.
# GroupIdClaim: "tgstation-server-group-id" # (Options) Name of claim used to set TGS group ID upon logging in. By default "tgstation-server-group-id" is used. Only applies when using OidcStrictMode.
Swarm: # Should be left empty if using swarm mode is not desired
# Identifier: # Required: The string identifier of the swarm node
# PrivateKey: # Required: The shared API key used for inter-server communication in the swarm. Must be the same secure string on all nodes