Skip to content

Commit

Permalink
fix(client): Restore support for both sub and user_id claims in a…
Browse files Browse the repository at this point in the history
…uth JWTs (#986)
  • Loading branch information
samwillis committed Feb 22, 2024
1 parent 848301f commit dace3fc
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/thin-trainers-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"electric-sql": patch
---

Restore support for both `sub` and `user_id` claim in auth JWTs
9 changes: 7 additions & 2 deletions clients/typescript/src/auth/secure/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,13 @@ export function mockSecureAuthToken(

export function decodeToken(token: string): JWTPayload & { sub: string } {
const decoded = decodeJwt(token)
if (typeof decoded.sub === 'undefined') {
throw new InvalidArgumentError('Token does not contain a sub claim')
if (
typeof decoded.sub === 'undefined' &&
typeof decoded.user_id === 'undefined'
) {
throw new InvalidArgumentError(
'Token does not contain a sub or user_id claim'
)
}
return decoded as JWTPayload & { sub: string }
}
11 changes: 7 additions & 4 deletions clients/typescript/src/satellite/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,19 +699,22 @@ export class SatelliteProcess implements Satellite {
* @param token The JWT token.
*/
setToken(token: string): void {
const { sub } = decodeToken(token)
const { sub, user_id } = decodeToken(token)
// `sub` is the standard claim, but `user_id` is also used in the Electric service
// We first check for sub, and if it's not present, we use user_id
const newUserId = sub ?? user_id
const userId: string | undefined = this._authState?.userId
if (typeof userId !== 'undefined' && sub !== userId) {
if (typeof userId !== 'undefined' && newUserId !== userId) {
// We must check that the new token is still using the same user ID.
// We can't accept a re-connection that changes the user ID because the Satellite process is statefull.
// To change user ID the user must re-electrify the database.
throw new InvalidArgumentError(
`Can't change user ID when reconnecting. Previously connected with user ID '${userId}' but trying to reconnect with user ID '${sub}'`
`Can't change user ID when reconnecting. Previously connected with user ID '${userId}' but trying to reconnect with user ID '${newUserId}'`
)
}
this._setAuthState({
...this._authState!,
userId: sub,
userId: newUserId,
token,
})
}
Expand Down
37 changes: 37 additions & 0 deletions clients/typescript/test/satellite/process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,43 @@ test('set persistent client id', async (t) => {
t.assert(clientId1 === clientId2)
})

test('can use user_id in JWT', async (t) => {
const { satellite, authState } = t.context

await t.notThrowsAsync(async () => {
await startSatellite(
satellite,
authState,
insecureAuthToken({ user_id: 'test-userA' })
)
})
})

test('can use sub in JWT', async (t) => {
const { satellite, authState } = t.context

await t.notThrowsAsync(async () => {
await startSatellite(
satellite,
authState,
insecureAuthToken({ sub: 'test-userB' })
)
})
})

test('require user_id or sub in JWT', async (t) => {
const { satellite, authState } = t.context

const error = await t.throwsAsync(async () => {
await startSatellite(
satellite,
authState,
insecureAuthToken({ custom_user_claim: 'test-userC' })
)
})
t.is(error?.message, 'Token does not contain a sub or user_id claim')
})

test('cannot update user id', async (t) => {
const { satellite, authState, token } = t.context

Expand Down

0 comments on commit dace3fc

Please sign in to comment.