/
/
/
1"""
2JWT token helper for Music Assistant authentication.
3
4Future OIDC Support:
5- Consuming external OIDC providers (Google, Keycloak, etc.): Can be added without
6 changes to token structure. MA would validate external OIDC tokens and issue its
7 own JWT tokens (similar to current Home Assistant OAuth flow).
8
9- Acting as OIDC provider for third parties: Would require implementing OAuth2
10 refresh token flow with a dedicated /auth/token endpoint for token refresh.
11 Short-lived access tokens (15 min) + long-lived refresh tokens would be needed
12 for proper OIDC compliance.
13"""
14
15from __future__ import annotations
16
17import secrets
18from datetime import datetime
19from typing import TYPE_CHECKING, Any
20
21import jwt
22
23from music_assistant.helpers.datetime import utc
24
25if TYPE_CHECKING:
26 from music_assistant_models.auth import User
27
28
29class JWTHelper:
30 """Helper class for JWT token operations."""
31
32 def __init__(self, secret_key: str) -> None:
33 """
34 Initialize JWT helper.
35
36 :param secret_key: Secret key for signing JWTs.
37 """
38 self.secret_key = secret_key
39 self.algorithm = "HS256"
40
41 def encode_token(
42 self,
43 user: User,
44 token_id: str,
45 token_name: str,
46 expires_at: datetime,
47 is_long_lived: bool = False,
48 ) -> str:
49 """
50 Encode a JWT token for a user.
51
52 :param user: User object to create token for.
53 :param token_id: Unique token identifier.
54 :param token_name: Human-readable token name.
55 :param expires_at: Token expiration datetime.
56 :param is_long_lived: Whether this is a long-lived token.
57 :return: Encoded JWT token string.
58 """
59 now = utc()
60 payload = {
61 "sub": user.user_id,
62 "jti": token_id,
63 "iat": int(now.timestamp()),
64 "exp": int(expires_at.timestamp()),
65 "username": user.username,
66 "role": user.role,
67 "token_name": token_name,
68 "is_long_lived": is_long_lived,
69 }
70
71 return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
72
73 def decode_token(self, token: str, verify_exp: bool = True) -> dict[str, Any]:
74 """
75 Decode and verify a JWT token.
76
77 :param token: JWT token string to decode.
78 :param verify_exp: Whether to verify token expiration.
79 :return: Decoded token payload.
80 :raises jwt.InvalidTokenError: If token is invalid or expired.
81 """
82 payload: dict[str, Any] = jwt.decode(
83 token,
84 self.secret_key,
85 algorithms=[self.algorithm],
86 options={"verify_exp": verify_exp},
87 )
88 return payload
89
90 @staticmethod
91 def generate_secret_key() -> str:
92 """
93 Generate a secure random secret key for JWT signing.
94
95 :return: Base64-encoded 256-bit random key.
96 """
97 return secrets.token_urlsafe(32) # 32 bytes = 256 bits
98
99 def get_token_id(self, token: str) -> str | None:
100 """
101 Extract token ID (jti) from JWT without full validation.
102
103 :param token: JWT token string.
104 :return: Token ID or None if invalid.
105 """
106 try:
107 payload: dict[str, Any] = jwt.decode(
108 token,
109 options={"verify_signature": False, "verify_exp": False},
110 )
111 jti = payload.get("jti")
112 return str(jti) if jti else None
113 except Exception:
114 return None
115