Skip to content

Commit 35c267f

Browse files
Supabase static keys - simplified config (#140)
* Add simplified Supabase static key config. * Use a separate config field for Supabase JWT keys. * Add changeset. --------- Co-authored-by: stevensJourney <[email protected]>
1 parent 2c18ad2 commit 35c267f

File tree

6 files changed

+61
-1
lines changed

6 files changed

+61
-1
lines changed

.changeset/green-forks-approve.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@powersync/service-core': minor
3+
'@powersync/service-types': minor
4+
'@powersync/service-image': minor
5+
---
6+
7+
Add "supabase_jwt_secret" config option to simplify static Supabase auth.

modules/module-postgres/src/auth/SupabaseKeyCollector.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import * as pgwire_utils from '../utils/pgwire_utils.js';
1010
*
1111
* Unfortunately, despite the JWTs containing a kid, we have no way to lookup that kid
1212
* before receiving a valid token.
13+
*
14+
* @deprecated Supabase is removing support for "app.settings.jwt_secret".
1315
*/
1416
export class SupabaseKeyCollector implements auth.KeyCollector {
1517
private pool: pgwire.PgClient;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as jose from 'jose';
2+
import { KeySpec, KeyOptions } from './KeySpec.js';
3+
import { KeyCollector, KeyResult } from './KeyCollector.js';
4+
5+
const SUPABASE_KEY_OPTIONS: KeyOptions = {
6+
requiresAudience: ['authenticated'],
7+
maxLifetimeSeconds: 86400 * 7 + 1200 // 1 week + 20 minutes margin
8+
};
9+
10+
/**
11+
* Set of static keys for Supabase.
12+
*
13+
* Same as StaticKeyCollector, but with some configuration tweaks for Supabase.
14+
*
15+
* Similar to SupabaseKeyCollector, but using hardcoded keys instead of fetching
16+
* from the database.
17+
*
18+
* A key can be added both with and without a kid, in case wildcard matching is desired.
19+
*/
20+
export class StaticSupabaseKeyCollector implements KeyCollector {
21+
static async importKeys(keys: jose.JWK[]) {
22+
const parsedKeys = await Promise.all(keys.map((key) => KeySpec.importKey(key, SUPABASE_KEY_OPTIONS)));
23+
return new StaticSupabaseKeyCollector(parsedKeys);
24+
}
25+
26+
constructor(private keys: KeySpec[]) {}
27+
28+
async getKeys(): Promise<KeyResult> {
29+
return { keys: this.keys, errors: [] };
30+
}
31+
}

packages/service-core/src/auth/auth-index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './KeyStore.js';
77
export * from './LeakyBucket.js';
88
export * from './RemoteJWKSCollector.js';
99
export * from './StaticKeyCollector.js';
10+
export * from './StaticSupabaseKeyCollector.js';

packages/service-core/src/util/config/compound-config-collector.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,27 @@ export class CompoundConfigCollector {
6565

6666
const inputKeys = baseConfig.client_auth?.jwks?.keys ?? [];
6767
const staticCollector = await auth.StaticKeyCollector.importKeys(inputKeys);
68-
6968
collectors.add(staticCollector);
7069

70+
if (baseConfig.client_auth?.supabase && baseConfig.client_auth?.supabase_jwt_secret != null) {
71+
// This replaces the old SupabaseKeyCollector, with a statically-configured key.
72+
// You can get the same effect with manual HS256 key configuration, but this
73+
// makes the config simpler.
74+
// We also a custom audience ("authenticated"), increased max lifetime (1 week),
75+
// and auto base64-url-encode the key.
76+
collectors.add(
77+
await auth.StaticSupabaseKeyCollector.importKeys([
78+
{
79+
kty: 'oct',
80+
alg: 'HS256',
81+
// In this case, the key is not base64-encoded yet.
82+
k: Buffer.from(baseConfig.client_auth.supabase_jwt_secret, 'utf8').toString('base64url'),
83+
kid: undefined // Wildcard kid - any kid can match
84+
}
85+
])
86+
);
87+
}
88+
7189
let jwks_uris = baseConfig.client_auth?.jwks_uri ?? [];
7290
if (typeof jwks_uris == 'string') {
7391
jwks_uris = [jwks_uris];

packages/types/src/config/PowerSyncConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export const powerSyncConfig = t.object({
115115
block_local_jwks: t.boolean.optional(),
116116
jwks: strictJwks.optional(),
117117
supabase: t.boolean.optional(),
118+
supabase_jwt_secret: t.string.optional(),
118119
audience: t.array(t.string).optional()
119120
})
120121
.optional(),

0 commit comments

Comments
 (0)