Skip to content

Commit 8768e0e

Browse files
authored
fix(cli): decode Go keyring tokens (#5406)
## What changed Decode `go-keyring-base64:` wrapped access tokens when reading from the OS keyring in both the legacy and next CLI credential layers. ## Why The Go keyring backend can store the real `sbp_...` access token inside a `go-keyring-base64:` wrapper. The TypeScript credential readers were validating and using the raw wrapped value, so legacy Management API commands such as `supabase secrets set` failed with an invalid access token format error even though the decoded token was valid. ## Notes for reviewers The normalization happens at the keyring-read boundary so existing validation continues to run against the actual token value, not the storage representation.
1 parent 9c4359d commit 8768e0e

5 files changed

Lines changed: 38 additions & 3 deletions

File tree

apps/cli/src/legacy/auth/legacy-credentials.layer.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Effect, FileSystem, Layer, Option, Path, Redacted } from "effect";
22

33
import { RuntimeInfo } from "../../shared/runtime/runtime-info.service.ts";
4+
import { normalizeKeyringToken } from "../../shared/auth/keyring-token.ts";
45
import { LegacyCliConfig } from "../config/legacy-cli-config.service.ts";
56
import { LegacyCredentials } from "./legacy-credentials.service.ts";
67
import { LegacyInvalidAccessTokenError } from "./legacy-errors.ts";
@@ -33,7 +34,9 @@ const tryKeyringRead = (
3334
try: () => {
3435
const entry = new module.Entry(KEYRING_SERVICE, account);
3536
const value = entry.getPassword();
36-
return value && value.length > 0 ? Option.some(value) : Option.none<string>();
37+
return value && value.length > 0
38+
? Option.some(normalizeKeyringToken(value))
39+
: Option.none<string>();
3740
},
3841
catch: () => Option.none<string>(),
3942
}).pipe(Effect.orElseSucceed(() => Option.none<string>()));

apps/cli/src/legacy/auth/legacy-credentials.layer.unit.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ afterEach(() => {
8888

8989
const VALID_TOKEN = "sbp_" + "a".repeat(40);
9090
const VALID_OAUTH_TOKEN = "sbp_oauth_" + "b".repeat(40);
91+
const encodeGoKeyringBase64 = (token: string) =>
92+
`go-keyring-base64:${Buffer.from(token).toString("base64")}`;
9193

9294
const expectSomeToken = (token: Option.Option<Redacted.Redacted<string>>, expected: string) => {
9395
expect(Option.isSome(token)).toBe(true);
@@ -115,6 +117,15 @@ describe("legacyCredentialsLayer.getAccessToken", () => {
115117
}).pipe(Effect.provide(makeLayer()));
116118
});
117119

120+
it.effect("decodes Go keyring base64 values from the keyring profile account", () => {
121+
passwords.set("Supabase CLI/supabase", encodeGoKeyringBase64(VALID_TOKEN));
122+
return Effect.gen(function* () {
123+
const { getAccessToken } = yield* LegacyCredentials;
124+
const token = yield* getAccessToken;
125+
expectSomeToken(token, VALID_TOKEN);
126+
}).pipe(Effect.provide(makeLayer()));
127+
});
128+
118129
it.effect("falls through to the legacy access-token keyring entry", () => {
119130
passwords.set("Supabase CLI/access-token", VALID_OAUTH_TOKEN);
120131
return Effect.gen(function* () {

apps/cli/src/next/auth/credentials.layer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Effect, FileSystem, Layer, Option, Path, Redacted } from "effect";
22

3+
import { normalizeKeyringToken } from "../../shared/auth/keyring-token.ts";
34
import { CliConfig } from "../config/cli-config.service.ts";
45
import { Credentials } from "./credentials.service.ts";
56

@@ -32,15 +33,15 @@ const makeCredentials = Effect.gen(function* () {
3233
try {
3334
const entry = new keyringModule.value.Entry(SERVICE, ACCOUNT);
3435
const token = entry.getPassword();
35-
if (token) return Option.some(Redacted.make(token));
36+
if (token) return Option.some(Redacted.make(normalizeKeyringToken(token)));
3637
} catch {
3738
/* fall through */
3839
}
3940

4041
try {
4142
const entry = new keyringModule.value.Entry(SERVICE, LEGACY_ACCOUNT);
4243
const token = entry.getPassword();
43-
if (token) return Option.some(Redacted.make(token));
44+
if (token) return Option.some(Redacted.make(normalizeKeyringToken(token)));
4445
} catch {
4546
/* fall through */
4647
}

apps/cli/src/next/auth/credentials.layer.unit.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ let throwOnSetPassword = false;
2020
const throwOnGetPasswordAccounts = new Set<string>();
2121
const returnNullForAccounts = new Set<string>();
2222
const throwOnDeletePasswordAccounts = new Set<string>();
23+
const encodeGoKeyringBase64 = (token: string) =>
24+
`go-keyring-base64:${Buffer.from(token).toString("base64")}`;
2325

2426
vi.mock("@napi-rs/keyring", () => ({
2527
Entry: class Entry {
@@ -108,6 +110,15 @@ describe("Credentials", () => {
108110
}).pipe(Effect.provide(makeLayer(tempHome)));
109111
});
110112

113+
it.effect("decodes Go keyring base64 values from current account", () => {
114+
passwords.set("Supabase CLI/access-token", encodeGoKeyringBase64("current-token"));
115+
return Effect.gen(function* () {
116+
const { getAccessToken } = yield* Credentials;
117+
const token = yield* getAccessToken;
118+
expectSomeToken(token, "current-token");
119+
}).pipe(Effect.provide(makeLayer(tempHome)));
120+
});
121+
111122
it.effect("falls back to legacy account when current is missing", () => {
112123
passwords.set("Supabase CLI/supabase", "legacy-token");
113124
return Effect.gen(function* () {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const GO_KEYRING_BASE64_PREFIX = "go-keyring-base64:";
2+
3+
export function normalizeKeyringToken(value: string): string {
4+
if (!value.startsWith(GO_KEYRING_BASE64_PREFIX)) {
5+
return value;
6+
}
7+
8+
return Buffer.from(value.slice(GO_KEYRING_BASE64_PREFIX.length), "base64").toString("utf8");
9+
}

0 commit comments

Comments
 (0)