@authentify2026/consent-widget
v0.5.0
Published
Embeddable React consent widget for agent authorization
Downloads
47
Maintainers
Readme
@authentify2026/consent-widget
React modal component for Authentify's B2B2C agent-consent flow: a bank or fintech embeds it in their own end-user-facing app so their customer can approve or decline an AI agent acting on their behalf.
For the full pilot walkthrough (sandbox signup, API keys, wiring this widget in, UTP signals, go-live), see the integration guide on the Authentify site.
Install
npm install @authentify2026/consent-widgetRequires React 16.14+ (the build uses the automatic JSX runtime, react/jsx-runtime, added in 16.14).
Usage
import AuthentifyConsentWidget from '@authentify2026/consent-widget';
export function LoanApplicationForm() {
const [consentToken, setConsentToken] = useState('');
return (
<div>
{!consentToken ? (
<AuthentifyConsentWidget
sessionId="sess_abc123xyz"
apiBaseUrl="https://www.authentify.bz"
onApprove={(token) => setConsentToken(token)}
onDecline={() => console.log('User declined')}
/>
) : (
<p>✅ Consent approved! Processing...</p>
)}
</div>
);
}sessionId comes from the widget_url/session_id your backend receives
from POST /api/v1/consent-sessions.
Props
interface AuthentifyConsentWidgetProps {
sessionId: string;
onApprove?: (consentToken: string, expiresAt: string, agentName: string) => void;
onDecline?: () => void;
onError?: (error: string) => void;
apiBaseUrl?: string; // default: https://www.authentify.bz
theme?: 'light' | 'dark'; // default: 'dark'
branding?: {
tokens?: Partial<DesignTokens>; // override any subset of the theme's colors
logoUrl?: string; // shown in the modal header, left of the title
fontFamily?: string; // default: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif'
};
environment?: 'sandbox' | 'production'; // default: 'sandbox'
width?: string; // default: '100%'
height?: string; // default: 'auto'
agentName?: string;
scope?: string[];
expiresInDays?: number;
authRedirectUrl?: string;
onAuthRedirect?: (url: string) => void;
}environment isn't in the original spec's prop list but was added because
without it the widget could only ever authorize sandbox consents — every
call it makes takes environment as a request field, so a bank integrating
in production needs a way to set it.
agentName / scope / expiresInDays
Optional fallbacks. On open, the widget loads the session's own details from
GET {apiBaseUrl}/api/v1/consent-sessions/:id/status — the agent's name, the
institution's name, each requested permission in plain language, and how
long access lasts — and shows those, so the end user always sees what was
actually requested. These props are used only when an older API deployment
doesn't return the details. Approve is not available until the details have
loaded; an expired or unknown session shows "This request has expired" with
nothing to approve.
What it does
- Loads the session, then renders a modal: who is asking (institution and agent), each permission in plain language, how long access lasts, and Approve/Decline buttons.
- Approve (a click on the button — there is deliberately no Enter
shortcut, so a keypress meant for the host page can never grant access) →
POST {apiBaseUrl}/api/v1/consent/authorizewith{ session_id, customer_approval: true, environment }. On success, callsonApprove(consent_token, expires_at, agent_name)and closes. - Decline (button, backdrop click, Esc, or the header's close button) →
calls
onDecline(), closes, and records the refusal:POST {apiBaseUrl}/api/v1/consent/authorizewithcustomer_approval: false, which writesCONSENT_DECLINEDto your audit log and ends the session so the same link can't be approved later. - Errors from the API surface inline in the modal and via
onError.
authRedirectUrl / onAuthRedirect — identity verification
Set authRedirectUrl when the session was created with require_auth: true
(POST /consent-sessions). The widget then shows a "Verify it's you" step
before the approval screen instead of rendering an in-widget challenge —
Authentify never touches credentials or biometric data.
<AuthentifyConsentWidget
sessionId={sessionId}
authRedirectUrl="https://mybank.com/verify?session=abc123"
onAuthRedirect={(url) => { window.location.href = url; }}
/* ...other props */
/>Flow:
- Widget shows "Confirm your identity". Clicking calls
onAuthRedirect(authRedirectUrl). - You must implement
onAuthRedirectyourself — the widget runs in a cross-origin iframe and cannot perform a top-level navigation on its own. Omitting the prop leaves the button showing an error instead of silently doing nothing. - Your page does the actual
window.locationredirect to your own auth (OTP, MFA, whatever you already have). - After the customer authenticates, your backend calls
POST {apiBaseUrl}/api/v1/consent-sessions/{session_id}/confirm-authwithAuthorization: Bearer <your_api_key>, server-to-server. This is deliberately not "the browser came back from a redirect" as proof — that's spoofable by hitting the return URL directly without ever authenticating. - Redirect the browser back to the same page that renders the widget with
the same
sessionId. On mount, the widget callsGET {apiBaseUrl}/api/v1/consent-sessions/{session_id}/statusto check whether step 4 happened yet, shows "✓ Verified", and moves to the approval screen. POST /consent/authorizerefuses to grant (403) untilconfirm-authhas been called for that session — Approve will fail with "Identity verification required before approval" if you skip straight to it.
Omit authRedirectUrl entirely (the default) and the widget behaves exactly
as it did before this existed — no extra network call, straight to the
approval screen.
ConsentManager
The revocation screen — not part of the approval flow above, used separately (e.g. on an account-settings page):
import { ConsentManager } from '@authentify2026/consent-widget';
<ConsentManager apiBaseUrl="https://www.authentify.bz" customerToken={customerToken} />customerToken is the end user's consent_token from POST
/consent/authorize. It reaches only that end user's own consents — never
other end users' at the same institution — and stops working once it
expires. Lists active, paused and revoked consents (GET /customer/consents),
revokes one (DELETE /customer/consents/:id), and pauses or resumes one
(POST / DELETE /customer/consents/:id/pause), all authenticated via
customer_token as a query parameter. While an agent is paused, Authentify
denies everything it tries for that end user (consent_paused) until they
resume it; pausing keeps the consent, so resuming needs no new approval.
ActivityFeed
The end user's own audit trail: every action an agent took or tried on their behalf (allowed, blocked, approved, denied, waiting for approval), and when they connected, declined, paused or disconnected agents, grouped by day, in plain language with amounts.
import { ActivityFeed } from '@authentify2026/consent-widget';
<ActivityFeed apiBaseUrl="https://www.authentify.bz" customerToken={customerToken} theme="light" />Same customerToken as ConsentManager; reads GET
/api/v1/customer/activity. Requests show up when your agent passes the end
user's id as endUserId to POST /api/v2/authorize.
Styling
No CSS file to import. Styles are injected as a single <style> tag on
first mount (once per page, however many widget instances you render) —
this package builds with plain tsc and no bundler, so a static .css
file wouldn't reliably end up in dist/ for every consumer's toolchain.
Colors come from the Authentify design tokens (src/tokens.ts), switched
by the theme prop, then layered with any overrides you pass via
branding (see below).
Branding
Pass branding to make the modal match your own product instead of
Authentify's default look — every field is optional and additive; omit it
entirely and the widget renders exactly as it always has.
<AuthentifyConsentWidget
sessionId={sessionId}
theme="light"
branding={{
logoUrl: 'https://mybank.com/logo.png',
tokens: { accentBlue: '#c0392b', accentBlueText: '#ffffff' },
fontFamily: "'Helvetica Neue', Arial, sans-serif",
}}
/* ...other props */
/>branding.tokensoverrides any subset of the active theme'sDesignTokens(src/tokens.ts) — e.g. justaccentBlueto recolor the Approve button and scope pills, leaving everything else (backgrounds, borders, text colors) on the Authentify default for that theme.branding.logoUrlrenders an image (recommended height ~24px) to the left of the modal title in the header.branding.fontFamilyoverrides the default'Inter', -apple-system, BlinkMacSystemFont, sans-serifstack for the whole modal.
Try it live on the deployed demo with ?logo_url=...&accent_color=...
(see "Demo" below).
Development
npm install
npm run build # tsc -> dist/
npm run dev # tsc --watchPublishing
npm run build
npm publish --access publicRequires npm auth for the @authentify org (npm login / NPM_TOKEN) —
not something this repo or CI has by default.
Demo (deployed to Vercel)
This package itself has no HTML/bundler — npm run build only emits
dist/index.js via tsc, nothing servable. demo/ is a small separate
Vite app that imports the widget from this repo directly (file:..) and
renders it, so there's something Vercel can actually deploy as a static
site.
npm run build:demo # builds the library, then the demo -> demo/dist
npm run dev:demo # same, but starts the demo's dev serverVisiting the deployed URL with no query string renders the widget against
example data (sess_demo_example — not a real session, so Approve will
surface whatever error the live API returns for it). Pass a real consent
session via query params to test an actual one:
https://<deployment>/?session=sess_abc123&api_base_url=https://www.authentify.bz&environment=sandboxAdd &auth_redirect_url=... to also exercise the identity-verification step
— the demo page implements onAuthRedirect itself (a real window.location
redirect), since that's the host's responsibility, not the widget's.
vercel.json at the repo root builds the library first, then the demo,
and serves demo/dist as the output directory — that's all a Vercel
project pointed at this repo needs.
ActionApproval
The end user approves or denies one escalated agent action: the amount, the payee, the agent's stated reason and why they're being asked, with a countdown, then a receipt of their decision.
import { ActionApproval } from '@authentify2026/consent-widget';
<ActionApproval approvalToken={tokenFromLink} apiBaseUrl="https://www.authentify.bz" theme="light" />When an agent with end_user_approval turned on escalates an action it is
taking for a named endUserId, POST /api/v2/authorize returns
endUserApproval: { url, expiresAt }. Send or show that link to the end
user; the hosted page renders this component for ?approval=…. Links work
once and expire (30 minutes). Your staff can still decide from the
dashboard; whichever decision comes first stands. Put the agent's reason in
the authorize call's context.reason so the end user sees it.
