Skip to content

Authorization request to keymaster returns 403 #1099

@ltcmdrkeen

Description

@ltcmdrkeen

Describe the bug

403 Status code is returned from a Mercury request when requesting token, see

Screenshots/Stracktraces/Logs

Image

Version/Commit
1.6.6

Patch

As @photovoltex suggested in librespot issue 1532, I've implemented a change to use Login5:

  • the login requires a b64 credentials string which we can still issue using AuthStrategy "OAUTH"
  • librespot will then create the credentials.json which we use in the AuthStrategy "STORED" replacing mercury with Login5
  • I have added a new helper class that generates the token using Login5 and changed the TokenProvider to use that class ...

Although it is not a complete solution, it works for now.

This is the patch

Replaced_getting_token_via_mercury_with_Login5.patch

Subject: [PATCH] Replaced getting token via mercury with Login5
---
Index: lib/src/main/java/xyz/gianlu/librespot/core/Login5TokenHelper.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/lib/src/main/java/xyz/gianlu/librespot/core/Login5TokenHelper.java b/lib/src/main/java/xyz/gianlu/librespot/core/Login5TokenHelper.java
new file mode 100644
--- /dev/null	(revision bf18fb6689373c435bf94fa978770accf222cddd)
+++ b/lib/src/main/java/xyz/gianlu/librespot/core/Login5TokenHelper.java	(revision bf18fb6689373c435bf94fa978770accf222cddd)
@@ -0,0 +1,82 @@
+package xyz.gianlu.librespot.core;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.protobuf.ByteString;
+import com.spotify.login5v3.Credentials;
+import com.spotify.login5v3.Login5;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Base64;
+
+public final class Login5TokenHelper {
+    private Login5TokenHelper() {}
+
+    /** Parse credentials.json and build the login5 StoredCredential. */
+    private static Credentials.StoredCredential readStoredCredential(File storedCredentialsFile) throws IOException {
+        try (FileReader reader = new FileReader(storedCredentialsFile)) {
+            JsonObject obj = JsonParser.parseReader(reader).getAsJsonObject();
+            String username = obj.get("username").getAsString();
+            String base64Creds = obj.get("credentials").getAsString();
+            byte[] data = Base64.getDecoder().decode(base64Creds);
+
+            return Credentials.StoredCredential.newBuilder()
+                    .setUsername(username)
+                    .setData(ByteString.copyFrom(data))
+                    .build();
+        }
+    }
+
+    /**
+     * Fetch a StoredToken via login5 using an absolute path to the credentials.json file.
+     *
+     * Requires a public static factory in TokenProvider:
+     *
+     *   public static StoredToken fromJsonObject(com.google.gson.JsonObject obj) {
+     *       return new StoredToken(obj);
+     *   }
+     */
+    public static TokenProvider.StoredToken fetchStoredToken(Session session,
+                                                             Path storedCredentialsPath,
+                                                             String... scopes) throws IOException {
+        File storedFile = storedCredentialsPath.toFile();
+        if (!storedFile.isFile()) {
+            throw new IOException("Credentials file not found: " + storedFile.getAbsolutePath());
+        }
+
+        Credentials.StoredCredential sc = readStoredCredential(storedFile);
+
+        String accessToken;
+        int expiresIn;
+
+        try {
+            Login5Api api = new Login5Api(session);
+            Login5.LoginResponse resp = api.login5(
+                    Login5.LoginRequest.newBuilder()
+                            .setStoredCredential(sc)
+                            .build());
+
+            if (!resp.hasOk()) throw new IOException("login5 did not return OK: " + resp);
+            accessToken = resp.getOk().getAccessToken();
+            expiresIn = resp.getOk().getAccessTokenExpiresIn();
+        } catch (Exception e) {
+            throw new IOException("login5 request failed", e);
+        }
+
+        // Build JSON that StoredToken expects: scope must be an array
+        JsonArray scopeArr = new JsonArray();
+        for (String s : scopes) scopeArr.add(s);
+
+        JsonObject obj = new JsonObject();
+        obj.addProperty("accessToken", accessToken);
+        obj.addProperty("expiresIn", expiresIn);
+        obj.addProperty("tokenType", "Bearer");
+        obj.add("scope", scopeArr);
+
+        return TokenProvider.fromJsonObject(obj); // Requires factory method in TokenProvider
+    }
+}
Index: lib/src/main/java/xyz/gianlu/librespot/core/TokenProvider.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/lib/src/main/java/xyz/gianlu/librespot/core/TokenProvider.java b/lib/src/main/java/xyz/gianlu/librespot/core/TokenProvider.java
--- a/lib/src/main/java/xyz/gianlu/librespot/core/TokenProvider.java	(revision 1437b1581efa92c08f9215f0be930237f5e6cda6)
+++ b/lib/src/main/java/xyz/gianlu/librespot/core/TokenProvider.java	(revision bf18fb6689373c435bf94fa978770accf222cddd)
@@ -23,9 +23,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import xyz.gianlu.librespot.common.Utils;
-import xyz.gianlu.librespot.json.GenericJson;
 import xyz.gianlu.librespot.mercury.MercuryClient;
-import xyz.gianlu.librespot.mercury.MercuryRequests;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -55,6 +53,7 @@
         return null;
     }
 
+
     @NotNull
     public synchronized StoredToken getToken(@NotNull String... scopes) throws IOException, MercuryClient.MercuryException {
         if (scopes.length == 0) throw new IllegalArgumentException();
@@ -66,8 +65,17 @@
         }
 
         LOGGER.debug("Token expired or not suitable, requesting again. {scopes: {}, oldToken: {}}", Arrays.asList(scopes), token);
-        GenericJson resp = session.mercury().sendSync(MercuryRequests.requestToken(session.deviceId(), String.join(",", scopes)));
-        token = new StoredToken(resp.obj);
+
+
+        //GenericJson resp = session.mercury().sendSync(MercuryRequests.requestToken(session.deviceId(), String.join(",", scopes)));
+        //token = new StoredToken(obj);
+        Session.Configuration conf = session.configuration();
+
+        try {
+            token = Login5TokenHelper.fetchStoredToken(session, java.nio.file.Paths.get(conf.storedCredentialsFile.getAbsolutePath()), scopes);
+        } catch (IOException e) {
+            throw new IOException("Failed to get token via login5", e);
+        }
 
         LOGGER.debug("Updated token successfully! {scopes: {}, newToken: {}}", Arrays.asList(scopes), token);
         tokens.add(token);
@@ -80,6 +88,10 @@
         return getToken(scope).accessToken;
     }
 
+    public static StoredToken fromJsonObject(com.google.gson.JsonObject obj) {
+        return new StoredToken(obj);
+    }
+
     public static class StoredToken {
         public final int expiresIn;
         public final String accessToken;

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions