Skip to content

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).


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:

  1. If the request already carries an Authorization header (guest or client tokens — see mechanisms 3 and 4), passes it through untouched.
  2. If a valid session exists, injects Authorization: Bearer <access_token> — refreshing it first if it's expiring soon.
  3. 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:
{ "error": "Session expired, please log in again", "code": "SESSION_EXPIRED" }

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/verify with 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 bearer refresh_token → new access token.
  • POST /auth/v1/signout — invalidates the refresh/id tokens, returns a URL for completing logout with the identity provider.

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

GET /auth/v1/getGuestToken/{code}

{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".
  • recipientId claim — 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 browserclient_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