Skip to content
Vex ID / Documentation

Vex ID by Arioron

Sign in with Vex ID

Vex ID is Arioron's account system and a standard OAuth 2.0 and OpenID Connect provider. Add “Sign in with Vex ID” to any app with the OIDC library you already use: point it at the discovery document and it configures itself.

Discovery document

https://auth.arioron.com/oauth2/.well-known/openid-configuration

At a glance

Issuerhttps://auth.arioron.com/oauth2
FlowsAuthorization code with PKCE, refresh tokens, client credentials
ID tokensRS256 JWTs, keys at /oauth2/.well-known/jwks.json
ExtrasPopup sign-in (response_mode=web_message), account chooser, prompt, login_hint, end-session

Quickstart

1. Register an app in the developer console. Apps with a server get a client ID and secret. Single-page and mobile apps are public clients: no secret, PKCE instead.

2. Add your redirect URIs. Vex ID only redirects to an exact match; http://localhost:3000/callback is fine while you develop.

3. Sign someone in. In the browser the quickest way is the SDK and a popup:

<script src="https://auth.arioron.com/static/js/vexid-sdk.js"></script>
<button id="signin">Sign in with Vex ID</button>
<script>
  const vex = new VexID({
    clientId: 'YOUR_CLIENT_ID',
    redirectUri: window.location.origin + '/callback',
  });

  document.getElementById('signin').onclick = async () => {
    const session = await vex.signInWithPopup();
    console.log('Hello,', session.user.name, session.user.email);
  };
</script>

On a server, use your framework's OIDC library instead; see server-side apps.

Authorization code flow

Every integration, however it's packaged, is these three steps.

1. Send the user to Vex ID

Make a random code_verifier (43–128 characters), keep it with state in the user's session, and send its SHA-256 hash as the code_challenge.

GET /oauth2/authorize?response_type=code
    &client_id=YOUR_CLIENT_ID
    &redirect_uri=https://yourapp.example/callback
    &scope=openid%20profile%20email
    &state=RANDOM_STATE
    &code_challenge=BASE64URL_SHA256_OF_VERIFIER
    &code_challenge_method=S256 HTTP/1.1
Host: auth.arioron.com

The person signs in (or picks an account), approves your app once, and comes back to your redirect URI with ?code=…&state=…. Check that state matches.

2. Exchange the code

Codes last 5 minutes and work once. Confidential apps authenticate with HTTP Basic; public apps send client_id and the code_verifier.

curl -X POST https://auth.arioron.com/oauth2/token \
  -u "CLIENT_ID:CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code=AUTHORIZATION_CODE \
  -d redirect_uri=https://yourapp.example/callback \
  -d code_verifier=CODE_VERIFIER
import requests

token = requests.post("https://auth.arioron.com/oauth2/token", auth=("CLIENT_ID", "CLIENT_SECRET"), data={
    "grant_type": "authorization_code",
    "code": code,
    "redirect_uri": "https://yourapp.example/callback",
    "code_verifier": code_verifier,
}).json()
const res = await fetch('https://auth.arioron.com/oauth2/token', {
  method: 'POST',
  headers: { Authorization: 'Basic ' + Buffer.from('CLIENT_ID:CLIENT_SECRET').toString('base64') },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code,
    redirect_uri: 'https://yourapp.example/callback',
    code_verifier: codeVerifier,
  }),
});
const token = await res.json();
{
  "access_token": "uMjVq2…",
  "token_type": "Bearer",
  "expires_in": 864000,
  "refresh_token": "Hc7kP0…",
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6…",
  "scope": "openid profile email"
}

3. Read the user

curl https://auth.arioron.com/oauth2/userinfo -H "Authorization: Bearer ACCESS_TOKEN"
{
  "sub": "8b0c6c3e-2f4a-4e8e-9d1b-6a0f3c2b7e51",
  "name": "Ada Lovelace",
  "preferred_username": "ada",
  "picture": "https://…",
  "email": "ada@example.com",
  "email_verified": true
}
Store sub as the user's ID. It never changes; email and username can.

Multiple accounts

People can be signed in to several Vex ID accounts in the same browser and switch between them from the account menu. When more than one is signed in, Vex ID shows an account chooser before your consent screen, so they pick which account your app gets.

Ask for an account explicitly

Send prompt=select_account to always show the chooser, even with one account signed in, for example behind a “Switch account” button in your app. The chooser also lets them add another account.

await vex.signInWithPopup({ prompt: 'select_account' });
GET /oauth2/authorize?…&prompt=select_account HTTP/1.1

Preselect an account

If you already know who's signing in, send login_hint with their email or username. If that account is signed in on the browser, Vex ID uses it without asking; otherwise the sign-in form is prefilled.

await vex.signInWithPopup({ loginHint: 'ada@example.com' });

Force a fresh sign-in

prompt=login asks for credentials again, even if someone is signed in. Use it before sensitive actions.

Single-page & mobile apps

