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 ..., andUniversal action: 2 parameter rules, - constrained parameters like
to = 0x...accountandamount = 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 = 100000Verified 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.