forked from mirrors/jellyfin-plugin-sso
a167d00b73
ci/woodpecker/pr/ci Pipeline was successful
Keys each in-flight OpenID flow in the server's Valkey/Redis connection so a login can start on one replica and finish on another. Falls back to an in-memory store when no multiplexer is registered.
116 lines
3.4 KiB
C#
116 lines
3.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using Jellyfin.Plugin.SSO_Auth.Api;
|
|
using StackExchange.Redis;
|
|
|
|
namespace Jellyfin.Plugin.SSO_Auth.State;
|
|
|
|
/// <summary>
|
|
/// A Redis/Valkey backed <see cref="ISsoStateStore"/>. Every login-path operation addresses a single
|
|
/// key so the store works unchanged against a cluster-mode deployment, and expiry is a key TTL rather
|
|
/// than a sweep, so a state times out even if no further login ever happens.
|
|
/// </summary>
|
|
public sealed class RedisSsoStateStore : ISsoStateStore
|
|
{
|
|
/// <summary>
|
|
/// The prefix every OpenID state key is stored under.
|
|
/// </summary>
|
|
public const string KeyPrefix = "jellyfin:sso:oid:state:";
|
|
|
|
private readonly IConnectionMultiplexer _redis;
|
|
private readonly IDatabase _database;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="RedisSsoStateStore"/> class.
|
|
/// </summary>
|
|
/// <param name="redis">The connection multiplexer shared with the server.</param>
|
|
public RedisSsoStateStore(IConnectionMultiplexer redis)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(redis);
|
|
|
|
_redis = redis;
|
|
_database = redis.GetDatabase();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task CreateAsync(TimedAuthorizeState state, TimeSpan expiry)
|
|
{
|
|
return WriteAsync(state, expiry);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task UpdateAsync(TimedAuthorizeState state, TimeSpan expiry)
|
|
{
|
|
return WriteAsync(state, expiry);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<TimedAuthorizeState> GetAsync(string state)
|
|
{
|
|
if (string.IsNullOrEmpty(state))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var payload = await _database.StringGetAsync(KeyFor(state)).ConfigureAwait(false);
|
|
return SsoStateSerializer.Deserialize(payload);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<TimedAuthorizeState> ConsumeAsync(string state)
|
|
{
|
|
if (string.IsNullOrEmpty(state))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var payload = await _database.StringGetDeleteAsync(KeyFor(state)).ConfigureAwait(false);
|
|
return SsoStateSerializer.Deserialize(payload);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<TimedAuthorizeState>> ListAsync()
|
|
{
|
|
var states = new List<TimedAuthorizeState>();
|
|
|
|
foreach (var endpoint in _redis.GetEndPoints())
|
|
{
|
|
var server = _redis.GetServer(endpoint);
|
|
if (!server.IsConnected || server.IsReplica)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
await foreach (var key in server.KeysAsync(pattern: KeyPrefix + "*").ConfigureAwait(false))
|
|
{
|
|
var parsed = SsoStateSerializer.Deserialize(await _database.StringGetAsync(key).ConfigureAwait(false));
|
|
if (parsed is not null)
|
|
{
|
|
states.Add(parsed);
|
|
}
|
|
}
|
|
}
|
|
|
|
return states;
|
|
}
|
|
|
|
private static RedisKey KeyFor(string state)
|
|
{
|
|
return KeyPrefix + state;
|
|
}
|
|
|
|
private Task WriteAsync(TimedAuthorizeState state, TimeSpan expiry)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(state);
|
|
|
|
return _database.StringSetAsync(
|
|
KeyFor(state.State.State),
|
|
SsoStateSerializer.Serialize(state),
|
|
expiry,
|
|
keepTtl: false,
|
|
when: When.Always,
|
|
flags: CommandFlags.None);
|
|
}
|
|
}
|