Integration examples
Complete code, not fragments. Each example runs against
https://auth.mesogeos-digital.com once you fill in a client id.
1 What you need
An administrator registers your application and gives you the first two. The rest you decide.
| Client id | Identifies your application. Not secret. |
| Client secret | Confidential clients only — a server your users cannot read. Never in a browser or a mobile binary; use PKCE there instead. |
| Redirect URI | Where the user comes back to. Registered in advance and matched exactly — protocol, host, port and path. |
| Scopes | What you are asking for. See the table below. |
| Server | https://auth.mesogeos-digital.com |
Scopes this server knows
| Scope | Grants |
|---|---|
profile |
Access to basic profile information (name, picture, locale, etc.). |
email |
Access to the user's primary email address. |
phone |
Access to the user's phone number. |
address |
Access to the user's physical address. |
user |
Access to user account information. |
openid |
Required for OpenID Connect requests — enables the ID Token. |
offline_access |
Allow the application to perform actions on your behalf when you are not online (issues a refresh token). |
system:admin |
Full administrative access — grants all standard scopes. |
system:audit_read |
Read access to audit logs. |
system:health |
Access to system health and monitoring data. |
system:notifications_read |
Read user notifications. |
system:notifications_write |
Send and manage user notifications. |
3 Device flow (RFC 8628)
For anything that can display a short code but cannot host a browser — a TV, a set-top box, a headless tool. The device polls; the user authorizes from a phone.
Respect
interval. Polling faster earns a slow_down, and the correct
response to that is to add five seconds — not to retry immediately, which is how a device gets
itself rate-limited off the server.
/**
* Device flow from Node, or from anything with fetch.
*
* The loop is the whole thing: ask for a code, show it, then poll patiently
* until the user finishes, refuses, or the code ages out.
*/
const AUTH_SERVER = 'https://auth.mesogeos-digital.com';
const CLIENT_ID = 'your-device-client-id';
const sleep = (seconds) => new Promise((r) => setTimeout(r, seconds * 1000));
async function form(path, body) {
const response = await fetch(AUTH_SERVER + path, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(body),
});
return { status: response.status, payload: await response.json() };
}
async function authorizeDevice() {
// 1 — ask for a code
const { payload: device } = await form('/oauth/deviceauthorization', {
client_id: CLIENT_ID,
scope: 'openid profile',
});
if (device.error) {
throw new Error(device.error_description || device.error);
}
// 2 — show it. The complete URI has the code embedded, which saves typing
// when the device can render a QR code.
console.log('Go to: ' + device.verification_uri);
console.log('Code: ' + device.user_code);
console.log('Or: ' + device.verification_uri_complete);
// 3 — poll, no faster than the server asked for
let interval = device.interval || 5;
const deadline = Date.now() + (device.expires_in || 1800) * 1000;
while (Date.now() < deadline) {
await sleep(interval);
const { payload } = await form('/oauth/token', {
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
client_id: CLIENT_ID,
device_code: device.device_code,
});
if (payload.access_token) {
return payload;
}
switch (payload.error) {
case 'authorization_pending':
break; // expected; keep waiting
case 'slow_down':
interval += 5; // back off, do not retry immediately
break;
case 'access_denied':
throw new Error('the user declined');
case 'expired_token':
throw new Error('the code expired — start again');
default:
throw new Error(payload.error_description || payload.error);
}
}
throw new Error('timed out waiting for authorization');
}<?php
/**
* Device flow from PHP — a CLI tool or a kiosk process.
*/
declare(strict_types=1);
const AUTH_SERVER = 'https://auth.mesogeos-digital.com';
const CLIENT_ID = 'your-device-client-id';
function form(string $path, array $body): array
{
$handle = curl_init(AUTH_SERVER . $path);
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($body),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$raw = curl_exec($handle);
curl_close($handle);
return json_decode((string) $raw, true) ?: ['error' => 'unreachable'];
}
function authorizeDevice(): array
{
$device = form('/oauth/deviceauthorization', [
'client_id' => CLIENT_ID,
'scope' => 'openid profile',
]);
if (isset($device['error'])) {
throw new RuntimeException($device['error_description'] ?? $device['error']);
}
echo 'Go to: ' . $device['verification_uri'] . PHP_EOL;
echo 'Code: ' . $device['user_code'] . PHP_EOL;
$interval = (int) ($device['interval'] ?? 5);
$deadline = time() + (int) ($device['expires_in'] ?? 1800);
while (time() < $deadline) {
sleep($interval);
$token = form('/oauth/token', [
'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code',
'client_id' => CLIENT_ID,
'device_code' => $device['device_code'],
]);
if (isset($token['access_token'])) {
return $token;
}
switch ($token['error'] ?? '') {
case 'authorization_pending':
break; // keep waiting
case 'slow_down':
$interval += 5; // the server asked us to ease off
break;
case 'access_denied':
throw new RuntimeException('the user declined');
case 'expired_token':
throw new RuntimeException('the code expired — start again');
default:
throw new RuntimeException($token['error_description'] ?? $token['error'] ?? 'unknown error');
}
}
throw new RuntimeException('timed out waiting for authorization');
}\"\"\"Device flow with `requests`.\"\"\"
import time
import requests
AUTH_SERVER = "https://auth.mesogeos-digital.com"
CLIENT_ID = "your-device-client-id"
def authorize_device() -> dict:
device = requests.post(
f"{AUTH_SERVER}/oauth/deviceauthorization",
data={"client_id": CLIENT_ID, "scope": "openid profile"},
timeout=15,
).json()
if "error" in device:
raise RuntimeError(device.get("error_description") or device["error"])
print("Go to: ", device["verification_uri"])
print("Code: ", device["user_code"])
interval = device.get("interval", 5)
deadline = time.time() + device.get("expires_in", 1800)
while time.time() < deadline:
time.sleep(interval)
token = requests.post(
f"{AUTH_SERVER}/oauth/token",
data={
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"client_id": CLIENT_ID,
"device_code": device["device_code"],
},
timeout=15,
).json()
if "access_token" in token:
return token
error = token.get("error")
if error == "authorization_pending":
continue # expected
if error == "slow_down":
interval += 5 # back off rather than retry
continue
if error == "access_denied":
raise RuntimeError("the user declined")
if error == "expired_token":
raise RuntimeError("the code expired — start again")
raise RuntimeError(token.get("error_description") or error)
raise TimeoutError("timed out waiting for authorization")AUTH_SERVER="https://auth.mesogeos-digital.com"
CLIENT_ID="your-device-client-id"
# 1 — ask for a code
curl -sS -X POST "$AUTH_SERVER/oauth/deviceauthorization" \
-d client_id="$CLIENT_ID" \
-d scope="openid profile" | jq .
# {
# "device_code": "GmRhmhcx…",
# "user_code": "WDJB-MJHT",
# "verification_uri": "$AUTH_SERVER/Device",
# "verification_uri_complete": "$AUTH_SERVER/Device?user_code=WDJB-MJHT",
# "expires_in": 1800,
# "interval": 5
# }
# 2 — open verification_uri in a browser and enter the user_code
# 3 — poll. Wait `interval` seconds between attempts; on slow_down, add five.
DEVICE_CODE="from-the-response"
while true; do
RESPONSE=$(curl -sS -X POST "$AUTH_SERVER/oauth/token" \
-d grant_type="urn:ietf:params:oauth:grant-type:device_code" \
-d client_id="$CLIENT_ID" \
-d device_code="$DEVICE_CODE")
echo "$RESPONSE" | jq -r '.error // "authorized"'
echo "$RESPONSE" | jq -e '.access_token' >/dev/null 2>&1 && break
case "$(echo "$RESPONSE" | jq -r '.error')" in
authorization_pending) sleep 5 ;;
slow_down) sleep 10 ;;
*) echo "$RESPONSE" | jq .; break ;;
esac
done4 Token operations
The four calls you make after a flow completes. All shown with curl; the shape is the same in any language.
AUTH_SERVER="https://auth.mesogeos-digital.com"
CLIENT_ID="your-client-id"
CLIENT_SECRET="your-client-secret"
ACCESS_TOKEN="…"
# Who is this token for?
curl -sS "$AUTH_SERVER/oauth/userinfo" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .
# Is it still good? (RFC 7662 — for a resource server, not for the client that
# holds the token; a client already knows.)
curl -sS -X POST "$AUTH_SERVER/oauth/introspect" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d token="$ACCESS_TOKEN" | jq .
# {"active":true,"scope":"openid profile","client_id":"…","exp":1640995200}
# Renew it
curl -sS -X POST "$AUTH_SERVER/oauth/token" \
-d grant_type=refresh_token \
-d client_id="$CLIENT_ID" \
-d refresh_token="…" | jq .
# Throw it away. Do this on logout — an abandoned token stays valid.
curl -sS -X POST "$AUTH_SERVER/oauth/revoke" \
-d client_id="$CLIENT_ID" \
-d token="$ACCESS_TOKEN"
# 200 with an empty body, whether or not the token existed: RFC 7009 says a
# revocation endpoint must not become an oracle for which tokens are real.