Bring Your Own Authentication

Integrate your existing authentication provider and add smart wallet functionality to your app without changing your users’ login experience. We support JWT-based and OIDC-compliant authentication providers.

Prerequisites

Before implementing JWT authentication, verify that your authentication system meets these requirements:

1. OIDC Compliance

Your authentication system must be OpenID Connect (OIDC) compliant and capable of issuing JWT tokens.

2. Required Endpoints

OpenID Connect Discovery Endpoint

Host a discovery endpoint at: <YOUR_API_DOMAIN>/.well-known/openid-configuration

The response must include these minimum fields:

1{
2 "issuer": "<YOUR_API_DOMAIN>",
3 "jwks_uri": "<YOUR_JWKS_ENDPOINT_URL>",
4 "id_token_signing_alg_values_supported": ["RS256"]
5}

Field explanations:

  • issuer: Your API domain (must match the iss claim in your JWTs)
  • jwks_uri: Endpoint URL where Alchemy can retrieve the JWT public key for verification
  • id_token_signing_alg_values_supported: Must include “RS256” algorithm

3. JWT Structure Requirements

Your JWTs must contain these claims:

ClaimDescriptionExample
issIssuer - your auth provider’s API domain"https://auth.example.com"
subSubject - unique user identifier"user123" or "550e8400-e29b-41d4-a716-446655440000"
audAudience - client ID issued by Alchemy"your-alchemy-audience-id"
nonceTarget public key hash (see below)"a1b2c3d4..."

Important: The nonce claim must be toHex(sha256(targetPublicKey)) without the leading 0x.

You can generate the targetPublicKey from the Alchemy Signer SDK

If your OAuth provider reserves the nonce claim, you can use tknonce as an alternative. Only one of nonce or tknonce needs to be set.

The combination of (iss, sub, aud) acts as a unique user identifier. Ensure these values are consistent for each user.

4. Audience Claim

The aud claim must be set to your Alchemy-provided audience ID. You can obtain the audience claim from your Smart Wallet configuration in the Alchemy Dashboard.

Audience Claim

Implementation Guide

Using React Hooks

Step 1: Install Dependencies

$npm install @account-kit/signer @account-kit/react

Step 2: Generate Target Public Key

Before creating your JWT, generate the required targetPublicKey:

import { 
class AlchemySignerWebClient

A lower level client used by the AlchemySigner used to communicate with Alchemy's signer service.

AlchemySignerWebClient
} from "@account-kit/signer";
const
const client: AlchemySignerWebClient
client
= new
new AlchemySignerWebClient(params: AlchemySignerClientParams): AlchemySignerWebClient

Initializes a new instance with the given parameters, setting up the connection, iframe configuration, and WebAuthn stamper.

AlchemySignerWebClient
({
connection: { apiKey: string; rpcUrl?: undefined; jwt?: undefined; } | { jwt: string; rpcUrl?: undefined; apiKey?: undefined; } | { rpcUrl: string; apiKey?: undefined; jwt?: undefined; } | { rpcUrl: string; jwt: string; apiKey?: undefined; }
connection
: {
apiKey: string
apiKey
: "your-api-key",
},
iframeConfig: { iframeContainerId: string; iframeElementId?: string | undefined; }
iframeConfig
: {
iframeContainerId: string
iframeContainerId
: "signer-iframe-container",
}, }); // Generate the target public key const
const targetPublicKey: string
targetPublicKey
= await
const client: AlchemySignerWebClient
client
.
AlchemySignerWebClient.targetPublicKey: () => Promise<string>

Initializes the iframe stamper and returns its public key.

targetPublicKey
();
// Use this in your JWT's nonce claim // nonce = toHex(sha256(targetPublicKey)) without the '0x'

Step 3: Create and Submit JWT

After generating your JWT with the proper claims, authenticate the user:

