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:
Field explanations:
issuer
: Your API domain (must match theiss
claim in your JWTs)jwks_uri
: Endpoint URL where Alchemy can retrieve the JWT public key for verificationid_token_signing_alg_values_supported
: Must include “RS256” algorithm
3. JWT Structure Requirements
Your JWTs must contain these claims:
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.

Implementation Guide
React
APIs
Using React Hooks
Step 1: Install Dependencies
Step 2: Generate Target Public Key
Before creating your JWT, generate the required targetPublicKey
:
import { class AlchemySignerWebClientA lower level client used by the AlchemySigner used to communicate with Alchemy's signer service.
AlchemySignerWebClient } from "@account-kit/signer";
const const client: AlchemySignerWebClientclient = new new AlchemySignerWebClient(params: AlchemySignerClientParams): AlchemySignerWebClientInitializes 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: stringapiKey: "your-api-key",
},
iframeConfig: {
iframeContainerId: string;
iframeElementId?: string | undefined;
}iframeConfig: {
iframeContainerId: stringiframeContainerId: "signer-iframe-container",
},
});
// Generate the target public key
const const targetPublicKey: stringtargetPublicKey = await const client: AlchemySignerWebClientclient.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): UseAuthenticateResultHook 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: booleanisPending } = function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResultHook 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: stringjwt: string) => {
try {
await const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => voidauthenticate({
type: "custom-jwt"type: "custom-jwt",
jwt: stringjwt // Your generated JWT with required claims
});
} catch (function (local var) error: unknownerror) {
var console: ConsoleThe 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: unknownerror);
}
};
Step 4: Check Authentication Status
Monitor authentication status and handle the connected user:
import { const useSignerStatus: (override?: AlchemyAccountContextProps) => UseSignerStatusResultHook to get the signer status, optionally using an override configuration, useful if you’re building your own login.
useSignerStatus, const useUser: () => UseUserResultA 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.ElementMyComponent = () => {
const { const isConnected: booleanisConnected } = function useSignerStatus(override?: AlchemyAccountContextProps): UseSignerStatusResultHook to get the signer status, optionally using an override configuration, useful if you’re building your own login.
useSignerStatus();
const const user: UseUserResultuser = function useUser(): UseUserResultA 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: booleanisConnected && const user: UseUserResultuser) {
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 | undefinedemail}!</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:
- User Authentication: User logs into your existing auth system
- JWT Generation: Your backend generates a JWT with required claims
- Submit to Alchemy: Frontend submits JWT to Alchemy’s auth endpoint
- Verification: Alchemy verifies JWT using your JWKS endpoint
- 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 issuersub
contains unique user identifieraud
contains your Alchemy-provided audience IDnonce
ortknonce
containstoHex(sha256(targetPublicKey))
without0x
- JWT is signed with RS256 algorithm
Integration Testing
- Verify your
.well-known/openid-configuration
endpoint is accessible - Test JWKS endpoint returns valid public keys
- Validate JWT signature verification works
- 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.
Sponsor Gas for a User Operation
Start sponsoring gas fees for transactions via the Gas Manager.