Skip to content

Commit 756d173

Browse files
authored
Merge pull request #303 from supabase/fix/gotrue_auth_events
fix: improve auth for realtime row level security
2 parents 594ed87 + 38c9bf3 commit 756d173

File tree

11 files changed

+8487
-2493
lines changed

11 files changed

+8487
-2493
lines changed

.github/workflows/ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,12 @@ jobs:
2424
run: |
2525
npm ci
2626
npm t
27+
28+
- name: Test & publish code coverage
29+
uses: paambaati/[email protected]
30+
env:
31+
CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}}
32+
with:
33+
coverageCommand: yarn run coverage
34+
coverageLocations: |
35+
${{github.workspace}}/test/coverage/lcov.info:lcov

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ You can now use plain `<script>`s to import supabase-js from CDNs, like:
2828
```html
2929
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js"></script>
3030
```
31+
3132
or even:
3233

3334
```html
@@ -41,7 +42,7 @@ Then you can use it from a global `supabase` variable:
4142
const { createClient } = supabase
4243
const _supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key')
4344
44-
console.log('Supabase Instance: ', _supabase);
45+
console.log('Supabase Instance: ', _supabase)
4546
// ...
4647
</script>
4748
```

example/next-todo/package-lock.json

Lines changed: 6666 additions & 755 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/next-todo/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
},
99
"dependencies": {
1010
"@supabase/supabase-js": "file:../..",
11-
"next": "latest",
12-
"react": "^16.13.1",
13-
"react-dom": "^16.13.1"
11+
"next": "^11.1.1",
12+
"react": "^17.0.1",
13+
"react-dom": "^17.0.1"
1414
},
1515
"devDependencies": {
1616
"postcss-flexbugs-fixes": "4.2.1",

jest.config.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
module.exports = {
22
preset: 'ts-jest',
33
testEnvironment: 'node',
4+
collectCoverage: false,
5+
coverageDirectory: './test/coverage',
6+
coverageReporters: ['json', 'html', 'lcov'],
7+
collectCoverageFrom: [
8+
'./src/**/*.{js,ts}',
9+
'./src/**/*.unit.test.ts',
10+
'!**/node_modules/**',
11+
'!**/vendor/**',
12+
'!**/vendor/**',
13+
],
414
}

package-lock.json

Lines changed: 1708 additions & 1729 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"repository": "supabase/supabase-js",
2323
"scripts": {
2424
"clean": "rimraf dist docs",
25+
"coverage": "jest --runInBand --coverage",
2526
"format": "prettier --write \"{src,test}/**/*.ts\"",
2627
"build": "genversion src/lib/version.ts --es6 && run-s clean format build:*",
2728
"build:main": "tsc -p tsconfig.json",
@@ -36,9 +37,9 @@
3637
"docs:json": "typedoc --json docs/spec.json --mode modules --includeDeclarations --excludeExternals"
3738
},
3839
"dependencies": {
39-
"@supabase/gotrue-js": "^1.21.0",
40+
"@supabase/gotrue-js": "^1.21.7",
4041
"@supabase/postgrest-js": "^0.35.0",
41-
"@supabase/realtime-js": "1.2.1",
42+
"@supabase/realtime-js": "^1.3.2",
4243
"@supabase/storage-js": "^1.5.0"
4344
},
4445
"devDependencies": {

src/SupabaseClient.ts

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
import { DEFAULT_HEADERS } from './lib/constants'
2-
import { stripTrailingSlash } from './lib/helpers'
1+
import { DEFAULT_HEADERS, STORAGE_KEY } from './lib/constants'
2+
import { stripTrailingSlash, isBrowser } from './lib/helpers'
33
import { Fetch, SupabaseClientOptions } from './lib/types'
44
import { SupabaseAuthClient } from './lib/SupabaseAuthClient'
55
import { SupabaseQueryBuilder } from './lib/SupabaseQueryBuilder'
66
import { SupabaseStorageClient } from '@supabase/storage-js'
77
import { PostgrestClient } from '@supabase/postgrest-js'
8+
import { AuthChangeEvent, Session, Subscription } from '@supabase/gotrue-js'
89
import { RealtimeClient, RealtimeSubscription, RealtimeClientOptions } from '@supabase/realtime-js'
910

1011
const DEFAULT_OPTIONS = {
1112
schema: 'public',
1213
autoRefreshToken: true,
1314
persistSession: true,
1415
detectSessionInUrl: true,
16+
multiTab: true,
1517
headers: DEFAULT_HEADERS,
1618
}
1719

@@ -32,7 +34,9 @@ export default class SupabaseClient {
3234
protected authUrl: string
3335
protected storageUrl: string
3436
protected realtime: RealtimeClient
37+
protected multiTab: boolean
3538
protected fetch?: Fetch
39+
protected changedAccessToken: string | undefined
3640
protected headers: {
3741
[key: string]: string
3842
}
@@ -47,6 +51,7 @@ export default class SupabaseClient {
4751
* @param options.detectSessionInUrl Set to "true" if you want to automatically detects OAuth grants in the URL and signs in the user.
4852
* @param options.headers Any additional headers to send with each network request.
4953
* @param options.realtime Options passed along to realtime-js constructor.
54+
* @param options.multiTab Set to "false" if you want to disable multi-tab/window events.
5055
* @param options.fetch A custom fetch implementation.
5156
*/
5257
constructor(
@@ -65,12 +70,16 @@ export default class SupabaseClient {
6570
this.authUrl = `${supabaseUrl}/auth/v1`
6671
this.storageUrl = `${supabaseUrl}/storage/v1`
6772
this.schema = settings.schema
73+
this.multiTab = settings.multiTab
6874
this.fetch = settings.fetch
6975
this.headers = { ...DEFAULT_HEADERS, ...options?.headers }
7076

7177
this.auth = this._initSupabaseAuthClient(settings)
7278
this.realtime = this._initRealtimeClient({ headers: this.headers, ...settings.realtime })
7379

80+
this._listenForAuthEvents()
81+
this._listenForMultiTabEvents()
82+
7483
// In the future we might allow the user to pass in a logger to receive these events.
7584
// this.realtime.onOpen(() => console.log('OPEN'))
7685
// this.realtime.onClose(() => console.log('CLOSED'))
@@ -121,6 +130,15 @@ export default class SupabaseClient {
121130
return rest.rpc<T>(fn, params, { head, count })
122131
}
123132

133+
/**
134+
* Remove all active subscriptions.
135+
*/
136+
removeAllSubscriptions() {
137+
this.realtime.channels.forEach((sub) => {
138+
this.removeSubscription(sub)
139+
})
140+
}
141+
124142
/**
125143
* Removes an active subscription and returns the number of open connections.
126144
*
@@ -213,4 +231,61 @@ export default class SupabaseClient {
213231
.receive('error', (e: Error) => reject(e))
214232
})
215233
}
234+
235+
private _listenForMultiTabEvents() {
236+
if (!this.multiTab || !isBrowser() || !window?.addEventListener) {
237+
return null
238+
}
239+
240+
try {
241+
return window?.addEventListener('storage', (e: StorageEvent) => {
242+
if (e.key === STORAGE_KEY) {
243+
const newSession = JSON.parse(String(e.newValue))
244+
const accessToken: string | undefined =
245+
newSession?.currentSession?.access_token ?? undefined
246+
const previousAccessToken = this.auth.session()?.access_token
247+
if (!accessToken) {
248+
this._handleTokenChanged('SIGNED_OUT', accessToken, 'STORAGE')
249+
} else if (!previousAccessToken && accessToken) {
250+
this._handleTokenChanged('SIGNED_IN', accessToken, 'STORAGE')
251+
} else if (previousAccessToken !== accessToken) {
252+
this._handleTokenChanged('TOKEN_REFRESHED', accessToken, 'STORAGE')
253+
}
254+
}
255+
})
256+
} catch (error) {
257+
console.error('_listenForMultiTabEvents', error)
258+
return null
259+
}
260+
}
261+
262+
private _listenForAuthEvents() {
263+
let { data } = this.auth.onAuthStateChange((event, session) => {
264+
this._handleTokenChanged(event, session?.access_token, 'CLIENT')
265+
})
266+
return data
267+
}
268+
269+
private _handleTokenChanged(
270+
event: AuthChangeEvent,
271+
token: string | undefined,
272+
source: 'CLIENT' | 'STORAGE'
273+
) {
274+
if (
275+
(event === 'TOKEN_REFRESHED' || event === 'SIGNED_IN') &&
276+
this.changedAccessToken !== token
277+
) {
278+
// Token has changed
279+
this.realtime.setAuth(token!)
280+
// Ideally we should call this.auth.recoverSession() - need to make public
281+
// to trigger a "SIGNED_IN" event on this client.
282+
if (source == 'STORAGE') this.auth.setAuth(token!)
283+
284+
this.changedAccessToken = token
285+
} else if (event === 'SIGNED_OUT' || event === 'USER_DELETED') {
286+
// Token is removed
287+
this.removeAllSubscriptions()
288+
if (source == 'STORAGE') this.auth.signOut()
289+
}
290+
}
216291
}

src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
// constants.ts
22
import { version } from './version'
33
export const DEFAULT_HEADERS = { 'X-Client-Info': `supabase-js/${version}` }
4+
export const STORAGE_KEY = 'supabase.auth.token'

src/lib/helpers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ export function uuid() {
99
}
1010

1111
export function stripTrailingSlash(url: string) {
12-
return url.replace(/\/$/, "");
12+
return url.replace(/\/$/, '')
1313
}
14+
15+
export const isBrowser = () => typeof window !== 'undefined'

0 commit comments

Comments
 (0)