mirror of
https://github.com/navidrome/navidrome.git
synced 2026-04-28 03:19:38 +00:00
* fix(sharing): validate JWT expiration and share existence on stream endpoint
The public stream endpoint (/public/s/{token}) was using
TokenAuth.Decode() which only verifies the JWT signature but skips
exp claim validation. This allowed expired share stream URLs to remain
functional indefinitely. Additionally, deleting a share did not revoke
previously issued stream tokens since the handler never performed a
server-side share lookup.
Fixed by switching decodeStreamInfo() to use auth.Validate() which
properly checks the exp claim, and by embedding the share ID ("sid")
in stream tokens so the handler can verify the share still exists.
Old tokens without the sid claim remain backward compatible but still
benefit from expiration validation.
* fix(sharing): check share expiration on stream requests
Replace the lightweight Exists() check with Get() + expiration
validation, so that shares whose ExpiresAt was updated to an earlier
time after token issuance are also rejected (410 Gone). Reuses the
existing checkShareError handler for consistent error responses.
104 lines
2.2 KiB
Go
104 lines
2.2 KiB
Go
package auth
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/lestrrat-go/jwx/v3/jwt"
|
|
)
|
|
|
|
// Claims represents the typed JWT claims used throughout Navidrome,
|
|
// replacing the untyped map[string]any approach.
|
|
type Claims struct {
|
|
// Standard JWT claims
|
|
Issuer string
|
|
Subject string // username for session tokens
|
|
IssuedAt time.Time
|
|
ExpiresAt time.Time
|
|
|
|
// Custom claims
|
|
UserID string // "uid"
|
|
IsAdmin bool // "adm"
|
|
ID string // "id" - artwork/mediafile ID
|
|
Format string // "f" - audio format
|
|
BitRate int // "b" - audio bitrate
|
|
ShareID string // "sid" - share ID for share stream tokens
|
|
}
|
|
|
|
// ToMap converts Claims to a map[string]any for use with TokenAuth.Encode().
|
|
// Only non-zero fields are included.
|
|
func (c Claims) ToMap() map[string]any {
|
|
m := make(map[string]any)
|
|
if c.Issuer != "" {
|
|
m[jwt.IssuerKey] = c.Issuer
|
|
}
|
|
if c.Subject != "" {
|
|
m[jwt.SubjectKey] = c.Subject
|
|
}
|
|
if !c.IssuedAt.IsZero() {
|
|
m[jwt.IssuedAtKey] = c.IssuedAt.UTC().Unix()
|
|
}
|
|
if !c.ExpiresAt.IsZero() {
|
|
m[jwt.ExpirationKey] = c.ExpiresAt.UTC().Unix()
|
|
}
|
|
if c.UserID != "" {
|
|
m["uid"] = c.UserID
|
|
}
|
|
if c.IsAdmin {
|
|
m["adm"] = c.IsAdmin
|
|
}
|
|
if c.ID != "" {
|
|
m["id"] = c.ID
|
|
}
|
|
if c.Format != "" {
|
|
m["f"] = c.Format
|
|
}
|
|
if c.BitRate != 0 {
|
|
m["b"] = c.BitRate
|
|
}
|
|
if c.ShareID != "" {
|
|
m["sid"] = c.ShareID
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (c Claims) WithExpiresAt(t time.Time) Claims {
|
|
c.ExpiresAt = t
|
|
return c
|
|
}
|
|
|
|
// ClaimsFromToken extracts Claims directly from a jwt.Token using token.Get().
|
|
func ClaimsFromToken(token jwt.Token) Claims {
|
|
var c Claims
|
|
c.Issuer, _ = token.Issuer()
|
|
c.Subject, _ = token.Subject()
|
|
c.IssuedAt, _ = token.IssuedAt()
|
|
c.ExpiresAt, _ = token.Expiration()
|
|
|
|
var uid string
|
|
if err := token.Get("uid", &uid); err == nil {
|
|
c.UserID = uid
|
|
}
|
|
var adm bool
|
|
if err := token.Get("adm", &adm); err == nil {
|
|
c.IsAdmin = adm
|
|
}
|
|
var id string
|
|
if err := token.Get("id", &id); err == nil {
|
|
c.ID = id
|
|
}
|
|
var f string
|
|
if err := token.Get("f", &f); err == nil {
|
|
c.Format = f
|
|
}
|
|
if err := token.Get("b", &c.BitRate); err != nil {
|
|
var bf float64
|
|
if err := token.Get("b", &bf); err == nil {
|
|
c.BitRate = int(bf)
|
|
}
|
|
}
|
|
var sid string
|
|
if err := token.Get("sid", &sid); err == nil {
|
|
c.ShareID = sid
|
|
}
|
|
return c
|
|
}
|