Skip to content

Headless / CLI Authentication

This page shows you how to authenticate Yoto users from a CLI tool or desktop app.

The recommended approach is to send the user to their browser to sign in, then catch the result automatically by briefly running a small local web server.

These steps aren’t specific to Yoto but part of the OAuth2 protocol 1. Don’t worry if you’ve never heard that term before.

The example code is written in JavaScript, but the concepts apply to any language that can make HTTP requests and create a web server.

  1. Your app generates a PKCE code verifier and challenge.
  2. Your app starts a temporary local web server on 127.0.0.1.
  3. Your app prints a login URL for the user to open in their browser.
  4. The user signs in, and Yoto redirects back to your local server with an authorization code.
  5. Your app exchanges that code (plus the PKCE verifier) for tokens.
  6. Your app stores the refresh token and reuses it on later runs, so the user only signs in once.

We’ll use a couple of small dependencies to keep the example terse: express to run the local callback server, and pkce-challenge to generate the PKCE values. Most languages have an equivalent PKCE helper (for example, the pkce package on PyPI).

Terminal window
npm install express pkce-challenge

1. Generate a PKCE code verifier and challenge

Section titled “1. Generate a PKCE code verifier and challenge”

PKCE lets a public client prove that the app exchanging the code is the same app that started the login, without needing a client secret. The verifier is a random string, and the challenge is its base64url-encoded SHA-256 hash.

import pkceChallenge from 'pkce-challenge';
const { code_verifier, code_challenge } = await pkceChallenge();

2. Start a local server and print the login URL

Section titled “2. Start a local server and print the login URL”

Start a local server on the loopback interface, then print Yoto’s authorization URL for the user to open in their browser. The redirect_uri must match the callback URL you registered in the developer dashboard.

This example requests the family:library:view scope alongside offline_access so the resulting app can read the user’s library and be refreshed later.

import express from 'express';
const clientId = '[YOUR-CLIENT-ID]';
const redirectUri = 'http://127.0.0.1:8787/callback';
const authUrl = new URL('https://login.yotoplay.com/authorize');
authUrl.search = new URLSearchParams({
audience: 'https://api.yotoplay.com',
scope: 'family:library:view offline_access',
response_type: 'code',
client_id: clientId,
code_challenge,
code_challenge_method: 'S256',
redirect_uri: redirectUri,
}).toString();
const app = express();
// Wait for Yoto to redirect back to our loopback callback with the code
const code = await new Promise(function waitForCode(resolve) {
app.get('/callback', function handleCallback(req, res) {
res.send(
'Login complete! You can close this tab and return to your terminal.'
);
resolve(req.query.code);
});
app.listen(8787, function onListening() {
console.log('To sign in, open this URL in your browser:');
console.log(authUrl.toString());
});
});

Now exchange the authorization code, along with the PKCE code_verifier from step 1, for tokens.

const tokenResponse = await fetch('https://login.yotoplay.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
code_verifier,
code,
redirect_uri: redirectUri,
}),
});
if (!tokenResponse.ok) {
throw new Error(`Token exchange failed: ${tokenResponse.statusText}`);
}
const { access_token, refresh_token } = await tokenResponse.json();

The response contains the access token and refresh token:

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.JzdWIiOiI",
"refresh_token": "1NiIsInR5cCI6IkpXVCJ9.IiOiI"
}

::: tip Access Token? Refresh Token? 🤔 If you’re not sure what these terms are, refer to our Authentication Overview :::

Store the refresh token somewhere safe (for example, a file in the user’s home directory). On later runs you can skip steps 1–3 entirely and jump straight to refreshing your tokens.

4. Use the access token to make API requests

Section titled “4. Use the access token to make API requests”

Now that you’ve got an access token, you can make API calls to Yoto’s services:

const response = await fetch('https://api.yotoplay.com/content/mine', {
headers: {
Authorization: `Bearer ${access_token}`,
},
});
const data = await response.json();
console.log(data.cards);
// you should see an array of cards in the console

Access tokens are JWTs (JSON Web Tokens) which contain their own expiration information. You have to decode the token to check when it expires:

// Simple function to decode the JWT payload (middle part)
function decodeJwt(token) {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = Buffer.from(base64, 'base64').toString('utf8');
return JSON.parse(jsonPayload);
}
// Check if the token is expired
function isTokenExpired(token) {
const decoded = decodeJwt(token);
// exp is in seconds, Date.now() is in milliseconds
// Add a 30-second buffer to refresh before it actually expires
return decoded.exp * 1000 < Date.now() + 30000;
}
// Usage
if (isTokenExpired(access_token)) {
console.log('Token is expired or about to expire, refreshing...');
// Proceed to refresh the token
} else {
console.log('Token is still valid');
}

When your access token expires, you’ll need to get a new set of tokens, which means using your refresh token to get a new access token as well as a new refresh token.

Refresh tokens are single-use, so always replace your stored refresh token with the new one you get back.

const tokenUrl = 'https://login.yotoplay.com/oauth/token';
const refreshResponse = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: '[YOUR-CLIENT-ID]',
refresh_token: '[YOUR-REFRESH-TOKEN]',
}),
});
const { access_token, refresh_token } = await refreshResponse.json();
// Now you can continue making API requests with the new access token

Now that you’ve got authentication working:

  1. This is specifically the Authorization Code flow with PKCE (Proof Key for Code Exchange) over a loopback redirect, which is what the OAuth 2.1 spec recommends for native apps.