| Type: | Package |
| Title: | Minimal OAuth 2.0 Client |
| Version: | 0.1.0 |
| Date: | 2026-06-10 |
| 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. Includes a helper for the 'OpenAI' https://openai.com/ 'Codex' device login, a similar but non-standard variant of the OAuth 2.0 device authorization grant. |
| License: | MIT + file LICENSE |
| Encoding: | UTF-8 |
| Depends: | R (≥ 4.0) |
| Imports: | curl, jsonlite |
| Suggests: | tinytest |
| URL: | https://github.com/cornball-ai/tinyoauth |
| BugReports: | https://github.com/cornball-ai/tinyoauth/issues |
| NeedsCompilation: | no |
| Packaged: | 2026-06-10 06:05:34 UTC; troy |
| Author: | Troy Hernandez |
| Maintainer: | Troy Hernandez <troy@cornball.ai> |
| Repository: | CRAN |
| Date/Publication: | 2026-06-17 17:40:02 UTC |
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. Includes a helper for the 'OpenAI' <https://openai.com/> 'Codex' device login, a similar but non-standard variant of the OAuth 2.0 device authorization grant.
Package Content
Index of help topics:
oauth_authorize_url Build an authorization URL
oauth_bearer Authorization header value for a token
oauth_cache_path Default on-disk cache path for a client's token
oauth_client Define an OAuth 2.0 client
oauth_exchange_code Exchange an authorization code for a token
oauth_expired Is a token expired?
oauth_import_httr Import an httr '.httr-oauth' cache into
tinyoauth
oauth_jwt_payload Decode a JWT payload
oauth_refresh Refresh an access token
oauth_request Make an authenticated request
oauth_token Get a valid token, using the cache and
refreshing as needed
oauth_token_authcode Run the authorization-code flow end to end
oauth_token_client Fetch a token via the client-credentials grant
oauth_token_openai_codex
Get a valid OpenAI Codex token, using the cache
and refreshing as needed
openai_codex_account_id
Extract the ChatGPT account id from a Codex
token
openai_codex_client OAuth client for the OpenAI Codex (ChatGPT)
device-login flow
Maintainer
Troy Hernandez <troy@cornball.ai>
Author(s)
Troy Hernandez [aut, cre] (ORCID: <https://orcid.org/0009-0005-4248-604X>), Sounkou Mahamane Toure [ctb], cornball.ai [cph]
Build a tinyoauth_token from a parsed token response
Description
Build a tinyoauth_token from a parsed token response
Usage
.as_token(body)
HTTP Basic authorization header value for client credentials
Description
HTTP Basic authorization header value for client credentials
Usage
.basic_auth(id, secret)
Poll the device-token endpoint until the user authorizes (or we time out)
Description
Poll the device-token endpoint until the user authorizes (or we time out)
Usage
.codex_device_poll(client, device, timeout = 600, sleep = Sys.sleep,
post = .codex_post_json)
Arguments
sleep |
Sleep function, injectable for testing. |
post |
JSON-POST function, injectable for testing. |
Start the device-authorization flow: get a user code to display
Description
Start the device-authorization flow: get a user code to display
Usage
.codex_device_start(client, post = .codex_post_json)
Arguments
post |
JSON-POST function, injectable for testing. |
Exchange the device authorization code (with its PKCE verifier) for a token
Description
Exchange the device authorization code (with its PKCE verifier) for a token
Usage
.codex_exchange(client, code, verifier)
Attach the ChatGPT account id (from the access-token JWT) to a token
Description
Attach the ChatGPT account id (from the access-token JWT) to a token
Usage
.codex_finalize(token)
Run the device-login flow end to end (display code, wait, exchange)
Description
Run the device-login flow end to end (display code, wait, exchange)
Usage
.codex_login(client, open_url = interactive(), timeout = 600)
Classify a device-token poll response
Description
Returns one of "ok" (authorization granted), "pending" (keep
waiting), "slow_down" (back off), or "error" (give up).
Usage
.codex_poll_classify(status, body)
POST a JSON body and parse the JSON response
Description
The device endpoints take JSON (the token endpoint takes form-encoding, which is what [.token_request] handles).
Usage
.codex_post_json(url, body)
Drop NULL list elements
Description
Drop NULL list elements
Usage
.drop_null(x)
Fetch with a few retries on transport errors / 5xx
Description
Fetch with a few retries on transport errors / 5xx
Usage
.fetch_retry(url, handle, times = 3L)
Form-encode a named list as application/x-www-form-urlencoded, dropping NULLs
Description
Form-encode a named list as application/x-www-form-urlencoded, dropping NULLs
Usage
.form_encode(fields)
Catch a single OAuth redirect on a loopback port
Description
Opens a one-shot serverSocket() listener, accepts the browser
redirect, replies with a small page, and returns the parsed query.
Usage
.listen_for_redirect(port = 1410L, timeout = 120)
Arguments
port |
Loopback port to listen on (must match the redirect URI). |
timeout |
Seconds to wait for the redirect. |
Value
Named list of query parameters from the redirect.
Is this a session where a loopback OAuth listener cannot catch the redirect?
Description
TRUE for SSH sessions, RStudio Server, and headless unix (no X or
Wayland display): in all of these the browser runs on a different machine, so
the redirect to a 127.0.0.1 listener never arrives and the listener
would hang. [oauth_token_authcode] uses this to default to the manual paste
flow. macOS desktops have no DISPLAY but a working browser, so they
are not treated as headless.
Usage
.oauth_no_loopback()
Parse a pasted redirect URL (or bare code) into query parameters
Description
Accepts the full http://127.0.0.1:.../?code=...&state=... address the
browser landed on, a bare code=...&state=... query string, or just the
code value on its own. Used by the manual = TRUE (no-listener) path.
Usage
.parse_redirect_input(x)
Parse the query parameters from an HTTP request line
Description
Parse the query parameters from an HTTP request line
Usage
.parse_request_query(req_line)
POST a grant to the token endpoint and parse the result
Description
POST a grant to the token endpoint and parse the result
Usage
.token_request(client, fields)
Build an authorization URL
Description
Build an authorization URL
Usage
oauth_authorize_url(client, scope = NULL, state = NULL)
Arguments
client |
A [oauth_client] with an |
scope |
Optional space-delimited scope string. |
state |
Optional opaque state for CSRF protection. |
Value
The authorization URL to open in a browser.
Examples
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
Description
Authorization header value for a token
Usage
oauth_bearer(token)
Arguments
token |
A |
Value
A string like "Bearer abc123" for use as an HTTP
Authorization header.
Examples
tok <- structure(list(access_token = "abc123"), class = "tinyoauth_token")
oauth_bearer(tok)
# pass it to a request, e.g.:
# curl::handle_setheaders(curl::new_handle(),
# Authorization = oauth_bearer(tok))
Default on-disk cache path for a client's token
Description
Default on-disk cache path for a client's token
Usage
oauth_cache_path(client)
Arguments
client |
A [oauth_client]. |
Value
Path to the token cache file under tools::R_user_dir.
Examples
client <- oauth_client("my_app", token_url = "https://example.com/token")
oauth_cache_path(client)
Define an OAuth 2.0 client
Description
Define an OAuth 2.0 client
Usage
oauth_client(id, secret = NULL, token_url, auth_url = NULL,
redirect_uri = "http://127.0.0.1:1410/")
Arguments
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 ( |
Value
A tinyoauth_client object.
Examples
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
Description
Exchange an authorization code for a token
Usage
oauth_exchange_code(client, code)
Arguments
client |
A [oauth_client]. |
code |
The authorization code from the redirect. |
Value
A tinyoauth_token.
Examples
## Not run:
# `code` is the value the provider redirects back after the user approves.
tok <- oauth_exchange_code(client, code)
## End(Not run)
Is a token expired?
Description
Is a token expired?
Usage
oauth_expired(token, leeway = 60)
Arguments
token |
A |
leeway |
Seconds of slack before the hard expiry (default 60). |
Value
TRUE if expired (or within leeway of it); FALSE
when there is no expiry recorded.
Examples
expired <- structure(list(expires_at = Sys.time() - 1),
class = "tinyoauth_token")
oauth_expired(expired)
fresh <- structure(list(expires_at = Sys.time() + 3600),
class = "tinyoauth_token")
oauth_expired(fresh)
Import an httr '.httr-oauth' cache into tinyoauth
Description
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.
Usage
oauth_import_httr(path = ".httr-oauth", which = 1L)
Arguments
path |
Path to the httr cache (default |
which |
Which cached token to import when the file holds several (1-based; default 1). |
Details
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.
Value
A list with client (a [oauth_client]) and token (a
tinyoauth_token).
Examples
## Not run:
imported <- oauth_import_httr("~/project/.httr-oauth")
token <- oauth_refresh(imported$client, imported$token)
## End(Not run)
Decode a JWT payload
Description
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).
Usage
oauth_jwt_payload(x)
Arguments
x |
A JWT string, or a |
Value
The decoded payload as a named list, or NULL if x has
no usable JWT.
Examples
# 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
Description
Refresh an access token
Usage
oauth_refresh(client, token)
Arguments
client |
A [oauth_client]. |
token |
A |
Value
A refreshed tinyoauth_token. Providers that omit a new refresh
token on refresh keep the existing one.
Examples
## Not run:
tok <- oauth_refresh(spotify, tok)
## End(Not run)
Make an authenticated request
Description
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.
Usage
oauth_request(token, url, method = "GET", query = NULL, body = NULL,
headers = NULL, flatten = FALSE, retries = 3L)
Arguments
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). |
Value
Parsed JSON, or invisibly NULL for an empty response body.
Non-2xx responses raise an error carrying the status and body.
Examples
## Not run:
oauth_request(tok, "https://api.spotify.com/v1/me")
## End(Not run)
Get a valid token, using the cache and refreshing as needed
Description
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.
Usage
oauth_token(client, scope = NULL, cache = oauth_cache_path(client), ...)
Arguments
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. |
Value
A valid tinyoauth_token.
Examples
## Not run:
tok <- oauth_token(spotify, scope = "user-read-email")
## End(Not run)
Run the authorization-code flow end to end
Description
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.
Usage
oauth_token_authcode(client, scope = NULL, port = 1410L,
open_browser = interactive(), timeout = 120, manual = NA)
Arguments
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 ( |
Value
A tinyoauth_token (with a refresh token, when the provider
issues one).
Examples
## Not run:
tok <- oauth_token_authcode(spotify, scope = "user-read-email")
tok <- oauth_token_authcode(google, manual = TRUE) # force manual paste
## End(Not run)
Fetch a token via the client-credentials grant
Description
App-only access (no user context).
Usage
oauth_token_client(client)
Arguments
client |
A [oauth_client]. |
Value
A tinyoauth_token.
Examples
## Not run:
tok <- oauth_token_client(spotify)
## End(Not run)
Get a valid OpenAI Codex token, using the cache and refreshing as needed
Description
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.
Usage
oauth_token_openai_codex(cache = oauth_cache_path(openai_codex_client()),
open_url = interactive(), timeout = 600, login = TRUE)
Arguments
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 |
Value
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.
Examples
## 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)
Extract the ChatGPT account id from a Codex token
Description
Reads the chatgpt_account_id claim that OpenAI nests under
https://api.openai.com/auth in the access-token JWT.
Usage
openai_codex_account_id(token)
Arguments
token |
A |
Value
The account id string, or NULL if absent.
Examples
# A token whose access-token JWT carries the account-id claim:
claim <- jsonlite::toJSON(
list("https://api.openai.com/auth" = list(chatgpt_account_id = "acct_123")),
auto_unbox = TRUE)
b64url <- function(s) {
chartr("+/", "-_", gsub("[\n=]", "", jsonlite::base64_enc(charToRaw(s))))
}
jwt <- paste("header", b64url(claim), "signature", sep = ".")
openai_codex_account_id(jwt)
OAuth client for the OpenAI Codex (ChatGPT) device-login flow
Description
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.
Usage
openai_codex_client()
Value
A tinyoauth_client with extra device_usercode_url,
device_token_url, and verification_uri fields.
Examples
openai_codex_client()