Authentication in Jazz

Jazz authentication is based on cryptographic keys ("Account keys"). Their public part represents a user's identity, their secret part lets you act as that user.

When a user loads a Jazz application for the first time, we create a new Account by generating keys and storing them locally.

Without any additional steps the user can use Jazz normally, but they would be limited to use on only one device.

To make Accounts work across devices, you can store/retrieve the account keys from an authentication method by using the corresponding hooks and providers.

Authentication with passkeys

Passkey authentication is fully local-first and the most secure of the auth methods that Jazz provides (because keys are managed by the device/operating system itself).

It is based on the Web Authentication API and is both very easy to use (using familiar FaceID/TouchID flows) and widely supported.

Using passkeys in Jazz is as easy as this:

export function AuthModal({ open, onOpenChange }: AuthModalProps) { const [username, setUsername] = useState(""); const auth = usePasskeyAuth({ // Must be inside the JazzProvider! appName: "My super-cool web app", }); if (auth.state === "signedIn") { // You can also use `useIsAuthenticated()` return <div>You are already signed in</div>; } const handleSignUp = async () => { await auth.signUp(username); onOpenChange(false); }; const handleLogIn = async () => { await auth.logIn(); onOpenChange(false); }; return ( <div> <button onClick={handleLogIn}>Log in</button> <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} /> <button onClick={handleSignUp}>Sign up</button> </div> ); }

You can try our passkey authentication using our passkey example or the music player demo.

Passphrase-based authentication

Passphrase authentication lets users log into any device using a Bitcoin-style passphrase. This means users are themselves responsible for storing the passphrase safely.

The passphrase is generated from the local account certificate using a wordlist of your choice.

You can find a set of ready-to-use wordlists in the bip39 repository.

For example:

import { englishWordlist } from "./wordlist" export function AuthModal({ open, onOpenChange }: AuthModalProps) { const [loginPassphrase, setLoginPassphrase] = useState(""); const auth = usePassphraseAuth({ // Must be inside the JazzProvider! wordlist: englishWordlist, }); if (auth.state === "signedIn") { // You can also use `useIsAuthenticated()` return <div>You are already signed in</div>; } const handleSignUp = async () => { await auth.signUp(); onOpenChange(false); }; const handleLogIn = async () => { await auth.logIn(loginPassphrase); onOpenChange(false); }; return ( <div> <label> Your current passphrase <textarea readOnly value={auth.passphrase} rows={5} /> </label> <button onClick={handleSignUp}>I have stored my passphrase</button> <label> Log in with your passphrase <textarea value={loginPassphrase} onChange={(e) => setLoginPassphrase(e.target.value)} placeholder="Enter your passphrase" rows={5} required /> </label> <button onClick={handleLogIn}>Log in</button> </div> ); }

You can try our passphrase authentication using our passphrase example or the todo list demo.

Integration with Clerk

Jazz can be used with Clerk to authenticate users.

This authentication method is not fully local-first, because the login and signup need to be done while online. Clerk and anyone who is an admin in the app's Clerk org are trusted with the user's key secret and could impersonate them.

However, once authenticated, your users won't need to interact with Clerk anymore, and are able to use all of Jazz's features without needing to be online.

The clerk provider is not built into jazz-react and needs the jazz-react-auth-clerk package to be installed.

After installing the package you can use the JazzProviderWithClerk component to wrap your app:

import { JazzProviderWithClerk } from "jazz-react-auth-clerk"; function JazzProvider({ children }: { children: React.ReactNode }) { const clerk = useClerk(); return ( <JazzProviderWithClerk clerk={clerk} sync={{ peer: `wss://cloud.jazz.tools/?key=${apiKey}`, }} > {children} </JazzProviderWithClerk> ); } createRoot(document.getElementById("root")!).render( <ClerkProvider publishableKey={PUBLISHABLE_KEY} afterSignOutUrl="/"> <JazzProvider> <App /> </JazzProvider> </ClerkProvider> );

Then you can use the Clerk auth methods to log in and sign up:

import { SignInButton } from "@clerk/clerk-react"; import { useAccount, useIsAuthenticated } from "jazz-react"; export function AuthButton() { const { logOut } = useAccount(); const isAuthenticated = useIsAuthenticated(); if (isAuthenticated) { return <button onClick={() => logOut()}>Logout</button>; } return <SignInButton />; }

Migrating data from anonymous to authenticated account

You may want allow your users to use your app without authenticating (a poll response for example). When signing up their anonymous account is transparently upgraded using the provided auth method, keeping the data stored in the account intact.

However, a user may realise that they already have an existing account after using the app anonymously and having already stored data in the anonymous account.

When they now log in, by default the anonymous account will be discarded and this could lead to unexpected data loss.

To avoid this situation we provide the onAnonymousAccountDiscarded handler to migrate the data from the anonymous account to the existing authenticated one.

This is an example from our music player:

export async function onAnonymousAccountDiscarded( anonymousAccount: MusicaAccount, ) { const { root: anonymousAccountRoot } = await anonymousAccount.ensureLoaded({ root: { rootPlaylist: { tracks: [{}], }, }, }); const me = await MusicaAccount.getMe().ensureLoaded({ root: { rootPlaylist: { tracks: [], }, }, }); for (const track of anonymousAccountRoot.rootPlaylist.tracks) { if (track.isExampleTrack) continue; const trackGroup = track._owner.castAs(Group); trackGroup.addMember(me, "admin"); me.root.rootPlaylist.tracks.push(track); } }

To see how this works in reality we suggest you to try to upload a song in the music player demo and then try to log in with an existing account.

Disable network sync for anonymous users

You can disable network sync to make your app local-only under specific circumstances.

For example, you may want to give the opportunity to non-authenticated users to try your app locally-only (incurring no sync traffic), then enable the network sync only when the user is authenticated:

<JazzProvider sync={{ peer: `wss://cloud.jazz.tools/?key=${apiKey}`, // This makes the app work in local mode when the user is anonymous when: "signedUp", }} > <App /> </JazzProvider>

For more complex behaviours, you can manually control sync by statefully switching when between "always" and "never".