Spotify: manueller PKCE-Login ohne SSH-Port-Forward (Redirect-URI ist hart auf localhost validiert)
This commit is contained in:
@@ -230,6 +230,25 @@ aufmachen:
|
|||||||
ssh -N -L 43827:127.0.0.1:43827 <user>@<hermes-maschine>
|
ssh -N -L 43827:127.0.0.1:43827 <user>@<hermes-maschine>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Kein SSH-Port-Forward moeglich?** Hermes validiert `redirect_uri` hart auf
|
||||||
|
`127.0.0.1`/`localhost` (kein oeffentlicher Callback moeglich), aber der
|
||||||
|
lokale Listener laesst sich umgehen: `scripts/spotify_manual_auth.py` baut
|
||||||
|
dieselbe PKCE-Autorisierungs-URL, wartet aber nicht auf den Callback,
|
||||||
|
sondern fragt Dich nach dem `code`-Parameter, den Du von Hand aus der
|
||||||
|
(fehlschlagenden) Redirect-URL in der Adressleiste kopierst. Das Ergebnis
|
||||||
|
landet 1:1 im selben Format in `~/.hermes/auth.json` wie beim normalen
|
||||||
|
Login.
|
||||||
|
```bash
|
||||||
|
docker cp scripts/spotify_manual_auth.py hermes-agent:/tmp/spotify_manual_auth.py
|
||||||
|
docker exec -it hermes-agent python3 /tmp/spotify_manual_auth.py
|
||||||
|
```
|
||||||
|
Details/Ablauf stehen im Docstring des Skripts. Du kannst dabei dieselbe
|
||||||
|
Spotify-Dev-App wie fuer ARIA wiederverwenden — dafuer im
|
||||||
|
[Spotify-Dashboard](https://developer.spotify.com/dashboard) bei der
|
||||||
|
bestehenden App unter *Settings → Redirect URIs* zusaetzlich
|
||||||
|
`http://127.0.0.1:43827/spotify/callback` eintragen (kein neues
|
||||||
|
Client-Secret noetig, PKCE braucht nur die Client-ID).
|
||||||
|
|
||||||
Hinweis: Free-Spotify-Account reicht fuer Suche/Playlists/Library, fuer
|
Hinweis: Free-Spotify-Account reicht fuer Suche/Playlists/Library, fuer
|
||||||
echte Playback-Steuerung (play/pause/skip auf einem Geraet) braucht's
|
echte Playback-Steuerung (play/pause/skip auf einem Geraet) braucht's
|
||||||
Spotify Premium.
|
Spotify Premium.
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Manueller Spotify-PKCE-Login fuer Hermes -- OHNE lokalen Callback-Server
|
||||||
|
und OHNE SSH-Port-Forward.
|
||||||
|
|
||||||
|
Hintergrund: `hermes auth spotify` startet einen HTTP-Listener auf
|
||||||
|
127.0.0.1:43827 IM Container und wartet auf den OAuth-Redirect. Der
|
||||||
|
redirect_uri-Wert ist von Hermes hart auf 127.0.0.1/localhost validiert
|
||||||
|
(hermes_cli/auth.py:_spotify_validate_redirect_uri) -- eine oeffentliche
|
||||||
|
Callback-URL ist nicht moeglich. Ohne `ssh -N -L 43827:127.0.0.1:43827`
|
||||||
|
kann der Browser auf dem Laptop/Handy diesen Listener nie erreichen.
|
||||||
|
|
||||||
|
Dieses Skript umgeht genau das: es baut dieselbe Autorisierungs-URL wie
|
||||||
|
`hermes auth spotify` (identischer PKCE-Code, identischer Redirect-URI),
|
||||||
|
aber statt auf den Callback zu warten, fragt es Dich nach dem `code`, den
|
||||||
|
Du von Hand aus der Adressleiste kopierst (der Redirect schlaegt fehl, weil
|
||||||
|
nichts auf 127.0.0.1:43827 Deines eigenen Geraets lauscht -- die Adresse
|
||||||
|
mit dem code-Parameter bleibt aber sichtbar). Das Ergebnis wird 1:1 in
|
||||||
|
~/.hermes/auth.json geschrieben, exakt im selben Format wie beim normalen
|
||||||
|
Login -- Hermes' Auto-Refresh (resolve_spotify_runtime_credentials)
|
||||||
|
funktioniert danach unveraendert.
|
||||||
|
|
||||||
|
Nutzung (im hermes-agent-Container, wo hermes_cli installiert ist):
|
||||||
|
docker cp scripts/spotify_manual_auth.py hermes-agent:/tmp/spotify_manual_auth.py
|
||||||
|
docker exec -it hermes-agent python3 /tmp/spotify_manual_auth.py
|
||||||
|
|
||||||
|
Du kannst dieselbe Spotify-Dev-App wie fuer ARIA wiederverwenden -- dafuer
|
||||||
|
nur im Spotify-Dashboard (developer.spotify.com/dashboard -> eure App ->
|
||||||
|
Settings -> Redirect URIs) zusaetzlich
|
||||||
|
http://127.0.0.1:43827/spotify/callback
|
||||||
|
eintragen. Ein Client-Secret wird hier NICHT gebraucht (PKCE, public
|
||||||
|
client) -- nur die Client-ID.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
try:
|
||||||
|
from hermes_cli.auth import (
|
||||||
|
_auth_store_lock,
|
||||||
|
_load_auth_store,
|
||||||
|
_save_auth_store,
|
||||||
|
_spotify_accounts_base_url,
|
||||||
|
_spotify_api_base_url,
|
||||||
|
_spotify_build_authorize_url,
|
||||||
|
_spotify_client_id,
|
||||||
|
_spotify_code_challenge,
|
||||||
|
_spotify_code_verifier,
|
||||||
|
_spotify_exchange_code_for_tokens,
|
||||||
|
_spotify_redirect_uri,
|
||||||
|
_spotify_scope_string,
|
||||||
|
_spotify_token_payload_to_state,
|
||||||
|
_store_provider_state,
|
||||||
|
get_provider_auth_state,
|
||||||
|
AuthError,
|
||||||
|
)
|
||||||
|
except ImportError as exc: # pragma: no cover
|
||||||
|
sys.exit(
|
||||||
|
"Konnte hermes_cli.auth nicht importieren -- Skript muss INNERHALB "
|
||||||
|
f"des hermes-agent-Containers laufen (python3 dort). Fehler: {exc}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
existing_state = get_provider_auth_state("spotify") or {}
|
||||||
|
|
||||||
|
default_client_id = ""
|
||||||
|
try:
|
||||||
|
default_client_id = _spotify_client_id(state=existing_state)
|
||||||
|
except AuthError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
prompt = "Spotify Client-ID"
|
||||||
|
prompt += f" [{default_client_id}]: " if default_client_id else ": "
|
||||||
|
client_id = input(prompt).strip() or default_client_id
|
||||||
|
if not client_id:
|
||||||
|
sys.exit("Keine Client-ID angegeben -- Abbruch.")
|
||||||
|
|
||||||
|
redirect_uri = _spotify_redirect_uri(state=existing_state)
|
||||||
|
scope = _spotify_scope_string(existing_state.get("scope"))
|
||||||
|
accounts_base_url = _spotify_accounts_base_url(existing_state)
|
||||||
|
api_base_url = _spotify_api_base_url(existing_state)
|
||||||
|
|
||||||
|
code_verifier = _spotify_code_verifier()
|
||||||
|
code_challenge = _spotify_code_challenge(code_verifier)
|
||||||
|
state_nonce = uuid.uuid4().hex
|
||||||
|
|
||||||
|
authorize_url = _spotify_build_authorize_url(
|
||||||
|
client_id=client_id,
|
||||||
|
redirect_uri=redirect_uri,
|
||||||
|
scope=scope,
|
||||||
|
state=state_nonce,
|
||||||
|
code_challenge=code_challenge,
|
||||||
|
accounts_base_url=accounts_base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("1) Diese URL in IRGENDEINEM Browser oeffnen (Handy reicht) und bei")
|
||||||
|
print(" Spotify einloggen + 'Zugriff erlauben' bestaetigen:")
|
||||||
|
print()
|
||||||
|
print(authorize_url)
|
||||||
|
print()
|
||||||
|
print(f" (Diese Redirect-URI muss im Spotify-Dashboard eingetragen sein: {redirect_uri})")
|
||||||
|
print()
|
||||||
|
print("2) Der Browser versucht danach zu http://127.0.0.1:43827/... zu")
|
||||||
|
print(" springen und wird das NICHT laden koennen -- das ist erwartet,")
|
||||||
|
print(" da nichts auf DEINEM Geraet dort lauscht. Die Adresse bleibt in")
|
||||||
|
print(" der Adressleiste sichtbar (oder in der Fehlermeldung).")
|
||||||
|
print("3) Den Wert hinter 'code=' kopieren (bis exklusive '&state=').")
|
||||||
|
print("=" * 78)
|
||||||
|
print()
|
||||||
|
|
||||||
|
pasted_code = input("code=... hier einfuegen: ").strip()
|
||||||
|
if not pasted_code:
|
||||||
|
sys.exit("Kein Code eingegeben -- Abbruch.")
|
||||||
|
pasted_state = input(
|
||||||
|
f"state=... hier einfuegen (erwartet '{state_nonce}', Enter zum Ueberspringen): "
|
||||||
|
).strip()
|
||||||
|
if pasted_state and pasted_state != state_nonce:
|
||||||
|
sys.exit(
|
||||||
|
"State-Wert stimmt nicht mit dem erwarteten ueberein -- Abbruch "
|
||||||
|
"(evtl. eine alte URL/Fenster benutzt, bitte Skript neu starten)."
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nTausche Code gegen Access-/Refresh-Token...")
|
||||||
|
token_payload = _spotify_exchange_code_for_tokens(
|
||||||
|
client_id=client_id,
|
||||||
|
code=pasted_code,
|
||||||
|
redirect_uri=redirect_uri,
|
||||||
|
code_verifier=code_verifier,
|
||||||
|
accounts_base_url=accounts_base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
spotify_state = _spotify_token_payload_to_state(
|
||||||
|
token_payload,
|
||||||
|
client_id=client_id,
|
||||||
|
redirect_uri=redirect_uri,
|
||||||
|
requested_scope=scope,
|
||||||
|
accounts_base_url=accounts_base_url,
|
||||||
|
api_base_url=api_base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
with _auth_store_lock():
|
||||||
|
auth_store = _load_auth_store()
|
||||||
|
_store_provider_state(auth_store, "spotify", spotify_state, set_active=False)
|
||||||
|
saved_to = _save_auth_store(auth_store)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"Spotify-Login gespeichert unter: {saved_to}")
|
||||||
|
print("providers.spotify ist jetzt gesetzt.")
|
||||||
|
print("Nicht vergessen: `docker exec -it hermes-agent hermes tools` -> Spotify aktivieren, falls noch nicht geschehen.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user