Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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.