Apps that run entirely on the device can't keep a secret. Register them with Confidential client turned off: Vex ID then requires PKCE and lets them exchange codes and refresh tokens without a secret. The token, userinfo and revoke endpoints accept cross-origin requests.

const vex = new VexID({
  clientId: 'YOUR_PUBLIC_CLIENT_ID',
  redirectUri: location.origin + '/callback',
  scope: 'openid profile email offline_access',   // offline_access: get a refresh token
});

// On your sign-in button: redirect instead of a popup
await vex.signInWithRedirect({ returnTo: location.href });

// On /callback
const session = await vex.handleRedirectCallback();
location.replace(session.returnTo || '/');

// Later
vex.onAuthStateChanged((user) => render(user));
const fresh = await vex.refresh();   // new access token when it expires

Native mobile apps should use the system browser (ASWebAuthenticationSession, Custom Tabs) with a library such as AppAuth, and a redirect URI using your app's scheme.

Server-side apps

Use a maintained OIDC library: it handles state, PKCE, the code exchange and ID token checks.

# pip install flask authlib requests
from flask import Flask, redirect, session, url_for
from authlib.integrations.flask_client import OAuth

app = Flask(__name__)
app.secret_key = "change-me"

oauth = OAuth(app)
oauth.register(
    "vexid",
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    server_metadata_url="https://auth.arioron.com/oauth2/.well-known/openid-configuration",
    client_kwargs={"scope": "openid profile email", "code_challenge_method": "S256"},
)

@app.route("/login")
def login():
    return oauth.vexid.authorize_redirect(url_for("callback", _external=True))

@app.route("/callback")
def callback():
    token = oauth.vexid.authorize_access_token()   # checks state, PKCE and the ID token
    session["user"] = token["userinfo"]
    return redirect("/")
# pip install authlib requests
from authlib.common.security import generate_token
from authlib.integrations.requests_client import OAuth2Session

client = OAuth2Session(
    "YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET",
    scope="openid profile email",
    redirect_uri="https://yourapp.example/callback",
    code_challenge_method="S256",
)

code_verifier = generate_token(48)   # keep it, and state, in the user's session
url, state = client.create_authorization_url("https://auth.arioron.com/oauth2/authorize", code_verifier=code_verifier)

# on your callback
token = client.fetch_token("https://auth.arioron.com/oauth2/token",
                           authorization_response=callback_url, code_verifier=code_verifier)
user = client.get("https://auth.arioron.com/oauth2/userinfo").json()
// npm install openid-client@5
import { Issuer, generators } from 'openid-client';

const vex = await Issuer.discover('https://auth.arioron.com/oauth2');
const client = new vex.Client({
  client_id: 'YOUR_CLIENT_ID',
  client_secret: 'YOUR_CLIENT_SECRET',
  redirect_uris: ['https://yourapp.example/callback'],
  response_types: ['code'],
});

const code_verifier = generators.codeVerifier();   // keep it in the user's session
const url = client.authorizationUrl({
  scope: 'openid profile email',
  code_challenge: generators.codeChallenge(code_verifier),
  code_challenge_method: 'S256',
});

// on your callback
const tokens = await client.callback('https://yourapp.example/callback', client.callbackParams(req), { code_verifier });
const user = await client.userinfo(tokens.access_token);

Tokens & refresh

TokenLifetimeNotes
Access token10 daysSend as Authorization: Bearer …
Refresh token30 daysNeeds offline_access. Refreshing returns a new pair and revokes the old one.
ID token1 hourRS256 JWT describing the sign-in.
Authorization code5 minutesSingle use.

Refreshing

curl -X POST https://auth.arioron.com/oauth2/token \
  -u "CLIENT_ID:CLIENT_SECRET" \
  -d grant_type=refresh_token \
  -d refresh_token=REFRESH_TOKEN

Machine-to-machine

Apps with the client credentials grant enabled can get a token for themselves, with no user involved.

curl -X POST https://auth.arioron.com/oauth2/token -u "CLIENT_ID:CLIENT_SECRET" -d grant_type=client_credentials

Each sign-in gets its own tokens, so signing in on a second device doesn't sign out the first.

ID tokens

With the openid scope the token response includes an id_token. Verify it before you trust it: the RS256 signature against the JWKS, iss is https://auth.arioron.com/oauth2, aud is your client ID, exp is in the future and nonce matches what you sent. OIDC libraries do this for you.

{
  "iss": "https://auth.arioron.com/oauth2",
  "sub": "8b0c6c3e-2f4a-4e8e-9d1b-6a0f3c2b7e51",
  "aud": "YOUR_CLIENT_ID",
  "iat": 1791705600,
  "exp": 1791709200,
  "auth_time": 1791705590,
  "nonce": "RANDOM_NONCE",
  "email": "ada@example.com",
  "email_verified": true
}
import requests
from authlib.jose import JsonWebKey, jwt

jwks = JsonWebKey.import_key_set(requests.get("https://auth.arioron.com/oauth2/.well-known/jwks.json").json())
claims = jwt.decode(id_token, jwks, claims_options={
    "iss": {"essential": True, "value": "https://auth.arioron.com/oauth2"},
    "aud": {"essential": True, "value": "YOUR_CLIENT_ID"},
})
claims.validate()   # exp, iat and the options above

