Code/Auth/NetworkStorageAuthTokens.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Sandbox;
public static partial class NetworkStorage
{
private static DateTimeOffset _lastAuthTokenLookupFailedAt;
private static readonly TimeSpan FailedAuthLookupCooldown = TimeSpan.FromSeconds( 5 );
private static readonly TimeSpan PreparedAuthTokenLifetime = TimeSpan.FromSeconds( 10 );
private static readonly object _preparedAuthTokensLock = new();
private static readonly Queue<PreparedAuthTokenEntry> _preparedAuthTokens = new();
private sealed class PreparedAuthTokenEntry
{
public string Token { get; init; }
public DateTimeOffset CreatedAt { get; init; }
}
private static void InvalidateCachedAuthToken( string reason )
{
if ( NetworkStorageLogConfig.LogTokens )
Log.Warning( $"[NetworkStorage] Clearing cached auth token: {reason}" );
lock ( _preparedAuthTokensLock )
_preparedAuthTokens.Clear();
}
public static async Task<bool> EnsureEndpointAuthAsync( string slug )
{
var endpoint = ResolveEndpointReference( slug );
ApplyEndpointReferenceConfiguration( endpoint );
slug = endpoint.Slug;
EnsureConfigured();
ClearLastEndpointError( slug );
// Dedicated-server secret keys are trusted server/backend auth. Dedicated
// servers do not have player s&box tokens, so never fall back to token auth.
if ( ShouldUseDedicatedServerSecret( endpoint ) )
return true;
if ( TryRejectDedicatedServerPlayerAuth( slug ) )
return false;
var steamId = Game.SteamId.ToString();
var token = await GetAuthTokenWithRetry( $"steamId={steamId}" );
if ( !string.IsNullOrWhiteSpace( token ) )
{
RememberPreparedAuthToken( token );
return true;
}
RecordEndpointError(
slug,
"SBOX_AUTH_FAILED",
$"Missing token or steamId. token=NO steamId={steamId} This endpoint requires a valid s&box player token." );
return false;
}
private static void RememberPreparedAuthToken( string token )
{
if ( string.IsNullOrWhiteSpace( token ) )
return;
lock ( _preparedAuthTokensLock )
{
_preparedAuthTokens.Enqueue( new PreparedAuthTokenEntry
{
Token = token,
CreatedAt = DateTimeOffset.UtcNow
} );
}
}
private static string TryTakePreparedAuthToken()
{
lock ( _preparedAuthTokensLock )
{
while ( _preparedAuthTokens.Count > 0 )
{
var next = _preparedAuthTokens.Dequeue();
if ( next is not null
&& !string.IsNullOrWhiteSpace( next.Token )
&& DateTimeOffset.UtcNow - next.CreatedAt < PreparedAuthTokenLifetime )
{
return next.Token;
}
}
}
return null;
}
/// <summary>
/// Auth tokens can lag briefly behind startup, especially in editor flows.
/// Retry a few times before treating the request as unauthenticated.
/// </summary>
private static async Task<string> GetAuthTokenWithRetry( string context, int attempts = 6, int delayMs = 500 )
{
if ( IsDedicatedServerProcess )
{
LogDedicatedPlayerAuthSuppressedOnce();
return null;
}
if ( _lastAuthTokenLookupFailedAt != default
&& DateTimeOffset.UtcNow - _lastAuthTokenLookupFailedAt < FailedAuthLookupCooldown )
{
return null;
}
string lastError = null;
for ( int attempt = 1; attempt <= attempts; attempt++ )
{
try
{
var token = await Services.Auth.GetToken( "sbox-network-storage" );
if ( !string.IsNullOrWhiteSpace( token ) )
{
_lastAuthTokenLookupFailedAt = default;
if ( attempt > 1 && NetworkStorageLogConfig.LogTokens )
Log.Info( $"[NetworkStorage] Auth token acquired for {context} after retry {attempt}/{attempts}" );
return token;
}
}
catch ( Exception ex )
{
lastError = ex.Message;
}
if ( attempt < attempts )
await Task.Delay( delayMs );
}
if ( NetworkStorageLogConfig.LogTokens )
{
if ( !string.IsNullOrEmpty( lastError ) )
Log.Warning( $"[NetworkStorage] Failed to get auth token for {context} after {attempts} attempts: {lastError}" );
else
Log.Warning( $"[NetworkStorage] Auth token remained empty for {context} after {attempts} attempts" );
}
_lastAuthTokenLookupFailedAt = DateTimeOffset.UtcNow;
return null;
}
}