mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
docs: purge safeGetSession + getUser from auth example code (#47042)
Sweeps the example code that creating-a-client.mdx and other auth docs pull via $CodeSample, so the rendered pages match the "use getClaims()" guidance. Also adds Database type stubs and parameterizes SupabaseClient<Database> across SvelteKit and Hono examples. Fixes #40985 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **Documentation** * Updated OAuth server getting-started guide to use a claims-based consent/auth gate and preserve the authorization identifier on redirect. * Added the `auth_methods` partial across framework sections in the server-side “creating a client” guide. * **Refactor** * Updated authentication examples for Hono, Next.js, and SvelteKit to rely on JWT claims for logged-in checks and protected routes. * Streamlined example auth state and UI rendering to use claims-derived information. * **Type Updates** * Improved TypeScript typing for Supabase clients and app auth data across examples, including generated database type stubs. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Chris Chinchilla <chris@chrischinchilla.com>
This commit is contained in:
1 parent
2bae11b549
commit
6eb40f17a7
23 files changed
+97
-122
No files matched your search
@@ -0,0 +1,4 @@
|
||||
// Replace this file by running:
|
||||
// npx supabase gen types typescript --local > src/database.types.ts
|
||||
// or pass --project-id <your-project-ref> for a remote project.
|
||||
export type Database = Record<string, never>
|
||||
@@ -6,11 +6,11 @@ app.use('*', supabaseMiddleware())
|
||||
|
||||
const routes = app.get('/api/user', async (c) => {
|
||||
const supabase = getSupabase(c)
|
||||
const { data, error } = await supabase.auth.getUser()
|
||||
const { data, error } = await supabase.auth.getClaims()
|
||||
|
||||
if (error) console.log('error', error)
|
||||
|
||||
if (!data?.user) {
|
||||
if (!data?.claims) {
|
||||
return c.json({
|
||||
message: 'You are not logged in.',
|
||||
})
|
||||
@@ -18,7 +18,7 @@ const routes = app.get('/api/user', async (c) => {
|
||||
|
||||
return c.json({
|
||||
message: 'You are logged in!',
|
||||
userId: data.user,
|
||||
userId: data.claims.sub,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@ import type { Context, MiddlewareHandler } from 'hono'
|
||||
import { env } from 'hono/adapter'
|
||||
import { setCookie } from 'hono/cookie'
|
||||
|
||||
import type { Database } from '../database.types'
|
||||
|
||||
declare module 'hono' {
|
||||
interface ContextVariableMap {
|
||||
supabase: SupabaseClient
|
||||
supabase: SupabaseClient<Database>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Replace this file by running:
|
||||
// npx supabase gen types typescript --local > src/database.types.ts
|
||||
// or pass --project-id <your-project-ref> for a remote project.
|
||||
export type Database = Record<string, never>
|
||||
@@ -4,9 +4,11 @@ import type { Context, MiddlewareHandler } from 'hono'
|
||||
import { env } from 'hono/adapter'
|
||||
import { setCookie } from 'hono/cookie'
|
||||
|
||||
import type { Database } from '../database.types'
|
||||
|
||||
declare module 'hono' {
|
||||
interface ContextVariableMap {
|
||||
supabase: SupabaseClient
|
||||
supabase: SupabaseClient<Database>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,10 @@ import { redirect } from 'next/navigation'
|
||||
export default async function ProtectedPage() {
|
||||
const supabase = await createClient()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
const { data } = await supabase.auth.getClaims()
|
||||
const claims = data?.claims
|
||||
|
||||
if (!user) {
|
||||
if (!claims) {
|
||||
return redirect('/login')
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,8 @@ import { redirect } from 'next/navigation'
|
||||
export default async function AuthButton() {
|
||||
const supabase = await createClient()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
const { data } = await supabase.auth.getClaims()
|
||||
const claims = data?.claims
|
||||
|
||||
const signOut = async () => {
|
||||
'use server'
|
||||
@@ -17,9 +16,9 @@ export default async function AuthButton() {
|
||||
return redirect('/login')
|
||||
}
|
||||
|
||||
return user ? (
|
||||
return claims ? (
|
||||
<div className="flex items-center gap-4">
|
||||
Hey, {user.email}!
|
||||
Hey, {claims.email}!
|
||||
<form action={signOut}>
|
||||
<button className="py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">
|
||||
Logout
|
||||
|
||||
+4
-6
@@ -1,17 +1,15 @@
|
||||
import type { Session, SupabaseClient, User } from '@supabase/supabase-js'
|
||||
import type { Database } from './database.types.ts' // import generated types
|
||||
import type { JwtPayload, SupabaseClient } from '@supabase/supabase-js'
|
||||
import type { Database } from './database.types'
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
supabase: SupabaseClient<Database>
|
||||
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>
|
||||
session: Session | null
|
||||
user: User | null
|
||||
claims: JwtPayload | null
|
||||
}
|
||||
interface PageData {
|
||||
session: Session | null
|
||||
claims: JwtPayload | null
|
||||
}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Replace this file by running:
|
||||
// npx supabase gen types typescript --local > src/database.types.ts
|
||||
// or pass --project-id <your-project-ref> for a remote project.
|
||||
export type Database = Record<string, never>
|
||||
@@ -29,31 +29,6 @@ const supabase: Handle = async ({ event, resolve }) => {
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Unlike `supabase.auth.getSession()`, which returns the session _without_
|
||||
* validating the JWT, this function also calls `getUser()` to validate the
|
||||
* JWT before returning the session.
|
||||
*/
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
if (!session) {
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser()
|
||||
if (error) {
|
||||
// JWT validation has failed
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
filterSerializedResponseHeaders(name) {
|
||||
/**
|
||||
@@ -66,15 +41,20 @@ const supabase: Handle = async ({ event, resolve }) => {
|
||||
}
|
||||
|
||||
const authGuard: Handle = async ({ event, resolve }) => {
|
||||
const { session, user } = await event.locals.safeGetSession()
|
||||
event.locals.session = session
|
||||
event.locals.user = user
|
||||
/**
|
||||
* `getClaims` validates the JWT signature locally (against the project's
|
||||
* asymmetric signing keys) without an extra round-trip to the Auth server.
|
||||
* Use this for route protection. Use `getUser()` only when you need the
|
||||
* canonical server-validated user record (e.g., after password changes).
|
||||
*/
|
||||
const { data: claimsData } = await event.locals.supabase.auth.getClaims()
|
||||
event.locals.claims = claimsData?.claims ?? null
|
||||
|
||||
if (!event.locals.session && event.url.pathname.startsWith('/private')) {
|
||||
if (!event.locals.claims && event.url.pathname.startsWith('/private')) {
|
||||
redirect(303, '/auth')
|
||||
}
|
||||
|
||||
if (event.locals.session && event.url.pathname === '/auth') {
|
||||
if (event.locals.claims && event.url.pathname === '/auth') {
|
||||
redirect(303, '/private')
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
|
||||
const { session, user } = await safeGetSession()
|
||||
export const load: LayoutServerLoad = async ({ locals: { claims }, cookies }) => {
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
claims,
|
||||
cookies: cookies.getAll(),
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,11 @@
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
let { data, children } = $props()
|
||||
let { session, supabase } = $derived(data)
|
||||
let { claims, supabase } = $derived(data)
|
||||
|
||||
onMount(() => {
|
||||
const { data } = supabase.auth.onAuthStateChange((_, newSession) => {
|
||||
if (newSession?.expires_at !== session?.expires_at) {
|
||||
if (newSession?.expires_at !== claims?.exp) {
|
||||
invalidate('supabase:auth')
|
||||
}
|
||||
})
|
||||
@@ -16,4 +16,4 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
{@render children()}
|
||||
@@ -27,17 +27,14 @@ export const load: LayoutLoad = async ({ data, depends, fetch }) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
* safe, and on the server, it reads `session` from the `LayoutData`, which
|
||||
* safely checked the session using `safeGetSession`.
|
||||
* `getClaims` validates the JWT signature locally (for asymmetric keys) once
|
||||
* the relevant signing keys are available or cached, and returns the decoded
|
||||
* claims. While an initial or periodic network request may be required to
|
||||
* fetch or refresh keys, this is both faster and safer than `getSession`,
|
||||
* which does not validate the JWT.
|
||||
*/
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
const { data: claimsData, error } = await supabase.auth.getClaims()
|
||||
const claims = error ? null : claimsData?.claims
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
return { session, supabase, user }
|
||||
return { supabase, claims }
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
import type { PageData } from './$types'
|
||||
|
||||
let { data } = $props()
|
||||
let { notes, supabase, user } = $derived(data)
|
||||
let { notes, supabase, claims } = $derived(data)
|
||||
|
||||
const handleSubmit: EventHandler<SubmitEvent, HTMLFormElement> = async (evt) => {
|
||||
evt.preventDefault()
|
||||
@@ -24,7 +24,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<h1>Private page for user: {user?.email}</h1>
|
||||
<h1>Private page for user: {claims?.email}</h1>
|
||||
<h2>Notes</h2>
|
||||
<ul>
|
||||
{#each notes as note}
|
||||
@@ -36,4 +36,4 @@
|
||||
Add a note
|
||||
<input name="note" type="text" />
|
||||
</label>
|
||||
</form>
|
||||
</form>
|
||||
Vendored
+2
-8
@@ -1,17 +1,11 @@
|
||||
import type { Session, SupabaseClient, User } from '@supabase/supabase-js'
|
||||
import type { Database } from './database.types.ts' // import generated types
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import type { Database } from './database.types'
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
supabase: SupabaseClient<Database>
|
||||
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>
|
||||
session: Session | null
|
||||
user: User | null
|
||||
}
|
||||
interface PageData {
|
||||
session: Session | null
|
||||
}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Replace this file by running:
|
||||
// npx supabase gen types typescript --local > src/database.types.ts
|
||||
// or pass --project-id <your-project-ref> for a remote project.
|
||||
export type Database = Record<string, never>
|
||||
@@ -12,7 +12,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
/**
|
||||
* Note: You have to add the `path` variable to the
|
||||
* set and remove method due to sveltekit's cookie API
|
||||
* requiring this to be set, setting the path to an empty string
|
||||
* requiring this to be set, setting the path to `/`
|
||||
* will replicate previous/standard behavior (https://kit.svelte.dev/docs/types#public-types-cookies)
|
||||
*/
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
@@ -25,31 +25,6 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Unlike `supabase.auth.getSession()`, which returns the session _without_
|
||||
* validating the JWT, this function also calls `getUser()` to validate the
|
||||
* JWT before returning the session.
|
||||
*/
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
if (!session) {
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser()
|
||||
if (error) {
|
||||
// JWT validation has failed
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
filterSerializedResponseHeaders(name) {
|
||||
return name === 'content-range' || name === 'x-supabase-api-version'
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals: { safeGetSession }, cookies }) => {
|
||||
const { session, user } = await safeGetSession()
|
||||
|
||||
export const load: LayoutServerLoad = async ({ cookies }) => {
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
cookies: cookies.getAll(),
|
||||
}
|
||||
}
|
||||
@@ -23,13 +23,14 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
* safe, and on the server, it reads `session` from the `LayoutData`, which
|
||||
* safely checked the session using `safeGetSession`.
|
||||
* `getClaims` validates the JWT signature locally (for asymmetric keys) once
|
||||
* the relevant signing keys are available or cached, and returns the decoded
|
||||
* claims. While an initial or periodic network request may be required to
|
||||
* fetch or refresh keys, this is both faster and safer than `getSession`,
|
||||
* which does not validate the JWT.
|
||||
*/
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
const { data: claimsData, error } = await supabase.auth.getClaims()
|
||||
const claims = error ? null : claimsData?.claims
|
||||
|
||||
return { supabase, session }
|
||||
return { supabase, claims }
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
|
||||
import type { Database } from './database.types'
|
||||
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
supabase: SupabaseClient
|
||||
supabase: SupabaseClient<Database>
|
||||
}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Replace this file by running:
|
||||
// npx supabase gen types typescript --local > src/database.types.ts
|
||||
// or pass --project-id <your-project-ref> for a remote project.
|
||||
export type Database = Record<string, never>
|
||||
Reference in new issue
Block a user