Authentication¶
vScrawl supports a few distinct authentication mechanisms, used by different kinds of callers. Pick the one that matches who's calling:
| Mechanism | Who uses it | Resulting identity |
|---|---|---|
| Session cookie | The vScrawl web app, logged in normally | Normal logged-in user |
| Direct JWT bearer | Mobile app, any client that manages its own token storage | Normal logged-in user |
| Guest JWT | A signing link's landing page (no account required) | type: GUEST, scoped to one recipient row |
| Client-credentials JWT | A partner's own backend, integrating e-sign into another application | type: CLIENT, scoped to one registered Business App |
All four ultimately produce a JWT that the API validates; they differ in how that JWT is obtained and how it reaches the API (cookie vs. header).
1. Session cookie (the web app)¶
The web app never handles a bearer token directly in the browser. Logging in goes through a redirect-based flow: the server holds the real access/refresh tokens, and the browser only ever holds an opaque, HttpOnly session cookie.
sequenceDiagram
participant Browser
participant Server
participant Keycloak
Browser->>Server: GET /bff/auth/authorize
Server->>Browser: 302 redirect to Keycloak
Browser->>Keycloak: User logs in
Keycloak->>Browser: 302 redirect to /bff/auth/callback?code=...&state=...
Browser->>Server: GET /bff/auth/callback?code=...&state=...
Server->>Keycloak: Exchange code for tokens
Server-->>Server: Store access/refresh token server-side in session
Server->>Browser: Set-Cookie: session (HttpOnly)
Browser->>Server: Any API call (cookie sent automatically)
Server-->>Server: Look up session, inject Authorization: Bearer <access_token>
Endpoints¶
| Method | Path | Purpose |
|---|---|---|
| GET | /bff/auth/authorize |
Starts login. Query: idpHint (optional). |
| GET | /bff/auth/callback |
Login callback target. Query: code, state. Sets the session cookie, redirects back into the app. |
| GET | /bff/auth/me |
Returns the current session's decoded identity, or 401 if no valid session. |
| POST | /bff/auth/logout |
Ends the session, returns the URL to complete logout. |
Requests to every other route¶
Every other request just needs credentials: "include" (or your HTTP client's cookie-forwarding equivalent) — no manual header needed. The server:
- If the request already carries an
Authorizationheader (guest or client tokens — see mechanisms 3 and 4), passes it through untouched. - If a valid session exists, injects
Authorization: Bearer <access_token>— refreshing it first if it's expiring soon. - If the session's own max lifetime has been reached, or the token is hard-expired and can't be refreshed, ends the session and returns:
with HTTP 401. This shape only appears here — it's emitted before the request reaches the rest of the API, so it does not follow the shape described in Errors & Response Format. Treat a 401 with code: "SESSION_EXPIRED" as "redirect to login."
Remember me¶
Set once at login — controls whether the session cookie persists across browser restarts or ends when the browser closes.
2. Direct JWT bearer (password, SSO, social, smart card, passkey)¶
For clients that manage their own token storage (the mobile app, or any integration not using a session cookie), the API issues tokens directly. All of these end in the same result — an access/refresh token pair sent as a normal Authorization: Bearer <token> header on every subsequent request.
Password login¶
POST /auth/v1/signin
Content-Type: application/json
{
"username": "jane@example.com",
"password": "correct horse battery staple",
"grant_type": "PASSWORD",
"rememberMe": true
}
grant_type (GrantType enum) selects the flow:
| Value | Meaning |
|---|---|
PASSWORD |
Standard username/password login |
G_AUTHENTICATOR |
Password + TOTP code (send tOtp alongside) |
GOOGLE_AUTH |
Google-issued ID token exchange |
CLIENT_CREDENTIALS |
Partner/business-app login — see mechanism 4 |
REFRESH_TOKEN |
Exchange a refresh token for a new access token |
Success response (JwtAuthenticationResponse, HTTP 200):
{
"accessToken": "eyJhbGciOiJSUzI1NiIs...",
"tokenType": "Bearer",
"expiry": 300,
"refreshToken": "eyJhbGciOiJSUzI1NiIs...",
"rememberMe": true
}
Error response (HTTP 401), e.g. wrong credentials or MFA required — see Settings → Security for the MFA setup that this can require.
SSO / social / smart card / passkey¶
- SSO (redirect flow):
GET /auth/v1/signin(returns an authorization URL) → user completes login at the identity provider →POST /auth/v1/signin/callback(body:code,state,redirectUri) exchanges the code, same response shape as password login. - Google Sign-In:
GET /auth/v1/signin/social/google?ott=<google-id-token>— exchanges a Google ID token directly. - Smart card:
POST /auth/v1/smart_card/hash→ sign the returned hash with the smart card's private key client-side →POST /auth/v1/smart_card/signature/verifywith the signature → JWT response. - Passkeys (WebAuthn):
POST /webauthn/login/options(query:username) → browser's WebAuthn API produces an assertion →POST /webauthn/login/verify?username=...with the assertion JSON → JWT response.
Refreshing and signing out¶
GET /auth/v1/refresh-session— cookie or bearerrefresh_token→ new access token.POST /auth/v1/signout— invalidates the refresh/id tokens, returns a URL for completing logout with the identity provider.
3. Guest JWT (recipient signing links)¶
A workflow recipient who is not an existing platform user still needs to view and sign a document — without creating an account. This is handled by a short-lived, single-purpose guest token tied to one specific recipient row. Full walkthrough: Guest Signing.
sequenceDiagram
participant Owner as Workflow Owner
participant API
participant Email
participant Recipient
Owner->>API: POST /workflow/v1/{workflowId}/commence
API-->>API: Mint encrypted "code" (recipientId + workflowId)
API->>Email: Send commence link containing "code"
Email->>Recipient: Delivers email
Recipient->>API: GET /auth/v1/getGuestToken/{code}
API->>Recipient: GuestTokenResponse { accessToken, refreshToken, ... }
Recipient->>API: Authenticated calls with Authorization: Bearer <guest access token>
Obtaining the token¶
{code} is the opaque value embedded in the emailed commence link — never constructed by a client. Response (GuestTokenResponse, HTTP 200):
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"userId": 1042,
"workflowId": 887,
"tokenType": "Bearer",
"expiry": 3600
}
What's inside the token¶
sub= the recipient's user id — a guest recipient always has a corresponding user record, created automatically the moment they were added as a recipient.type: "GUEST".recipientIdclaim — present when the link was minted for a specific recipient row (the normal case), and authoritative: the API resolves which recipient row an action applies to from this claim first. This matters because one person can hold two distinct recipient rows in the same workflow (e.g. a real signer and someone else's hidden delegate/assistant row) — the claim keeps actions pointed at the right one.
Using the token¶
Send it as a normal Authorization: Bearer <guest access token> header. It is scoped narrowly to the one workflow/recipient it was minted for, not general platform access.
Step-up identity verification (optional, per recipient)¶
Some recipients are additionally flagged to require identity verification. See Guest Signing → Step-up identity verification for the OTP challenge this requires before signing.
4. Client-credentials JWT (partner integrations)¶
A partner backend that wants to programmatically create and dispatch workflows — or embed the signing UI in an iframe inside its own product — authenticates as a registered Business App, not as an end user. Full walkthrough: Partner & Iframe Embed.
One-time setup (self-service, per organization)¶
An org owner registers an app:
POST /organization/v1/apps
Content-Type: application/json
{
"clientId": "acme-crm",
"appName": "Acme CRM Integration",
"callbackUrl": "https://acme.example.com/vscrawl/callback",
"status": true
}
Response returns the client secret once — store it securely. See Organization → Business Apps for the full field reference.
Obtaining a token¶
The partner's backend calls the same sign-in endpoint used for password login, with a different grant type:
POST /auth/v1/signin
Content-Type: application/json
{
"grant_type": "CLIENT_CREDENTIALS",
"client_id": "acme-crm",
"client_secret": "•••••••••••••••••",
"email": "owner@acmeorg.com"
}
email identifies which org member's context the resulting token acts within — the Business App must belong to the same organization as this user, and the app must not be disabled.
Response is the same JwtAuthenticationResponse shape as password login, except the token's type claim is "CLIENT".
Using the token¶
With a client token, a partner backend can dispatch workflows from a template server-side, and a partner frontend can embed the vScrawl signing UI in an iframe. See Partner & Iframe Embed for both.
Never call the token endpoint from a browser — client_secret must stay server-side.
Identity summary¶
JWT type claim |
Typical caller |
|---|---|
| (none / standard token) | Logged-in end user |
GUEST |
Recipient signing link |
CLIENT |
Partner integration |