| Title: | Minimal OAuth 2.0 Client |
|---|---|
| Description: | A dependency-light OAuth 2.0 <https://www.rfc-editor.org/rfc/rfc6749> client supporting the client-credentials and authorization-code grants with token refresh. Built on 'curl' and 'jsonlite', with base R's socket server for the redirect listener, avoiding heavier HTTP stacks. |
| Authors: | Troy Hernandez [aut, cre] (ORCID: <https://orcid.org/0009-0005-4248-604X>), cornball.ai [cph] |
| Maintainer: | Troy Hernandez <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0.1 |
| Built: | 2026-06-21 15:14:26 UTC |
| Source: | https://github.com/cornball-ai/tinyoauth |
A preconfigured [oauth_client] for Claude-subscription-backed access, carrying Anthropic's authorize and token endpoints plus the Claude Code scope string. The client id is Anthropic's public Claude Code identifier, not a secret.
anthropic_claude_client()anthropic_claude_client()
A tinyoauth_client with an extra scope field.
anthropic_claude_client()anthropic_claude_client()
Build an authorization URL
oauth_authorize_url(client, scope = NULL, state = NULL)oauth_authorize_url(client, scope = NULL, state = NULL)
client |
A [oauth_client] with an |
scope |
Optional space-delimited scope string. |
state |
Optional opaque state for CSRF protection. |
The authorization URL to open in a browser.
oauth_authorize_url( oauth_client("id", token_url = "https://x/token", auth_url = "https://x/authorize"), scope = "user-read-email")oauth_authorize_url( oauth_client("id", token_url = "https://x/token", auth_url = "https://x/authorize"), scope = "user-read-email")
Authorization header value for a token
oauth_bearer(token)oauth_bearer(token)
token |
A |
A string like "Bearer abc123" for use as an HTTP
Authorization header.
## Not run: h <- curl::new_handle() curl::handle_setheaders(h, Authorization = oauth_bearer(tok)) ## End(Not run)## Not run: h <- curl::new_handle() curl::handle_setheaders(h, Authorization = oauth_bearer(tok)) ## End(Not run)
Default on-disk cache path for a client's token
oauth_cache_path(client)oauth_cache_path(client)
client |
A [oauth_client]. |
Path to the token cache file under tools::R_user_dir.
Define an OAuth 2.0 client
oauth_client(id, secret = NULL, token_url, auth_url = NULL, redirect_uri = "http://127.0.0.1:1410/")oauth_client(id, secret = NULL, token_url, auth_url = NULL, redirect_uri = "http://127.0.0.1:1410/")
id |
Client (application) id. |
secret |
Client secret, or NULL for public clients. |
token_url |
The provider's token endpoint. |
auth_url |
The provider's authorization endpoint (needed for the authorization-code grant; omit for client-credentials only). |
redirect_uri |
Redirect URI registered with the provider. Use a
loopback IP literal over http ( |
A tinyoauth_client object.
spotify <- oauth_client( id = "your_id", secret = "your_secret", token_url = "https://accounts.spotify.com/api/token", auth_url = "https://accounts.spotify.com/authorize")spotify <- oauth_client( id = "your_id", secret = "your_secret", token_url = "https://accounts.spotify.com/api/token", auth_url = "https://accounts.spotify.com/authorize")
Exchange an authorization code for a token
oauth_exchange_code(client, code)oauth_exchange_code(client, code)
client |
A [oauth_client]. |
code |
The authorization code from the redirect. |
A tinyoauth_token.
Is a token expired?
oauth_expired(token, leeway = 60)oauth_expired(token, leeway = 60)
token |
A |
leeway |
Seconds of slack before the hard expiry (default 60). |
TRUE if expired (or within leeway of it); FALSE
when there is no expiry recorded.
Reads a token cached by httr's oauth2.0_token() and returns a
tinyoauth client and token built from it – the app credentials, endpoints,
and (crucially) the refresh token. This lets a package migrating off
httr reuse an existing authorization instead of forcing users to log in
again.
oauth_import_httr(path = ".httr-oauth", which = 1L)oauth_import_httr(path = ".httr-oauth", which = 1L)
path |
Path to the httr cache (default |
which |
Which cached token to import when the file holds several (1-based; default 1). |
The imported access token is marked expired, since httr's cached access token is usually stale: the durable credential is the refresh token. Pass the result to [oauth_refresh] or [oauth_token] to mint a fresh access token.
A list with client (a [oauth_client]) and token (a
tinyoauth_token).
## Not run: imported <- oauth_import_httr("~/project/.httr-oauth") token <- oauth_refresh(imported$client, imported$token) ## End(Not run)## Not run: imported <- oauth_import_httr("~/project/.httr-oauth") token <- oauth_refresh(imported$client, imported$token) ## End(Not run)
Base64url-decodes the payload (middle) segment of a JSON Web Token and parses it as JSON. Does not verify the signature; use only on tokens you already trust (e.g. one the provider just issued you).
oauth_jwt_payload(x)oauth_jwt_payload(x)
x |
A JWT string, or a |
The decoded payload as a named list, or NULL if x has
no usable JWT.
# A toy token: header.payload.signature, payload = {"sub":"abc"} payload <- jsonlite::base64_enc(charToRaw('{"sub":"abc"}')) jwt <- paste("x", gsub("=", "", payload), "y", sep = ".") oauth_jwt_payload(jwt)$sub# A toy token: header.payload.signature, payload = {"sub":"abc"} payload <- jsonlite::base64_enc(charToRaw('{"sub":"abc"}')) jwt <- paste("x", gsub("=", "", payload), "y", sep = ".") oauth_jwt_payload(jwt)$sub
Refresh an access token
oauth_refresh(client, token)oauth_refresh(client, token)
client |
A [oauth_client]. |
token |
A |
A refreshed tinyoauth_token. Providers that omit a new refresh
token on refresh keep the existing one.
## Not run: tok <- oauth_refresh(spotify, tok) ## End(Not run)## Not run: tok <- oauth_refresh(spotify, tok) ## End(Not run)
Sends an HTTP request with the token as a Bearer header, retrying transient failures, and parses a JSON response. A convenience over building a curl handle by hand; for anything exotic, use [oauth_bearer] with curl directly.
oauth_request(token, url, method = "GET", query = NULL, body = NULL, headers = NULL, flatten = FALSE, retries = 3L)oauth_request(token, url, method = "GET", query = NULL, body = NULL, headers = NULL, flatten = FALSE, retries = 3L)
token |
A |
url |
Endpoint URL. |
method |
HTTP method (default "GET"). |
query |
Optional named list of query parameters. |
body |
Optional R object sent as a JSON body. |
headers |
Optional named character vector of extra headers. |
flatten |
Passed to |
retries |
Attempts on transport errors / HTTP 5xx (default 3). |
Parsed JSON, or invisibly NULL for an empty response body.
Non-2xx responses raise an error carrying the status and body.
## Not run: oauth_request(tok, "https://api.spotify.com/v1/me") ## End(Not run)## Not run: oauth_request(tok, "https://api.spotify.com/v1/me") ## End(Not run)
Returns a cached token if still valid; refreshes it if expired and a refresh
token is available; otherwise runs the authorization-code flow. The result is
written back to cache.
oauth_token(client, scope = NULL, cache = oauth_cache_path(client), ...)oauth_token(client, scope = NULL, cache = oauth_cache_path(client), ...)
client |
A [oauth_client]. |
scope |
Optional space-delimited scope string (for first authorization). |
cache |
Cache file path, or |
... |
Passed to [oauth_token_authcode] (e.g. |
A valid tinyoauth_token.
## Not run: tok <- oauth_token(spotify, scope = "user-read-email") ## End(Not run)## Not run: tok <- oauth_token(spotify, scope = "user-read-email") ## End(Not run)
The Claude analogue of [oauth_token]: returns a cached token if still valid,
refreshes it if expired and a refresh token is available, otherwise runs the
manual-paste login flow. The token is written back to cache.
oauth_token_anthropic(cache = oauth_cache_path(anthropic_claude_client()), open_url = interactive(), login = TRUE)oauth_token_anthropic(cache = oauth_cache_path(anthropic_claude_client()), open_url = interactive(), login = TRUE)
cache |
Cache file path, or |
open_url |
Open the authorization URL automatically (default: interactive sessions only). |
login |
Run the login flow when no usable cached/refreshable token exists
(default |
These are subscription credentials minted for Claude Code; using them is subject to Anthropic's terms for that product.
A tinyoauth_token with access_token,
refresh_token, and expires_at; or NULL when
login is FALSE and no usable token is cached.
## Not run: tok <- oauth_token_anthropic() curl::handle_setheaders(curl::new_handle(), Authorization = oauth_bearer(tok), "anthropic-beta" = "oauth-2025-04-20") ## End(Not run)## Not run: tok <- oauth_token_anthropic() curl::handle_setheaders(curl::new_handle(), Authorization = oauth_bearer(tok), "anthropic-beta" = "oauth-2025-04-20") ## End(Not run)
Prints (and optionally opens) the authorization URL, then obtains the
redirect either by catching it on a loopback listener (default) or, with
manual = TRUE, by having you paste the redirected URL back. After
verifying state, it exchanges the code.
oauth_token_authcode(client, scope = NULL, port = 1410L, open_browser = interactive(), timeout = 120, manual = NA)oauth_token_authcode(client, scope = NULL, port = 1410L, open_browser = interactive(), timeout = 120, manual = NA)
client |
A [oauth_client] with an |
scope |
Optional space-delimited scope string. |
port |
Loopback port for the listener; must match the port in
|
open_browser |
Open the URL automatically (default: interactive only). |
timeout |
Seconds to wait for the redirect. |
manual |
Skip the loopback listener and read the redirected address (or
bare code) from the console instead. The default ( |
A tinyoauth_token (with a refresh token, when the provider
issues one).
## Not run: tok <- oauth_token_authcode(spotify, scope = "user-read-email") tok <- oauth_token_authcode(google, manual = TRUE) # force manual paste ## End(Not run)## Not run: tok <- oauth_token_authcode(spotify, scope = "user-read-email") tok <- oauth_token_authcode(google, manual = TRUE) # force manual paste ## End(Not run)
App-only access (no user context).
oauth_token_client(client)oauth_token_client(client)
client |
A [oauth_client]. |
A tinyoauth_token.
## Not run: tok <- oauth_token_client(spotify) ## End(Not run)## Not run: tok <- oauth_token_client(spotify) ## End(Not run)
The Codex analogue of [oauth_token]: returns a cached token if still valid,
refreshes it if expired and a refresh token is available, otherwise runs the
device-login flow. The token carries an extra account_id field (the
ChatGPT account id) and is written back to cache.
oauth_token_openai_codex(cache = oauth_cache_path(openai_codex_client()), open_url = interactive(), timeout = 600, login = TRUE)oauth_token_openai_codex(cache = oauth_cache_path(openai_codex_client()), open_url = interactive(), timeout = 600, login = TRUE)
cache |
Cache file path, or |
open_url |
Open the verification URL automatically (default: interactive sessions only). |
timeout |
Seconds to wait for device authorization (default 600). |
login |
Run the device-login flow when no usable cached/refreshable
token exists (default |
A tinyoauth_token with access_token, refresh_token,
expires_at, and account_id; or NULL when login
is FALSE and no usable token is cached.
## Not run: tok <- oauth_token_openai_codex() curl::handle_setheaders(curl::new_handle(), Authorization = oauth_bearer(tok), "chatgpt-account-id" = tok$account_id) ## End(Not run)## Not run: tok <- oauth_token_openai_codex() curl::handle_setheaders(curl::new_handle(), Authorization = oauth_bearer(tok), "chatgpt-account-id" = tok$account_id) ## End(Not run)
Reads the chatgpt_account_id claim that OpenAI nests under
https://api.openai.com/auth in the access-token JWT.
openai_codex_account_id(token)openai_codex_account_id(token)
token |
A |
The account id string, or NULL if absent.
A preconfigured [oauth_client] for ChatGPT-subscription-backed Codex access, carrying OpenAI's device-authorization endpoints alongside the standard token endpoint. The client id is OpenAI's public native-app identifier, not a secret.
openai_codex_client()openai_codex_client()
A tinyoauth_client with extra device_usercode_url,
device_token_url, and verification_uri fields.
openai_codex_client()openai_codex_client()