role-impersonation.ts166 lines · main
1import { getImpersonationSQL, type SafeSqlFragment } from '@supabase/pg-meta'
2
3import { uuidv4 } from './helpers'
4import type { User } from '@/data/auth/users-infinite-query'
5import { RoleImpersonationState as ValtioRoleImpersonationState } from '@/state/role-impersonation-state'
6
7type PostgrestImpersonationRole =
8 | {
9 type: 'postgrest'
10 role: 'anon'
11 }
12 | {
13 type: 'postgrest'
14 role: 'service_role'
15 }
16 | {
17 type: 'postgrest'
18 role: 'authenticated'
19 userType: 'native'
20 user?: User
21 aal?: 'aal1' | 'aal2'
22 }
23 | {
24 type: 'postgrest'
25 role: 'authenticated'
26 userType: 'external'
27 externalAuth?: {
28 sub: string
29 additionalClaims?: Record<string, any>
30 }
31 aal?: 'aal1' | 'aal2'
32 }
33
34export type PostgrestRole = PostgrestImpersonationRole['role']
35
36type CustomImpersonationRole = {
37 type: 'custom'
38 role: string
39}
40
41export type ImpersonationRole = PostgrestImpersonationRole | CustomImpersonationRole
42
43export function getExp1HourFromNow() {
44 return Math.floor((Date.now() + 60 * 60 * 1000) / 1000)
45}
46
47export function getPostgrestClaims(projectRef: string, role: PostgrestImpersonationRole) {
48 const exp = getExp1HourFromNow()
49 const nowTimestamp = Math.floor(Date.now() / 1000)
50
51 if (role.role === 'authenticated') {
52 // Supabase native auth case
53 if (role.userType === 'native' && role.user) {
54 const user = role.user
55 return {
56 aal: role.aal ?? 'aal1',
57 amr: [{ method: 'password', timestamp: nowTimestamp }],
58 app_metadata: user.raw_app_meta_data,
59 aud: 'authenticated',
60 email: user.email,
61 exp,
62 iat: nowTimestamp,
63 iss: `https://${projectRef}.supabase.co/auth/v1`,
64 phone: user.phone,
65 role: user.role ?? role.role,
66 session_id: uuidv4(),
67 sub: user.id,
68 user_metadata: user.raw_user_meta_data,
69 is_anonymous: user.is_anonymous,
70 }
71 }
72
73 // External auth case
74 if (role.userType === 'external' && role.externalAuth) {
75 return {
76 aal: role.aal ?? 'aal1',
77 aud: 'authenticated',
78 exp,
79 iat: nowTimestamp,
80 role: 'authenticated',
81 session_id: uuidv4(),
82 sub: role.externalAuth.sub,
83 ...role.externalAuth.additionalClaims,
84 }
85 }
86 }
87
88 return {
89 iss: 'supabase',
90 ref: projectRef,
91 role: role.role,
92 iat: nowTimestamp,
93 exp,
94 }
95}
96
97export type RoleImpersonationState = Pick<ValtioRoleImpersonationState, 'role' | 'claims'>
98
99export function wrapWithRoleImpersonation(
100 sql: SafeSqlFragment,
101 state?: RoleImpersonationState
102): SafeSqlFragment {
103 const { role, claims } = state ?? { role: undefined, claims: undefined }
104
105 if (role === undefined) return sql
106
107 const unexpiredClaims =
108 claims !== undefined ? { ...claims, exp: getExp1HourFromNow() } : undefined
109 const impersonationSql = getImpersonationSQL({ role: role, unexpiredClaims, sql })
110 return impersonationSql
111}
112
113function encodeText(data: string) {
114 return new TextEncoder().encode(data)
115}
116
117function encodeBase64Url(data: ArrayBuffer | Uint8Array | string): string {
118 return btoa(
119 String.fromCharCode(...new Uint8Array(typeof data === 'string' ? encodeText(data) : data))
120 )
121 .replace(/\+/g, '-')
122 .replace(/\//g, '_')
123 .replace(/=+$/, '')
124}
125
126function genKey(rawKey: string) {
127 return window.crypto.subtle.importKey(
128 'raw',
129 encodeText(rawKey),
130 { name: 'HMAC', hash: 'SHA-256' },
131 false,
132 ['sign', 'verify']
133 )
134}
135
136async function createToken(jwtPayload: object, key: string) {
137 const headerAndPayload =
138 encodeBase64Url(encodeText(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))) +
139 '.' +
140 encodeBase64Url(encodeText(JSON.stringify(jwtPayload)))
141
142 const signature = encodeBase64Url(
143 new Uint8Array(
144 await window.crypto.subtle.sign(
145 { name: 'HMAC' },
146 await genKey(key),
147 encodeText(headerAndPayload)
148 )
149 )
150 )
151
152 return `${headerAndPayload}.${signature}`
153}
154
155export function getRoleImpersonationJWT(
156 projectRef: string,
157 jwtSecret: string,
158 role: PostgrestImpersonationRole
159): Promise<string> {
160 const claims = {
161 ...getPostgrestClaims(projectRef, role),
162 exp: getExp1HourFromNow(),
163 }
164
165 return createToken(claims, jwtSecret)
166}