Visa: Authentication API

Overview

The Visa service is a Go Echo web server located at https://auth.pivot.app responsible for authentication logic for Pivot users. The name 'Visa' is inspired by the visas used to enter a foreign country. Similarly, the Visa service identifies who a user is and verifies they can enter the system. Crucially, Visa is an authentication system but not an authorization system – the user can enter, but we don't know what they can or can't do.

The frontend for our authentication related routes is provided by the Next app, however unlike the Friend service, Visa provides Rest-style HTTP APIs rather than using a type safe protocol like Connect, because Visa is consumed programatically, by the browser, and as a redirect from various authentication providers.

Note that Visa is entirely designed for web browser based authentication flows. Next app is responsible for interfacing with Visa on behalf of our native apps (Expo and Tauri) as well.

Visa's features can be summerized as follows:

  1. Recieve a valid callback URL as a URL parameter, or default to the https://pivot.app/dashboard. Validate that the callback URL prefix is either https:// or pivot://
  2. Authenticate using email (magic link), Google, Apple, or SAML SSO.
    • Force SAML SSO if the domain of an email address matches a verified domain corresponding to an organization which has a valid Domain in the Facebox service. (Note that we are referring to any verified email that the user has, not neccessarilly the email they used to initiate the login.)
    • Send emails via a purpose-build Buzzbuzz gRPC method. (Buzzbuzz generally does not expose synchronous endpoints that other services can use to send their own emails, but Visa is an exception.)
  3. Support initial sign up flows via email, Apple, and Google as well as just-in-time SAML provisioning.
  4. Return a refresh token and access token as JWTs for both login and join flows.
  5. Interface with Facebox to accept an invite as a new verified user with the given email and provide back to the client a refresh token and access token.
  6. Return a new access token and refresh token for a valid refresh token.

Login Flow

Login

Join Flow

Join

Invite Acceptance Flow

Invite

Token Refresh Flow

Refresh

Source TLDraw (opens in a new tab)

API

Visa provides an HTTP JSON API on the public internet. Visa does not have a private API (it does not accept gRPC requests, NATS messages, or any other internal traffic).

/login/start and /login/callback

/join/start and /join/callback

The sign up routes are used to enable individual users to create new user accounts without being invited (and presumably then new organizations). Visa is responsible for authenticating them as usual, but this time it reaches to out to Facebox to create a new user rather than just to get the details of an existing user. After the email is verified with a magic link or a callback from Apple/Google, the user is created, and the refresh/access tokens are returned as normal.

/invite/redeem

When an invite URL is clicked, the app (Next, Expo, or Tauri) already has a secret value from that URL which identifies that the user who clicked the link has access to the email account the invite was sent to. Therefore, if the invite is being accepted by way of creating a new Pivot user account (and therefore corresponding JWTs), Visa is responsible for accepting an invite by calling out to Facebox to create the user, not the Friend service like you might expect.

The app needs a refresh token and an access token for the new user account in order to redirect from the invite acceptance screen to some other screen. Visa therefore provides a /invite/redeem route. This route takes in the same invite ID and secret that the Friend service used to render the invite information screen.

When called, Visa uses Facebox to redeem the invite and returns a refresh token and access token. (Facebox uses Blockhead or Messenge to provision the membership that corresponds to the invite acceptance.)

refresh

POST auth.pivot.app/token/refresh receives a refresh token from the Authorization header of the request and returns an access token and new refresh token.

Token Expiration

Access tokens expire after 5 minutes. This minimizes the risk of an exfiltrated token being used or a banned user maintaining access, but balances that with minimizing pressure on Visa's /token/refresh endpoint.

Refresh token expiration is set to 14 days, but a when an access token is refreshed the new refresh token will reset that timeline.

Existing User Cookie

When redirecting to the callback at the end of a successful login/signup flow, Visa sets the x-pivot-existing-user cookie for the .pivot.app domain, which is used to avoid routing the base URL to the marketing site for the root URL path.

pivot.app/pricing will still route to the marketing site, but pivot.app will redirect to pivot.app/dashboard instead if this cookie exists.

NATS

Publication

Visa publishes to ephemeral.visa.events.login.suceededV1 and .failedV1 via core NATS. Visa does not publish events when users are created, as it expects Facebox to do that, but when a user is created and a JWT is provided, Visa will publish a normal login message for that user.

TODO: Actually implement failed NATS messages in Visa, right not it doesn't do those.

Databases

N/A as the user and organization data Visa needs is stored in the Facebox service, so Visa makes gRPC requests to Facebox.

Deployment

Security

  • Rate limiting at the firewall level.
  • Validation of callback URLs in login requests
    • We allow pivot.app (pivot.dev in staging environment or the organization's domain for single-tenant deployments)
    • If the schema is pivot:// then we always allow, b/c that is going to our mobile app or desktop app.
    • Of course if callback URL is http, (no 's') then we error b/c we don't want to return access token without TLS.
    • Error if callback URL is null.
  • Visa validates that a user is not banned (according to Facebox) before providing a refresh token or refreshing an access token.
  • Visa checks the user's DenyTokensIssuedBefore property (from Facebox) before refreshing an access token. If that date is after the date the refresh token was issued, the operation should fail. This allows all refresh tokens issued for a user to be effectively rolled.
  • When using Apple, Google or SAML, the Visa service checks whether the user is banned or not and whether SAML is required or not after that provider has returned from its flow, because it is unknown what username/email the provider will return.