Skip to content

Authentication#

Reading a public book needs no credential at all, but the bookshelf does have some features that require authentication, such as reading private books, drafting a book, and publishing one.

This page covers the methods the SDK and the bookshelf CLI support, which one to pick, and the order they are tried in. The environment variables named here are described in full in Configuration.

The bookshelf credentials may be accepted by other Climate Resource services for authentication and authorisation.

Authentication methods#

Pick the method that matches who is calling.

Method Who it is for How to start
Anonymous anyone reading public books nothing to do
User login, browser a person on a machine with a browser bookshelf auth login
User login, device code a person on a headless machine or over SSH bookshelf auth login --no-browser
Bearer token a script handed a token by something else $BOOKSHELF_TOKEN, or Bookshelf(auth="...")
GitHub Actions a workflow reading its organisation's books $BOOKSHELF_AUTH=github-actions

Log in as a person#

bookshelf auth login starts an interactive login and stores the result, so you do this once per machine rather than once per session.

On a machine with a browser it opens one, you sign in, and the credential comes back to the CLI.

On a machine with no browser it uses the device authorization flow instead, which moves the sign-in to a machine that has one. The CLI prints a short code and a URL:

Your code:  WDJB-MJHT
Visit       <the verification URL the issuer returns>
  1. The CLI prints the code and starts waiting.
  2. You open that URL on any other machine, on a phone if that is what is to hand.
  3. You sign in and confirm the code shown there matches the one the CLI printed.
  4. The CLI notices the approval and stores the credential.

The code expires after a few minutes. Run the command again for a fresh one.

bookshelf auth login always tries a browser first, so pass --no-browser to take this path. In Python, Bookshelf().ensure_authenticated() picks the device flow by itself in a notebook, an SSH session or a terminal with no usable browser.

The client renews the stored credential by itself, so a login lasts across sessions. If the issuer refuses to renew it, the client warns and falls back to anonymous access. Run bookshelf auth login again to replace it.

Use a token directly#

$BOOKSHELF_TOKEN is sent as a bearer token exactly as given. Nothing refreshes it, so a short-lived token will expire part way through a long run. bookshelf auth token prints a current token from a session that is already logged in, which is the usual way to get one.

In Python, auth= on Bookshelf overrides everything else. It takes a bearer token string, any httpx.Auth instance, or None.

from bookshelf import Bookshelf

with Bookshelf(auth="...") as bs:
    ...

auth=None stays unauthenticated even when a stored login exists, which is the way to guarantee a client only ever sees public data.

Read from GitHub Actions#

A workflow job reads its organisation's books with the token GitHub mints for it, so the repository holds no Bookshelf secret. The repository has to be enrolled, which is what the Bookshelf GitHub App installation does.

Give the job the permission and set the opt-in variable:

permissions:
  id-token: write
env:
  BOOKSHELF_AUTH: github-actions

The opt-in is deliberate. A job that holds id-token: write for something else never sends its token to Bookshelf unless the workflow explicitly opts in.

The token is read-only, and every write is refused, so a job that publishes still needs a credential of its own. Tokens last minutes, so the client mints a fresh one and replaces it whenever the API refuses one.

Credential precedence#

With auth= omitted, the client walks this chain and takes the first step that answers.

  1. $BOOKSHELF_TOKEN
  2. the job's GitHub Actions OIDC token, when $BOOKSHELF_AUTH is github-actions
  3. $BOOKSHELF_CLIENT_ID with $BOOKSHELF_CLIENT_SECRET
  4. the stored login for that deployment
  5. unauthenticated

Passing auth= skips the chain entirely. Any credentials are scoped to a single bookshelf deployment.

bookshelf auth whoami reports the current identity. In Python, Bookshelf().ensure_authenticated() confirms the API accepts the credential, and attempts to prompt for authentication if you are not logged in.

Manage stored logins#

One machine holds one login per deployment. Pick the deployment with --api-url or $BOOKSHELF_URL.

  • bookshelf auth list shows every stored login. --json emits one object per login. expired says the access token has expired, and needs_login says nothing stored can renew it, so only a fresh login brings it back.
  • bookshelf auth token prints the current access token and nothing else, refreshing it first when one is due.
  • bookshelf auth logout clears the stored login. --all covers every deployment.

Use a Bookshelf login against another service#

Other Climate Resource services verify the same WorkOS tokens, so a library talking to one of them can reuse the identity from bookshelf auth login. bookshelf.auth resolves the credential exactly as a Bookshelf client does.

import httpx
from bookshelf.auth import access_token, default_auth

with httpx.Client(base_url="https://ndc-api.example", auth=default_auth()) as client:
    client.get("/v1/me")

headers = {"Authorization": f"Bearer {access_token()}"}
  • default_auth() returns an httpx.Auth that refreshes the token and retries once on a 401.
  • A stored login whose refresh fails carries on unauthenticated with a warning. Pass strict=True to get the AuthenticationError instead.
  • access_token() returns a current bearer string for clients that are not built on httpx. It raises AuthenticationError when a stored login cannot be refreshed.
  • Both take api_url to choose which Bookshelf deployment's stored login to use.
  • The provider classes (ClientCredentials, ActionsOidcToken and the rest) are exported for building a credential by hand.

Whether a token is accepted is up to the receiving service. The GitHub Actions token is minted for the bookshelf-read audience, so pass ActionsOidcToken(audience=...) explicitly for a service that expects its own.

Every name in bookshelf.auth.__all__ is covered by the semver promise.

Where credentials are stored#

Credentials live in credentials.json, in the user config directory that platformdirs picks:

  • On Linux it is $XDG_CONFIG_HOME/bookshelf, which defaults to ~/.config/bookshelf.
  • On macOS it is ~/Library/Application Support/bookshelf. A recent platformdirs honours $XDG_CONFIG_HOME there too, so with it set the file is under $XDG_CONFIG_HOME/bookshelf instead.

bookshelf auth login prints the path it stored to, and bookshelf auth list --json shows what the file holds without reading it yourself. The file is written 0600.

It holds every stored login, one per deployment.

The file format may change in any release, so do not read or write it yourself, instead use CLI helpers such as bookshelf auth token to access the contents.