role-impersonation.ts 4.2 KB

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