Allow group ID claim name to be configurable

This commit is contained in:
Jordan Dominion
2025-03-23 20:32:20 -04:00
parent 12e22939a9
commit 59cd745676
7 changed files with 40 additions and 18 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.
+1 -1
View File
@@ -4,7 +4,7 @@
<Import Project="WebpanelVersion.props" />
<PropertyGroup>
<TgsCoreVersion>6.15.2</TgsCoreVersion>
<TgsConfigVersion>5.6.0</TgsConfigVersion>
<TgsConfigVersion>5.7.0</TgsConfigVersion>
<TgsRestVersion>10.13.0</TgsRestVersion>
<TgsGraphQLVersion>0.6.0</TgsGraphQLVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
@@ -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,7 @@ namespace Tgstation.Server.Host.Security
}
/// <inheritdoc />
public async Task ValidateOidcToken(RemoteAuthenticationContext<OpenIdConnectOptions> tokenValidatedContext, string schemeKey, CancellationToken cancellationToken)
public async Task ValidateOidcToken(RemoteAuthenticationContext<OpenIdConnectOptions> tokenValidatedContext, string schemeKey, string groupIdClaimName, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(tokenValidatedContext);
@@ -270,7 +265,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 +273,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 +286,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 +306,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(
@@ -353,6 +350,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 +363,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