Permissions
Permissions add an app-controlled, scoped session key to an account. 1auth handles approval, passkey authorization, and installation; it never receives the session private key.
Start with one target contract, one selector, and a 24-hour validity window. The app can then execute that action without another passkey dialog. After granting, use Headless Mode to prepare, sign, and submit intents.
Explicit targetChains install contract-and-selector authority; no chain is
selected implicitly. For cross-chain routes, put the destination execution chain
in targetChains and every funding chain in sourceChains. An Arbitrum Sepolia
swap funded by mUSD on Base Sepolia needs swap permission on Arbitrum and
source-chain Permit2-claim coverage on Base. Omitting both sourceChains and
crossChainPermits grants no Permit2 or cross-chain claim authority.
Grant scoped access
Create or load a signer
Create or load the ECDSA signer your app will use for headless execution.
It may live in the browser, backend, KMS/HSM, or another system that can produce
SmartSession signatures. Send only its public sessionKeyAddress to 1auth.
Define permissions
Use definePermissions() so app code does not hardcode selectors or
calldata offsets. Start with one named function on one target contract.
When granting the permission, the application explicitly supplies
validAfter and a validUntil 24 hours later.
import { definePermissions } from "@rhinestone/1auth";
const mockUsdMintAbi = [
{
type: "function",
name: "mint",
stateMutability: "nonpayable",
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [],
},
] as const;
const validAfter = Math.floor(Date.now() / 1000);
const validUntil = validAfter + 24 * 60 * 60;
const permissions = definePermissions({
address: mUSD,
name: "mUSD",
abi: mockUsdMintAbi,
functions: {
mint: {},
},
});definePermissions() returns the permissions and contracts for
grantPermissions(). This scopes the action to the contract and
mint(address,uint256) selector, leaving to and amount unconstrained.
1auth does not choose an expiry: the application must pass validUntil.
Grant permissions
import { OneAuthClient } from "@rhinestone/1auth";
const oneAuth = new OneAuthClient({
providerUrl: "https://passkey.1auth.app",
clientId: "webshop",
sponsorship: {
accessTokenUrl: "/api/sponsorship/access-token",
extensionTokenUrl: "/api/sponsorship/extension-token",
},
});
const sessionKeyAddress = await appSigner.getAddress();
const result = await oneAuth.grantPermissions({
accountAddress,
targetChains: [8453, 84532],
sessionKeyAddress,
validAfter,
validUntil,
maxUses: 25,
...permissions,
});
if (!result.success) {
throw new Error(result.error.message);
}After this one-time grant, the app can submit calldata accepted by
mint(address,uint256) during the validity window without another signature
dialog. On-chain SmartSession enforcement checks the contract, selector, time
window, and maxUses approved by the user.
This starting pattern is selector-scoped. Add params for fixed recipients,
amount ceilings, or other calldata rules; use maxUses to cap executions. The
embedded GrantPermissionDemo also constrains recipient and mint amount.
Add a bridge claim permission
Selector permissions cover calls on the destination chain. If the
headless intent is funded by a source-chain asset through Permit2 and a
settlement layer such as Across, add a crossChainPermits entry as
well. This tells the SmartSession what source token may be claimed,
which destination token may be delivered, who may receive it, and which
settlement layer is allowed.
import {
createCrossChainPermission,
definePermissions,
} from "@rhinestone/1auth";
import { parseUnits } from "viem";
import { arbitrumSepolia, baseSepolia } from "viem/chains";
const ARB_SEPOLIA_CHAIN_ID = 421614;
const BASE_SEPOLIA_CHAIN_ID = 84532;
const usdcOnArb = "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d";
const maxBridgeSpend = parseUnits("0.1", 6);
const now = Math.floor(Date.now() / 1000);
const validUntil = now + 24 * 60 * 60;
const swap = definePermissions({
address: rwaSwap,
name: "RWASwap",
abi: rwaSwapAbi,
functions: {
swap: {
params: {
usdcIn: { condition: "equal", value: maxBridgeSpend },
recipient: { condition: "equal", value: accountAddress },
},
},
},
});
const bridge = createCrossChainPermission({
from: {
chain: baseSepolia,
token: mUSDOnBase,
maxAmount: maxBridgeSpend,
},
to: {
chain: arbitrumSepolia,
token: usdcOnArb,
recipient: accountAddress,
},
validAfter: BigInt(now),
validUntil: BigInt(validUntil),
settlementLayers: ["ACROSS"],
});
const result = await oneAuth.grantPermissions({
accountAddress,
// Arbitrum verifies the swap call. Base only verifies the Permit2 claim.
targetChains: [ARB_SEPOLIA_CHAIN_ID],
sourceChains: [BASE_SEPOLIA_CHAIN_ID],
sessionKeyAddress,
validAfter: now,
validUntil,
maxUses: 25,
permissions: swap.permissions,
crossChainPermits: [bridge],
contracts: swap.contracts,
});The grant dialog shows the bridge's source token and chain, destination token, recipient, cap, validity window, and settlement layer before approval. Labels and ABIs improve readability only; the structured bridge permit and selector policies are signed.
Do not omit the source chain: Permit2 calls isValidSignature on the
source-chain account. A SmartSession enabled only on the destination cannot
make the later headless claim, even when destination swap permission exists.
grantPermissions() returns:
{
success: true,
grantId,
sessionKeyHandle: {
sessionKeyAddress,
permissionId,
permissionIdsByChain,
accountAddress,
permissions,
chainId,
chainIds,
expiresAt,
},
permissionId,
permissionIdsByChain,
intentId,
intentIds,
status,
chainResults,
transactionHash,
transactionHashesByChain,
statusUrl,
waitUrl,
}Persist the private key, KMS key ID, or other signer reference needed later;
1auth never stores that capability. It stores public grant metadata, including
the recoverable sessionKeyHandle, permission IDs, chain status, transaction
hashes, and requesting origin. Grants are keyed by exact browser origin, not
clientId: https://checkout.example.com cannot recover grants created by
https://app.example.com.
Your app can recover existing grants from the same registered origin:
const grants = await oneAuth.listSessionGrants({
accountAddress,
});
const activeGrant = grants.success
? grants.grants.find((grant) => !grant.revokedAt)
: undefined;The registry is only a UX and recovery index. The on-chain SmartSession
validator remains the execution authority, and the app signer must produce
headless signatures. Use the handle's chainIds and permissionIdsByChain for
the executing chain.
Users can inspect and revoke grants in the 1auth account dialog. Revocation
submits SmartSession removeConfig; 1auth marks the registry entry revoked only
after submission.
Review in 1auth
The permission review answers what this signer can do later, rather than clear signing's what happens now. It groups actions by contract and function, showing applicable chains, ABI/selector trust badge, usage/time/parameter policies, unconstrained values, and warnings for app-supplied ABIs, raw selectors, no expiry, or broad permissions.
Only 1auth's clear-signing registry produces a verified badge; app-supplied ABI metadata is an unverified readability hint.
Execute headlessly
Use the returned handle with the headless client. This opens no additional approval dialog: your app signs with the session key and on-chain SmartSession enforces the approved scope.
import { OneAuthHeadlessClient } from "@rhinestone/1auth/headless";
const headless = new OneAuthHeadlessClient({
providerUrl: "https://passkey.1auth.app",
clientId: "your-client-id",
sponsorship: {
accessTokenUrl: "/api/sponsorship/access-token",
extensionTokenUrl: "/api/sponsorship/extension-token",
},
});
const prepared = await headless.prepareIntent({
accountAddress,
targetChain: 84532,
calls,
sessionKeyHandle: stored.handle,
});
// Sign prepared SmartSession hashes with your app-managed signer,
// encode the validator-prefixed signatures, then submit.
const submitted = await headless.submitIntent({
intentOp: prepared.intentOp,
digestResult: prepared.digestResult,
binding: prepared.binding,
accountAddress: prepared.accountAddress,
targetChain: prepared.targetChain,
calls: prepared.calls,
expiresAt: prepared.expiresAt,
originSignatures,
destinationSignature,
targetExecutionSignature,
});See the Headless Mode guide for the signing and submit details, including validator-prefixed signature encoding and intent status polling.
Advanced example: approve and deposit
The same pattern can grant a small workflow instead of one function. For
an ERC-4626 vault deposit, the session key needs permission to approve
USDC for the vault and then call deposit on the vault. Keep both
actions scoped to the same spender, receiver, and maximum amount.
import { definePermissions } from "@rhinestone/1auth";
import { parseUnits } from "viem";
const erc20ApproveAbi = [
{
type: "function",
name: "approve",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
] as const;
const erc4626DepositAbi = [
{
type: "function",
name: "deposit",
stateMutability: "nonpayable",
inputs: [
{ name: "assets", type: "uint256" },
{ name: "receiver", type: "address" },
],
outputs: [{ name: "shares", type: "uint256" }],
},
] as const;
const maxDeposit = parseUnits("500", 6);
const validAfter = Math.floor(Date.now() / 1000);
const validUntil = validAfter + 24 * 60 * 60;
const usdcApproval = definePermissions({
address: USDC,
name: "USDC",
abi: erc20ApproveAbi,
functions: {
approve: {
params: {
spender: {
condition: "equal",
value: vault,
},
amount: {
condition: "lessThanOrEqual",
value: maxDeposit,
},
},
},
},
});
const vaultDeposit = definePermissions({
address: vault,
name: "USDC Vault",
abi: erc4626DepositAbi,
functions: {
deposit: {
params: {
assets: {
condition: "lessThanOrEqual",
value: maxDeposit,
},
receiver: {
condition: "equal",
value: accountAddress,
},
},
},
},
});
const permissions = {
permissions: [...usdcApproval.permissions, ...vaultDeposit.permissions],
contracts: [...usdcApproval.contracts, ...vaultDeposit.contracts],
validAfter,
validUntil,
maxUses: 25,
};The review groups USDC.approve(spender, amount) and
USDC Vault.deposit(assets, receiver), including their shared limits and
constraints. With multiple targetChains, one approval covers every requested
chain while 1auth installs/enables the session on each needed chain.
Session limits
Usage limit
Limits how many times the session may use its granted actions.
await oneAuth.grantPermissions({
// ...
maxUses: 100,
});Time frame
Limits when the action may be used.
await oneAuth.grantPermissions({
// ...
validAfter: 0,
validUntil: Math.floor(Date.now() / 1000) + 24 * 60 * 60,
});Pass validAfter and validUntil as Unix timestamps in seconds, not milliseconds.
Value limit
Limits the native value for a use. Add it as a function config field
inside definePermissions().
functions: {
deposit: {
valueLimitPerUse: parseEther("0.1"),
params: { /* ... */ },
},
}Universal action
Constrains calldata parameters. Prefer writing this through
definePermissions():
params: {
asset: { condition: "equal", value: USDC },
amount: { condition: "lessThanOrEqual", value: parseUnits("50000", 6) },
onBehalfOf: { condition: "equal", value: treasury },
}definePermissions() derives the required calldata constraints.
Security notes
- Do not send private session keys to 1auth.
- Prefer exact parameter constraints over broad selector access.
- Treat app-supplied ABI labels as unverified readability hints.
- Show short expiries and usage limits for automated sessions.
- Rotate or revoke session keys when users disconnect or change devices.