Shell Sign in once as the integration's user
# once: a PKCE pair and the URL to open in a private window
VERIFIER=$(openssl rand -base64 48 | tr -d '=+/\n')
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n')
echo "https://www.example.com/oauth/authorize?response_type=code&client_id=$COREDNA_CLIENT_ID&redirect_uri=https%3A%2F%2Fsync.example.org%2Foauth%2Fcallback&scope=offline&state=$(openssl rand -hex 16)&code_challenge=$CHALLENGE&code_challenge_method=S256"
# then, within five minutes of approving: exchange the code
curl https://www.example.com/oauth/token \
-d grant_type=authorization_code \
-d code=THE_CODE \
-d redirect_uri=https://sync.example.org/oauth/callback \
-d client_id="$COREDNA_CLIENT_ID" \
-d client_secret="$COREDNA_CLIENT_SECRET" \
-d code_verifier="$VERIFIER"
JSON The token file the module below reads and writes (expires_at 0 forces a refresh)
{
"access_token": "CDNA-A-…",
"expires_at": 0,
"refresh_token": "…"
}
JavaScript coredna.js
// coredna.js: the one module that holds the integration's credentials (Node 18 or later)
import { readFile, writeFile, rename } from 'node:fs/promises';
const SITE = process.env.COREDNA_URL; // https://www.example.com
const CLIENT_ID = process.env.COREDNA_CLIENT_ID;
const CLIENT_SECRET = process.env.COREDNA_CLIENT_SECRET;
const STORE = process.env.COREDNA_TOKEN_FILE; // readable by this service only
let tokens = null;
let refreshing = null; // one refresh at a time in this process
async function load() {
tokens ??= JSON.parse(await readFile(STORE, 'utf8'));
return tokens;
}
async function save(next) {
await writeFile(STORE + '.tmp', JSON.stringify(next), { mode: 0o600 });
await rename(STORE + '.tmp', STORE); // never a half-written file
tokens = next;
}
async function refresh() {
const { refresh_token } = await load();
const response = await fetch(SITE + '/oauth/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET, // in the body: a Basic header is refused
}),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error('OAuth refresh failed (' + (data.error ?? response.status) + '): sign in as the integration user again');
}
await save({
access_token: data.access_token,
expires_at: Date.now() + data.expires_in * 1000,
refresh_token: data.refresh_token ?? refresh_token, // a new one on the fiftieth exchange
});
}
async function accessToken(force = false) {
const t = await load();
if (force || t.expires_at - Date.now() < 5 * 60 * 1000) {
refreshing ??= refresh().finally(() => { refreshing = null; });
await refreshing;
}
return tokens.access_token;
}
export async function coredna(method, path, { query, body } = {}, retried = false) {
const url = new URL(SITE + '/cdna-api/' + path);
for (const [k, v] of Object.entries(query ?? {})) url.searchParams.set(k, v);
const response = await fetch(url, {
method,
headers: {
Authorization: 'Bearer ' + await accessToken(retried),
Accept: 'application/json',
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const data = await response.json().catch(() => ({}));
if (response.status === 403 && !retried && /Token doesn't exist or is not valid/.test(data.message ?? '')) {
return coredna(method, path, { query, body }, true); // refresh once and send it again
}
if (!response.ok) throw new Error(method + ' ' + path + ': ' + response.status + ' ' + (data.message ?? ''));
return data;
}
What the failures look like:
| Response | Meaning |
| 403, Token doesn't exist or is not valid, on an API call | The access token has expired: refresh and repeat the call |
400 invalid_grant from /oauth/token on a refresh | The refresh token is past its sixty days, used up, or was replaced on its fiftieth exchange and the new one was not stored |
400 invalid_grant on the code exchange | The code is more than five minutes old or was used already; a second use also revokes the tokens the first one issued |
400 invalid_client | The client id or secret is wrong |
403 Authentication failed from /oauth/token | The client's id and secret were sent as a Basic header, which the site reads as a user's username and password |
| 403 on one endpoint only | The integration's user lacks permission for that module |