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 every chain where the session should be valid in targetChains. 1auth asks the user for one SmartSession owner signature that commits to all requested chains, then submits the required install/enable intents per chain.

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 MockUSD, and only to the user's own account.

import { definePermissions, type SmartSessionPolicy } 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 validUntil = Math.floor(Date.now() / 1000) + 24 * 60 * 60;
const policies: SmartSessionPolicy[] = [
  { type: "usage-limit", limit: 25n },
  { type: "time-frame", validAfter: 0, validUntil },
];
 
const permissions = definePermissions({
  address: MockUSD,
  name: "MockUSD",
  abi: mockUsdMintAbi,
  functions: {
    mint: {
      policies,
      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.box",
  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,
  ...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.

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 MockUSD mint example above, the review would show one permission group:

MockUSD
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.box",
  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, type SmartSessionPolicy } 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 validUntil = Math.floor(Date.now() / 1000) + 24 * 60 * 60;
const sharedPolicies: SmartSessionPolicy[] = [
  { type: "usage-limit", limit: 25n },
  { type: "time-frame", validAfter: 0, validUntil },
];
 
const usdcApproval = definePermissions({
  address: USDC,
  name: "USDC",
  abi: erc20ApproveAbi,
  functions: {
    approve: {
      policies: sharedPolicies,
      params: {
        spender: {
          condition: "equal",
          value: vault,
        },
        amount: {
          condition: "lessThanOrEqual",
          value: maxDeposit,
        },
      },
    },
  },
});
 
const vaultDeposit = definePermissions({
  address: vault,
  name: "USDC Vault",
  abi: erc4626DepositAbi,
  functions: {
    deposit: {
      policies: sharedPolicies,
      params: {
        assets: {
          condition: "lessThanOrEqual",
          value: maxDeposit,
        },
        receiver: {
          condition: "equal",
          value: accountAddress,
        },
      },
    },
  },
});
 
const permissions = {
  permissions: [...usdcApproval.permissions, ...vaultDeposit.permissions],
  contracts: [...usdcApproval.contracts, ...vaultDeposit.contracts],
};

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.

Policy types

Usage limit

Limits how many times the session may use an action.

{ type: "usage-limit", limit: 100n }

Time frame

Limits when the action may be used.

{
  type: "time-frame",
  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.

{ type: "value-limit", limit: parseEther("0.1") }

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.