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-configurationAt a glance
| Issuer | https://auth.arioron.com/oauth2 |
| Flows | Authorization code with PKCE, refresh tokens, client credentials |
| ID tokens | RS256 JWTs, keys at /oauth2/.well-known/jwks.json |
| Extras | Popup 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
}
sub as the user's ID. It never changes; email and username can.Popup sign-in
Sign people in without navigating away from your page. Vex ID opens in a small window; when they're done it hands the result straight back to your page and closes.
const vex = new VexID({ clientId: 'YOUR_CLIENT_ID', redirectUri: location.origin + '/callback' });
button.addEventListener('click', async () => {
try {
const session = await vex.signInWithPopup();
// session.user, session.accessToken, session.idToken
} catch (err) {
if (err.code === 'popup_closed') return; // they closed the window
if (err.code === 'popup_blocked') vex.signInWithRedirect();
else console.error(err.code, err.message);
}
});
// Open the window first (inside the click handler) so it isn't blocked.
const popup = window.open('', 'vexid', 'popup=yes,width=480,height=680');
popup.location = 'https://auth.arioron.com/oauth2/authorize?' + new URLSearchParams({
response_type: 'code', client_id: 'YOUR_CLIENT_ID',
redirect_uri: 'https://yourapp.example/callback',
scope: 'openid profile email', state, code_challenge, code_challenge_method: 'S256',
response_mode: 'web_message', display: 'popup',
});
window.addEventListener('message', (e) => {
if (e.origin !== 'https://auth.arioron.com' || e.source !== popup) return;
if (e.data?.source !== 'vexid') return;
const { code, state: returned, error } = e.data.response;
// check returned === state, then exchange the code
});
How it works
Add response_mode=web_message to the authorize request. Instead of redirecting the popup, Vex ID posts { type: 'authorization_response', source: 'vexid', response: { code, state } } (or error) to the window that opened it, and only to the origin of your registered redirect_uri. The redirect URI must still be registered, but nothing has to be hosted there for popups.
signInWithPopup() in the click handler itself, and fall back to signInWithRedirect() on popup_blocked.Exchanging on your server
Apps with a client secret should exchange the code on the server. Pass exchange: 'server' and the popup resolves with what your backend needs:
const vex = new VexID({ clientId: 'YOUR_CLIENT_ID', redirectUri: 'https://yourapp.example/callback', exchange: 'server' });
const { code, codeVerifier, redirectUri } = await vex.signInWithPopup();
await fetch('/auth/vexid', { method: 'POST', body: JSON.stringify({ code, codeVerifier, redirectUri }) });
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
| Token | Lifetime | Notes |
|---|---|---|
| Access token | 10 days | Send as Authorization: Bearer … |
| Refresh token | 30 days | Needs offline_access. Refreshing returns a new pair and revokes the old one. |
| ID token | 1 hour | RS256 JWT describing the sign-in. |
| Authorization code | 5 minutes | Single 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.
Endpoints
All paths are relative to https://auth.arioron.com.
| Endpoint | Purpose |
|---|---|
GET /oauth2/.well-known/openid-configuration | Discovery document (also at /.well-known/openid-configuration). |
GET /oauth2/.well-known/jwks.json | Public keys for verifying ID tokens. |
GET /oauth2/authorize | Start sign-in. See parameters. |
POST /oauth2/token | authorization_code, refresh_token, client_credentials. HTTP Basic or client_secret_post. |
GET /oauth2/userinfo | Claims for the access token’s user (GET or POST). Also at /api/v1/userinfo. |
POST /oauth2/revoke | Revoke an access or refresh token (RFC 7009). |
POST /oauth2/introspect | Check a token (RFC 7662). Confidential clients, own tokens. |
GET /oauth2/logout | End-session: id_token_hint, post_logout_redirect_uri, state. |
GET /api/v1/me | The user’s profile (needs profile; email with the email scope). |
POST /api/v1/validate-token | Quick token check. JSON or form body with token. |
Scopes & claims
| Scope | Gives you | Claims |
|---|---|---|
openid | OpenID Connect and an ID token. | sub |
profile | Name, username and photo. | name preferred_username picture updated_at |
email | Email address and whether it's verified. | email email_verified |
phone | Phone number, if they added one. | phone_number |
offline_access | A refresh token. | – |
Browser SDK
https://auth.arioron.com/static/js/vexid-sdk.jsnew VexID(options)
clientIdstringRequiredYour client ID.
redirectUristringA registered redirect URI. Default location.origin + '/callback'.
scopestringDefault 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.
domainstringDefault 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." }
| Error | Usually means |
|---|---|
access_denied | The person chose Cancel. |
login_required | prompt=none and nobody is signed in. |
consent_required | prompt=none and they haven't approved your app yet. |
invalid_client | Wrong client ID or secret, or the wrong authentication for the client type. |
invalid_grant | Code or refresh token expired, used, revoked, or the code_verifier doesn't match. |
invalid_request | A required parameter is missing, or a public client didn't send PKCE. |
unauthorized_client | That grant type isn't enabled for your app. |
invalid_token | The bearer token is missing, expired or revoked (401). |
Stuck? Contact support or email support@arioron.com.