mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
* feat(auth): add per-user token_epoch column and bump method * feat(auth): add aud and ep claims, omitted when zero * feat(auth): add CreateAPIToken for non-expiring, audience-scoped tokens * feat(auth): add CheckClaims for epoch and audience validation * feat(jellyfin): issue non-expiring, jellyfin-scoped access tokens * fix(subsonic): reject API-scoped and revoked tokens on the jwt path * fix(server): reject API-scoped and revoked tokens on the native API * fix(server): pin the token-subject guard and stop leaking test config Adds a regression spec for the DevAutoLogin/ExtAuth guard in tokenAllowed, switches its comparison to case-insensitive to match the user lookup's own COLLATE NOCASE semantics, and restores Subsonic JWT test config after each spec instead of leaking SessionTimeout. * feat(request): add a token epoch holder for handler-to-middleware signalling * refactor(server): write the refreshed JWT header after the handler runs * feat(auth): revoke all tokens for a user when their password changes * fix(server): restore Unwrap on the JWT refresh writer so SSE write deadlines apply * test(auth): pin that non-session tokens reject API access tokens * test(jellyfin): pin token scoping and epoch revocation end to end Exercises auth.CreateAPIToken and CheckClaims against the real Jellyfin router and SQLite DB: the minted token has no exp and is aud-scoped to jellyfin, and bumping token_epoch through the real UserRepository revokes an already-issued token on the next protected request. * test(nativeapi): pin the token-epoch handoff through a real password-change request Drive a self password change through the real Authenticator/JWTRefresher chain and a real SQLite-backed userRepository, so the epoch handoff between Put and the refreshed-token writer is verified end to end, not as two separately-tested halves. Also fix tokenAllowed to read the enriched ctx it was given instead of r.Context(), so its warning log carries the username. * refactor(server): drop tokenAllowed's now-unused request parameter Finding-2 already moved every use to ctx; r was dead weight. Also note in the new nativeapi test why it must stay the package's only real-DB spec: db.Db() is a process-wide singleton its cleanup closes for good. * refactor(auth): remove duplication in claim decoding and token minting * refactor(auth): group aud with the standard JWT claims * refactor(auth): read aud with the standard-claim accessor pattern * fix(log): redact every api_key spelling the Jellyfin API accepts * fix(auth): bind session tokens to the user id, not just the username * fix(auth): return the token epoch from the same atomic increment * fix(auth): bump the token epoch in the same statement as the password write * chore(auth): trim comments to the why-only budget
159 lines
4.0 KiB
Go
159 lines
4.0 KiB
Go
package request
|
|
|
|
import (
|
|
"context"
|
|
"sync/atomic"
|
|
|
|
"github.com/navidrome/navidrome/model"
|
|
)
|
|
|
|
type contextKey string
|
|
|
|
const (
|
|
User = contextKey("user")
|
|
Username = contextKey("username")
|
|
Client = contextKey("client")
|
|
Version = contextKey("version")
|
|
Player = contextKey("player")
|
|
Transcoding = contextKey("transcoding")
|
|
ClientUniqueId = contextKey("clientUniqueId")
|
|
ReverseProxyIp = contextKey("reverseProxyIp")
|
|
InternalAuth = contextKey("internalAuth") // Used for internal API calls, e.g., from the plugins
|
|
TokenEpochHolder = contextKey("tokenEpochHolder")
|
|
)
|
|
|
|
var allKeys = []contextKey{
|
|
User,
|
|
Username,
|
|
Client,
|
|
Version,
|
|
Player,
|
|
Transcoding,
|
|
ClientUniqueId,
|
|
ReverseProxyIp,
|
|
InternalAuth,
|
|
}
|
|
|
|
func WithUser(ctx context.Context, u model.User) context.Context {
|
|
return context.WithValue(ctx, User, u)
|
|
}
|
|
|
|
func WithUsername(ctx context.Context, username string) context.Context {
|
|
return context.WithValue(ctx, Username, username)
|
|
}
|
|
|
|
func WithClient(ctx context.Context, client string) context.Context {
|
|
return context.WithValue(ctx, Client, client)
|
|
}
|
|
|
|
func WithVersion(ctx context.Context, version string) context.Context {
|
|
return context.WithValue(ctx, Version, version)
|
|
}
|
|
|
|
func WithPlayer(ctx context.Context, player model.Player) context.Context {
|
|
return context.WithValue(ctx, Player, player)
|
|
}
|
|
|
|
func WithTranscoding(ctx context.Context, t model.Transcoding) context.Context {
|
|
return context.WithValue(ctx, Transcoding, t)
|
|
}
|
|
|
|
func WithClientUniqueId(ctx context.Context, clientUniqueId string) context.Context {
|
|
return context.WithValue(ctx, ClientUniqueId, clientUniqueId)
|
|
}
|
|
|
|
func WithReverseProxyIp(ctx context.Context, reverseProxyIp string) context.Context {
|
|
return context.WithValue(ctx, ReverseProxyIp, reverseProxyIp)
|
|
}
|
|
|
|
func WithInternalAuth(ctx context.Context, username string) context.Context {
|
|
return context.WithValue(ctx, InternalAuth, username)
|
|
}
|
|
|
|
func UserFrom(ctx context.Context) (model.User, bool) {
|
|
v, ok := ctx.Value(User).(model.User)
|
|
return v, ok
|
|
}
|
|
|
|
func UsernameFrom(ctx context.Context) (string, bool) {
|
|
v, ok := ctx.Value(Username).(string)
|
|
return v, ok
|
|
}
|
|
|
|
func ClientFrom(ctx context.Context) (string, bool) {
|
|
v, ok := ctx.Value(Client).(string)
|
|
return v, ok
|
|
}
|
|
|
|
func VersionFrom(ctx context.Context) (string, bool) {
|
|
v, ok := ctx.Value(Version).(string)
|
|
return v, ok
|
|
}
|
|
|
|
func PlayerFrom(ctx context.Context) (model.Player, bool) {
|
|
v, ok := ctx.Value(Player).(model.Player)
|
|
return v, ok
|
|
}
|
|
|
|
func TranscodingFrom(ctx context.Context) (model.Transcoding, bool) {
|
|
v, ok := ctx.Value(Transcoding).(model.Transcoding)
|
|
return v, ok
|
|
}
|
|
|
|
func ClientUniqueIdFrom(ctx context.Context) (string, bool) {
|
|
v, ok := ctx.Value(ClientUniqueId).(string)
|
|
return v, ok
|
|
}
|
|
|
|
func ReverseProxyIpFrom(ctx context.Context) (string, bool) {
|
|
v, ok := ctx.Value(ReverseProxyIp).(string)
|
|
return v, ok
|
|
}
|
|
|
|
func InternalAuthFrom(ctx context.Context) (string, bool) {
|
|
if v := ctx.Value(InternalAuth); v != nil {
|
|
if username, ok := v.(string); ok {
|
|
return username, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func AddValues(ctx, requestCtx context.Context) context.Context {
|
|
for _, key := range allKeys {
|
|
if v := requestCtx.Value(key); v != nil {
|
|
ctx = context.WithValue(ctx, key, v)
|
|
}
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
type tokenEpochHolder struct {
|
|
value atomic.Int64
|
|
}
|
|
|
|
// WithTokenEpochHolder installs a slot a handler can use to report a bumped token epoch
|
|
// back to middleware that has already returned from the handler's perspective.
|
|
func WithTokenEpochHolder(ctx context.Context) context.Context {
|
|
h := &tokenEpochHolder{}
|
|
h.value.Store(-1)
|
|
return context.WithValue(ctx, TokenEpochHolder, h)
|
|
}
|
|
|
|
func SetTokenEpoch(ctx context.Context, epoch int) {
|
|
if h, ok := ctx.Value(TokenEpochHolder).(*tokenEpochHolder); ok {
|
|
h.value.Store(int64(epoch))
|
|
}
|
|
}
|
|
|
|
func TokenEpochFrom(ctx context.Context) (int, bool) {
|
|
h, ok := ctx.Value(TokenEpochHolder).(*tokenEpochHolder)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
if v := h.value.Load(); v >= 0 {
|
|
return int(v), true
|
|
}
|
|
return 0, false
|
|
}
|