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

Permissions

Permissions let a user add a scoped session key to their account. The app owns the session key; 1auth owns the approval UI, passkey authorization, and install/enable transaction.

Use this when your app needs to execute a narrow set of actions later without opening a passkey dialog for every intent.

This guide covers the one-time grant. After the user approves the permission, continue with Headless Mode to prepare, sign outside 1auth, and submit intents under that permission.

1auth never receives the session private key.

Permissions are multi-chain by default. Pass chains where selector permissions execute in targetChains. For cross-chain funding, pass chains that only need settlement authorization in sourceChains. 1auth asks the user for one SmartSession owner signature that commits to all session chains, then submits the required install/enable intents per chain.

For cross-chain routes, put the destination execution chain in targetChains and every funding chain in sourceChains. For example, an Arbitrum Sepolia swap funded by mUSD on Base Sepolia needs selector permissions on Arbitrum Sepolia for the swap calls and source-chain coverage on Base Sepolia for the Permit2 claim.

Grant scoped access

Create or load a signer

Create or load the ECDSA signer your app will use later for headless execution. The signer can live in the browser, on your backend, in AWS KMS, another HSM/KMS, or any signing system that can produce the SmartSession ECDSA signatures later.

Only the public sessionKeyAddress is sent to 1auth.

Define permissions

Use definePermissions() so app code does not hardcode selectors or calldata offsets. Start with a narrow permission: this session key may mint exactly 0.1 mUSD, and only to the user's own account.

import { definePermissions } from "@rhinestone/1auth";
import { parseUnits } from "viem";
 
const mockUsdMintAbi = [
  {
    type: "function",
    name: "mint",
    stateMutability: "nonpayable",
    inputs: [
      { name: "to", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    outputs: [],
  },
] as const;
 
const mintAmount = parseUnits("0.1", 6);
const validAfter = Math.floor(Date.now() / 1000);
const validUntil = validAfter + 24 * 60 * 60;
 
const permissions = definePermissions({
  address: mUSD,
  name: "mUSD",
  abi: mockUsdMintAbi,
  functions: {
    mint: {
      params: {
        to: {
          condition: "equal",
          value: accountAddress,
        },
        amount: {
          condition: "equal",
          value: mintAmount,
        },
      },
    },
  },
});

definePermissions() returns the permissions and contracts that grantPermissions() expects.

Grant permissions

Create or load the session key in your app, then pass only its public address to 1auth. The key can live wherever your app's signer lives: browser storage, your backend, AWS KMS, another HSM/KMS, or any signer that can produce the SmartSession ECDSA signatures later.

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);
}

appSigner is whatever signer your application controls. In a browser demo it might wrap a locally generated private key; in production it can be a backend service, AWS KMS key, HSM, or another signing system. The only value sent into grantPermissions() is the signer's public sessionKeyAddress.

After this one-time grant, the app can use that session key to submit a matching mint(accountAddress, parseUnits("0.1", 6)) intent without opening another user-signature dialog. The SmartSession validator enforces the to and amount constraints the user approved.

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,
  resolveTokenAddress,
} 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 = resolveTokenAddress("USDC", ARB_SEPOLIA_CHAIN_ID);
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,
});

crossChainPermits is part of the safety boundary in the grant dialog: the user sees the source token, source chain, destination token, recipient, cap, validity window, and settlement layer before approving. App-supplied labels and ABIs can improve readability, but the signable permission is the structured bridge permit plus the selector policies.

Do not omit the source chain from sourceChains. Permit2 calls isValidSignature on the source-chain account during the claim. If the SmartSession is only enabled on the destination chain, the later headless route cannot use the permission even though the 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 whatever your app needs to sign later: the session private key, a KMS key identifier, or another signer reference. 1auth never stores the private key or signing capability.

1auth does store public grant metadata for the requesting origin after a successful grant. The stored record includes the recoverable sessionKeyHandle, permission IDs, chain status, transaction hashes, and the origin that requested the grant. Grants are keyed by the exact browser origin, not by clientId, so 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 a UX and recovery index. The SmartSession validator is still the source of truth for execution, and the app-controlled signer must still produce the headless signatures. The handle includes chainIds and permissionIdsByChain so your headless signer can choose the right permission for the chain it is executing on.

Users can inspect and revoke stored grants from the 1auth account dialog. Revocation submits a SmartSession removeConfig transaction for the selected grant; 1auth marks the registry entry revoked only after that transaction is submitted.

Review in 1auth

The grant iframe renders a permission-specific review. It answers:

What can this signer do later?

This is different from transaction clear signing, which answers:

What will happen now?

The review groups permissions by contract and function. For each action, 1auth shows:

  • every chain the permission applies to,
  • contract and function name,
  • verified, app-supplied ABI, or raw selector badge,
  • policy chips such as 100 uses, Valid until ..., and Universal action: 2 parameter rules,
  • constrained parameters like to = 0x...account and amount = 100000,
  • unconstrained parameters as any value,
  • warnings for app-supplied ABIs, raw selectors, no expiry, or broad permissions.

For the mUSD mint example above, the review would show one permission group:

mUSD
mint(address to, uint256 amount)
App supplied ABI
25 uses | Valid until 24h from now | Universal action: 2 parameter rules
to = 0x...account
amount = 100000

Verified badges only come from 1auth's clear-signing registry. ABI metadata supplied by the app is useful for readability, but it is never marked verified.

Execute headlessly

After the permission is granted, use the returned handle with the headless client. This step does not open another 1auth approval dialog: your app signs with its session-key signer, and the on-chain SmartSession validator enforces the scope the user approved in the grant iframe.

import { OneAuthHeadlessClient } from "@rhinestone/1auth/headless";
 
const headless = new OneAuthHeadlessClient({
  providerUrl: "https://passkey.1auth.app",
  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,
  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,
};

In the 1auth review UI, this appears as two grouped permissions: one for USDC.approve(spender, amount) and one for USDC Vault.deposit(assets, receiver). Both groups show the same usage and time limits, plus the parameter constraints that make the permission safe for that specific vault flow. If you pass multiple targetChains, the same approval review and SmartSession owner signature cover every requested chain, while 1auth still submits the install/enable intent on each chain that needs it.

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,
});

validAfter and validUntil are Unix timestamps in seconds in the 1auth SDK and docs. 1auth normalizes these values before calling the experimental SmartSession encoder, which currently expects millisecond timestamps internally. Use seconds at the app boundary; do not multiply by 1000 yourself.

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 },
}

That expands to a SmartSession universal-action policy with the correct calldata offsets for asset, amount, and onBehalfOf.

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.