Signing out

Clear your own session, then send the user to the end-session endpoint to sign them out of Vex ID too. If several accounts are signed in, only the account the id_token_hint belongs to is signed out. post_logout_redirect_uri must be one of your redirect URIs.

await vex.signOut();                       // your app only (revokes the refresh token)
await vex.signOut({ everywhere: true });   // and Vex ID, then back to your redirect URI
GET /oauth2/logout?id_token_hint=ID_TOKEN
    &post_logout_redirect_uri=https://yourapp.example/
    &state=RANDOM_STATE HTTP/1.1
Host: auth.arioron.com

To end your app's access without signing anyone out of Vex ID, revoke the refresh token at /oauth2/revoke.

Authorize parameters

GEThttps://auth.arioron.com/oauth2/authorize
response_typestringRequired

code.

client_idstringRequired

Your app's client ID.

redirect_uristringRequired

Exactly one of your registered redirect URIs.

scopestringRequired

Space-separated scopes. Include openid for an ID token.

statestringRecommended

Returned unchanged. Check it on your callback.

code_challengestringPublic clients

PKCE challenge; code_challenge_method S256 (or plain). Required for public clients.

noncestring

Copied into the ID token.

promptstring

select_account shows the account chooser · login asks for credentials again · consent shows the consent screen again · none never shows anything: returns login_required or consent_required instead.

login_hintstring

Email or username. Picks that account if it's signed in on the browser, otherwise prefills the sign-in form.

response_modestring

query (default) · fragment · web_message for popup sign-in.

displaystring

page or popup.

Endpoints

All paths are relative to https://auth.arioron.com.

EndpointPurpose
GET /oauth2/.well-known/openid-configurationDiscovery document (also at /.well-known/openid-configuration).
GET /oauth2/.well-known/jwks.jsonPublic keys for verifying ID tokens.
GET /oauth2/authorizeStart sign-in. See parameters.
POST /oauth2/tokenauthorization_code, refresh_token, client_credentials. HTTP Basic or client_secret_post.
GET /oauth2/userinfoClaims for the access token’s user (GET or POST). Also at /api/v1/userinfo.
POST /oauth2/revokeRevoke an access or refresh token (RFC 7009).
POST /oauth2/introspectCheck a token (RFC 7662). Confidential clients, own tokens.
GET /oauth2/logoutEnd-session: id_token_hint, post_logout_redirect_uri, state.
GET /api/v1/meThe user’s profile (needs profile; email with the email scope).
POST /api/v1/validate-tokenQuick token check. JSON or form body with token.

Scopes & claims

ScopeGives youClaims
openidOpenID Connect and an ID token.sub
profileName, username and photo.name preferred_username picture updated_at
emailEmail address and whether it's verified.email email_verified
phonePhone number, if they added one.phone_number
offline_accessA refresh token.–

Browser SDK

https://auth.arioron.com/static/js/vexid-sdk.js

new VexID(options)

clientIdstringRequired

Your client ID.

redirectUristring

A registered redirect URI. Default location.origin + '/callback'.

scopestring

Default openid profile email.

exchange'browser' | 'server'

browser (default) gets tokens in the page; public clients only. server returns the code for your backend.

persist'local' | 'session' | 'memory'

Where the session is kept. Default local.

domainstring

Default https://auth.arioron.com.

Methods

signInWithPopup({ prompt, loginHint })Sign in in a popup. Resolves with the session (or the code with exchange: 'server').
signInWithRedirect({ prompt, loginHint, returnTo })Send this tab to Vex ID.
handleRedirectCallback()On your redirect URI: finish signing in.
getSession() · getUser()The stored session · fresh UserInfo.
refresh()New access token from the refresh token.
onAuthStateChanged(cb)Called with the user (or null) now and on every change, across tabs. Returns an unsubscribe function.
signOut({ everywhere })Sign out of your app, and optionally Vex ID.

Errors are thrown with a code: popup_blocked, popup_closed, state_mismatch, or an OAuth error such as access_denied.

Errors

The token, revoke and introspect endpoints answer errors with JSON and a 4xx status. Sign-in errors go back to your redirect URI as ?error=…&error_description=…&state=… (or in the popup message).

{ "error": "invalid_grant", "error_description": "Invalid \"code\" in request." }
ErrorUsually means
access_deniedThe person chose Cancel.
login_requiredprompt=none and nobody is signed in.
consent_requiredprompt=none and they haven't approved your app yet.
invalid_clientWrong client ID or secret, or the wrong authentication for the client type.
invalid_grantCode or refresh token expired, used, revoked, or the code_verifier doesn't match.
invalid_requestA required parameter is missing, or a public client didn't send PKCE.
unauthorized_clientThat grant type isn't enabled for your app.
invalid_tokenThe bearer token is missing, expired or revoked (401).

Stuck? Contact support or email support@arioron.com.