import { 
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
} from "@account-kit/react";
// Inside your component const {
const authenticate: UseMutateFunction<User, Error, AuthParams, unknown>
authenticate
,
const isPending: boolean
isPending
} =
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
();
const
const handleJwtAuth: (jwt: string) => Promise<void>
handleJwtAuth
= async (
jwt: string
jwt
: string) => {
try { await
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => void
authenticate
({
type: "custom-jwt"
type
: "custom-jwt",
jwt: string
jwt
// Your generated JWT with required claims
}); } catch (
function (local var) error: unknown
error
) {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream. * A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:


const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```

Example using the `Console` class:

```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);

myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console
.
Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stderr with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

js const code = 5; console.error('error #%d', code); // Prints: error #5, to stderr console.error('error', code); // Prints: error 5, to stderr

If formatting elements (e.g. %d) are not found in the first string then util.inspect() is called on each argument and the resulting string values are concatenated. See util.format() for more information.

error
("Authentication failed:",
function (local var) error: unknown
error
);
} };

Step 4: Check Authentication Status

Monitor authentication status and handle the connected user:

import { 
const useSignerStatus: (override?: AlchemyAccountContextProps) => UseSignerStatusResult

Hook to get the signer status, optionally using an override configuration, useful if you’re building your own login.

useSignerStatus
,
const useUser: () => UseUserResult

A React hook that returns the current user information, either from an External Owned Account (EOA) or from the client store. It uses the Alchemy account context and synchronizes with external store updates. The best way to check if user is logged in for both smart account contract users and EOA.

If using smart contract account, returns address of the signer. If only using smart account contracts then you can use useSignerStatus or useAccount to see if the account is defined.

useUser
} from "@account-kit/react";
const
const MyComponent: () => JSX.Element
MyComponent
= () => {
const {
const isConnected: boolean
isConnected
} =
function useSignerStatus(override?: AlchemyAccountContextProps): UseSignerStatusResult

Hook to get the signer status, optionally using an override configuration, useful if you’re building your own login.

useSignerStatus
();
const
const user: UseUserResult
user
=
function useUser(): UseUserResult

A React hook that returns the current user information, either from an External Owned Account (EOA) or from the client store. It uses the Alchemy account context and synchronizes with external store updates. The best way to check if user is logged in for both smart account contract users and EOA.

If using smart contract account, returns address of the signer. If only using smart account contracts then you can use useSignerStatus or useAccount to see if the account is defined.

useUser
();
if (
const isConnected: boolean
isConnected
&&
const user: UseUserResult
user
) {
return ( <
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
<
React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p
>Welcome, {
const user: User & { type: "eoa" | "sca"; }
user
.
email?: string | undefined
email
}!</
React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p
>
<
React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p
>Wallet Address: {
const user: User & { type: "eoa" | "sca"; }
user
.
address: `0x${string}`
address
}</
React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p
>
</
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
); } return <
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>Not authenticated</
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>;
};

Architecture Overview

The JWT authentication flow works as follows:

  1. User Authentication: User logs into your existing auth system
  2. JWT Generation: Your backend generates a JWT with required claims
  3. Submit to Alchemy: Frontend submits JWT to Alchemy’s auth endpoint
  4. Verification: Alchemy verifies JWT using your JWKS endpoint
  5. Wallet Access: User gains access to their smart wallet

Testing and Validation

JWT Validation Tool

Use jwt.io to decode and validate your JWT structure before integration.

Required Claims Checklist

  • iss matches your OpenID config issuer
  • sub contains unique user identifier
  • aud contains your Alchemy-provided audience ID
  • nonce or tknonce contains toHex(sha256(targetPublicKey)) without 0x
  • JWT is signed with RS256 algorithm

Integration Testing

  1. Verify your .well-known/openid-configuration endpoint is accessible
  2. Test JWKS endpoint returns valid public keys
  3. Validate JWT signature verification works
  4. Test complete authentication flow with sample users

Next Steps

Sending a User Operation

Once your users have been authenticated, you can start sending user operations.

Start sponsoring gas fees for transactions via the Gas Manager.