1
+ import { useProfile } from "components/db"
2
+ import { useProfileState } from "components/db/profile/redux"
1
3
import { User } from "firebase/auth"
2
4
import Router , { useRouter } from "next/router"
3
- import React , { useEffect } from "react"
5
+ import React , { useCallback , useEffect , useRef } from "react"
4
6
import { auth } from "../firebase"
5
7
import { useAppDispatch } from "../hooks"
6
8
import { createService } from "../service"
@@ -9,21 +11,64 @@ import { Claim } from "./types"
9
11
10
12
export const { Provider } = createService ( ( ) => {
11
13
const dispatch = useAppDispatch ( )
12
- useEffect (
13
- ( ) =>
14
- auth . onAuthStateChanged ( async user => {
15
- let claims : Claim | undefined = undefined
16
- if ( user ) {
17
- const token = await user . getIdTokenResult ( )
18
- const fromToken = Claim . validate ( token . claims )
19
- if ( fromToken . success ) claims = fromToken . value
20
- }
21
- dispatch ( authChanged ( { user, claims } ) )
22
- } ) ,
23
- [ dispatch ]
24
- )
14
+ const getToken = useGetTokenWithRefresh ( )
15
+
16
+ useEffect ( ( ) => {
17
+ const unsubscribe = auth . onAuthStateChanged ( async user => {
18
+ let claims : Claim | undefined = undefined
19
+ if ( user ) {
20
+ let token = await getToken ( user )
21
+ const fromToken = Claim . validate ( token . claims )
22
+ if ( fromToken . success ) claims = fromToken . value
23
+ }
24
+ dispatch ( authChanged ( { user, claims } ) )
25
+ } )
26
+ return ( ) => {
27
+ unsubscribe ( )
28
+ }
29
+ } , [ dispatch , getToken ] )
25
30
} )
26
31
32
+ /**
33
+ * The token does not refresh when the user's profile or claims in firebase auth change,
34
+ * So we need to manually refresh it if the user's profile or claims change.
35
+ * It's always possible (but a bug) that the profile is out of sync with the auth database,
36
+ * so we need to avoid refresh loops by only refreshing once per service instance.
37
+ */
38
+ const useGetTokenWithRefresh = ( ) => {
39
+ const hasRefreshedToken = useRef ( false )
40
+ const profile = useProfileState ( ) ,
41
+ hasProfile = ! profile . loading ,
42
+ roleInProfile = profile . profile ?. role
43
+
44
+ return useCallback (
45
+ async ( user : User ) => {
46
+ let token = await user . getIdTokenResult ( )
47
+
48
+ const isRoleOutOfSyncInToken =
49
+ hasProfile && roleInProfile !== token . claims . role ,
50
+ isEmailVerifiedOutOfSyncInToken =
51
+ user . emailVerified !== Boolean ( token . claims . email_verified ) ,
52
+ isTokenOutOfSync =
53
+ isRoleOutOfSyncInToken || isEmailVerifiedOutOfSyncInToken
54
+
55
+ // If the profile has a role but the token doesn't, try refreshing the token.
56
+ // If the user indicates email is verified but the token doesn't, try refreshing the token.
57
+ // Only refresh once per service instance.
58
+ if ( isTokenOutOfSync && ! hasRefreshedToken . current ) {
59
+ console . log (
60
+ "Refreshing token because it is out of sync with profile or user"
61
+ )
62
+ token = await user . getIdTokenResult ( true )
63
+ hasRefreshedToken . current = true
64
+ }
65
+
66
+ return token
67
+ } ,
68
+ [ hasProfile , roleInProfile ]
69
+ )
70
+ }
71
+
27
72
/**
28
73
* Renders the given component if authenticated, otherwise redirects.
29
74
*/
0 commit comments