oauth2c is a flag-driven OAuth 2.0 / OpenID Connect client for testing
OAuth apps. There are no interactive prompts: every grant is configured
from flags. LLM agents and scripts can drive a real authorization server,
read token JSON on stdout, and read a JSON-lines protocol trace on
stderr: what was sent, what came back, which checks passed or failed,
and what the tokens contain. If you want YAML, install
yq (brew install yq) and pipe
stderr through yq -p json -o yaml.
Printing raw tokens, client secrets, JWTs, and request parameters is intentional so a model (or a human) can diagnose the grant. This is not a production client. Do not point it at credentials you cannot afford to leak into a log.
--silent discards the protocol trace so callers get only the token
JSON on stdout. error events still appear if the command fails.
Browser grants (authorization code, hybrid, implicit, device) still need a
user at the authorization server. --no-browser only skips opening the
URL; the process still waits for the callback or device approval. Client
credentials, password, refresh token, JWT bearer, and token exchange do
not wait on a user.
Agents: install the oauth2c skill with gh skill
(see Installation).
This is a fork of cloudentity/oauth2c. Cloudentity built the original; this repo is a near-total rewrite.
cloudentity/oauth2c is a user-friendly client for fetching access tokens (interactive prompts, TUI, Homebrew). This fork drops the TUI so an LLM or script can drive every grant from flags and parse a JSON-lines trace of the protocol. Demos use an Auth0 tenant you control.
Grant types and client authentication methods are the same. The Go module and
version line are new (github.com/sukujgrg/oauth2c v1.0.0), not a
continuation of upstream v1.20.x.
- support for authorization code, hybrid, implicit, password, client credentials, refresh token, JWT bearer, token exchange, device grant flows
- support for client secret basic, client secret post, client secret JWT, private key JWT, TLS client auth client authentication methods
- passing request parameters as plaintext, signed, and/or encrypted JWT
- support for Proof Key for Code Exchange (PKCE)
- support for OpenID Connect nonce
- support for JWT Secured Authorization Response Mode (JARM)
- support for Pushed Authorization Requests (PAR)
- support for Demonstration of Proof of Possession (DPoP)
- support for Rich Authorization Requests (RAR)
Go is not required. The installer picks the binary for your machine.
macOS / Linux:
OAUTH2C_VERSION=v2.1.0 curl -fsSL https://raw.githubusercontent.com/sukujgrg/oauth2c/v2.1.0/scripts/install.sh | shWindows (PowerShell):
$env:OAUTH2C_VERSION = "v2.1.0"
irm https://raw.githubusercontent.com/sukujgrg/oauth2c/v2.1.0/scripts/install.ps1 | iexOmit OAUTH2C_VERSION to install the latest release. Change the install
directory with OAUTH2C_BINDIR.
Or download a binary from the releases page.
From source (needs Go):
git clone https://github.com/sukujgrg/oauth2c.git
cd oauth2c
make installmake sets GOEXPERIMENT=jsonv2 so the experimental encoding/json/v2 package
is available, and stamps oauth2c version from the current git tag.
GOEXPERIMENT=jsonv2 go install github.com/sukujgrg/oauth2c@v2.1.0Agent skill (GitHub CLI v2.90+):
gh skill install sukujgrg/oauth2c oauth2c@v2.1.0Add --agent cursor (or claude-code, codex, …) if you are not using
GitHub Copilot. --scope user installs it for every repo.
Prerequisite: a free Auth0 tenant. Create one at manage.auth0.com (no paid plan is required for the core examples below).
Then provision the demo apps with the
Auth0 CLI
(brew install auth0 or the installer for your OS):
auth0 login
auth0 tenants use <your-tenant>.us.auth0.com # or .eu.auth0.com / .au.auth0.com
./scripts/setup-auth0.sh
set -a && source .env.auth0 && set +ascripts/setup-auth0.sh is idempotent. It creates (or updates) an API, a
confidential web app, an M2M app, a public SPA, a native device app, and a
database user (oauth2c-demo@example.com by default, override with --email),
then writes client IDs, secrets, and that user's password to .env.auth0
(gitignored). Browser login and the password grant use $OAUTH2C_USERNAME /
$OAUTH2C_PASSWORD.
Pass the issuer without a trailing slash. oauth2c appends
/.well-known/openid-configuration to the value you give it.
A free Auth0 tenant covers the core grants in this README. Flows that need another authorization server are in Advanced profiles.
oauth2c [issuer url] [flags]--grant-type is required. The available flags are:
--acr-values strings ACR values
--actor-token string acting party token
--actor-token-type string acting party token type
--assertion string claims for jwt bearer assertion
--audience strings requested audience (access-token aud; not RFC 8707 resource)
--auth-method string token endpoint authentication method
--authentication-code string authentication code used for passwordless authentication
--authorization-endpoint string server's authorization endpoint
--browser-timeout duration browser timeout (default 10m0s)
--callback-addr string local callback bind address, e.g. 0.0.0.0:8080 (not redirect_uri)
--callback-tls-cert string path to callback tls cert pem file
--callback-tls-key string path to callback tls key pem file
--claims string use claims
--client-id string client identifier
--client-secret string client secret
--device-authorization-endpoint string server's device authorization endpoint
--dpop use DPoP
--encrypted-request-object pass request parameters as encrypted jwt
--encryption-key string path or url to encryption key in jwks format
--grant-type string grant type
-h, --help help for oauth2c
--http-timeout duration http client timeout (default 1m0s)
--id-token-hint string id token hint
--idp-hint string identity provider hint
--insecure allow insecure connections
--login-hint string user identifier hint
--max-age string maximum authentication age in seconds
--mtls-pushed-authorization-request-endpoint string server's mtls pushed authorization request endpoint
--mtls-token-endpoint string server's mtls token endpoint
--no-browser do not open a browser; still waits for the callback or device approval
--no-origin do not include an Origin header
--nonce string openid connect nonce
--par enable pushed authorization requests (PAR)
--password string resource owner password credentials grant flow password
--pkce enable proof key for code exchange (PKCE)
--prompt strings OpenID Connect prompt (none, login, consent, select_account)
--purpose string string describing the purpose for obtaining End-User authorization
--pushed-authorization-request-endpoint string server's pushed authorization request endpoint
--rar string use rich authorization request (RAR)
--redirect-uri string alias of --redirect-url (default "http://localhost:9876/callback")
--redirect-url string OAuth redirect_uri sent to the authorization server (alias --redirect-uri) (default "http://localhost:9876/callback")
--refresh-token string refresh token
--request-object pass request parameters as jwt
--resource strings requested resource
--response-mode string response mode
--response-types strings response type
--scopes strings requested scopes
--signing-key string path or url to signing key in jwks format
-s, --silent print only the token JSON on stdout
--subject-token string third party token
--subject-token-type string third party token type
--tls-cert string path to tls cert pem file
--tls-key string path to tls key pem file
--tls-root-ca string path to tls root ca pem file
--token-endpoint string server's token endpoint
--username string resource owner password credentials grant flow usernameoauth2c opens a browser for flows such as authorization code and starts an
HTTP server which acts as a client application and waits for a callback.
Note: To make browser flows work add
http://localhost:9876/callbackas a redirect URL to your client.
stderr is JSON lines (one object per event): the protocol trace
(requests with nested response.status plus body, decoded tokens,
checks) and CLI failures (unknown flags, missing args, interrupt).
error events are objects (error, optional status,
error_description). --help stays human text on stdout. Use
--silent when you only want the token JSON on stdout; error events
still appear if the command fails. If you want YAML, install
yq (brew install yq) and pipe
stderr through yq -p json -o yaml.
Load .env.auth0 (written by ./scripts/setup-auth0.sh):
set -a && source .env.auth0 && set +aAuth0 access tokens for the demo API need --audience "$OAUTH2C_AUDIENCE".
NOTE: The authorization code, implicit, hybrid and device grant flows require browser and user authentication.
The client receives an authorization code in the browser callback and exchanges it for tokens. Typical for confidential server-side apps.
oauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_WEB_CLIENT_ID" \
--client-secret "$OAUTH2C_WEB_CLIENT_SECRET" \
--response-types code \
--response-mode query \
--grant-type authorization_code \
--auth-method client_secret_basic \
--audience "$OAUTH2C_AUDIENCE" \
--scopes openid,email,offline_accessLearn more about authorization code flow
Tokens are returned to the browser without a code exchange. Prefer authorization code with PKCE for new apps.
oauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_SPA_CLIENT_ID" \
--response-types token \
--response-mode form_post \
--grant-type implicit \
--audience "$OAUTH2C_AUDIENCE" \
--scopes openid,emailLearn more about implicit flow
The authorization response includes both a code and an ID token
(response_types code,id_token).
oauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_WEB_CLIENT_ID" \
--client-secret "$OAUTH2C_WEB_CLIENT_SECRET" \
--response-types code,id_token \
--response-mode form_post \
--grant-type authorization_code \
--auth-method client_secret_basic \
--audience "$OAUTH2C_AUDIENCE" \
--scopes openid,email,offline_accessLearn more about the hybrid flow
The client authenticates as itself and receives an access token. Used for server-to-server calls.
oauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_WEB_CLIENT_ID" \
--client-secret "$OAUTH2C_WEB_CLIENT_SECRET" \
--grant-type client_credentials \
--auth-method client_secret_basic \
--audience "$OAUTH2C_AUDIENCE" \
--scopes "$OAUTH2C_SCOPE"Learn more about the client credentials flow
Exchange a refresh token for a new access token.
oauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_WEB_CLIENT_ID" \
--client-secret "$OAUTH2C_WEB_CLIENT_SECRET" \
--grant-type refresh_token \
--auth-method client_secret_basic \
--refresh-token "$REFRESH_TOKEN"Note In order to use this command, you must first set the REFRESH_TOKEN environment variable
Show example
export REFRESH_TOKEN=`oauth2c "$OAUTH2C_ISSUER" \ --client-id "$OAUTH2C_WEB_CLIENT_ID" \ --client-secret "$OAUTH2C_WEB_CLIENT_SECRET" \ --response-types code \ --response-mode query \ --grant-type authorization_code \ --auth-method client_secret_basic \ --audience "$OAUTH2C_AUDIENCE" \ --scopes openid,email,offline_access \ --silent | jq -r .refresh_token`
Learn more about the refresh token flow
The client sends the resource owner's username and password to the token endpoint.
oauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_WEB_CLIENT_ID" \
--client-secret "$OAUTH2C_WEB_CLIENT_SECRET" \
--grant-type password \
--username "$OAUTH2C_USERNAME" \
--password "$OAUTH2C_PASSWORD" \
--auth-method client_secret_basic \
--audience "$OAUTH2C_AUDIENCE" \
--scopes openidLearn more about the password flow
The device authorization grant is for clients that cannot host a browser, such as a TV or CLI. oauth2c asks the authorization server for a device code, prints a verification URL and user code, then polls the token endpoint until the user approves the request in a browser on another device.
oauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_DEVICE_CLIENT_ID" \
--grant-type urn:ietf:params:oauth:grant-type:device_code \
--auth-method none \
--audience "$OAUTH2C_AUDIENCE" \
--scopes openid,email,offline_accessLearn more about the device flow
The client authenticates with an HTTP Basic Authorization header.
oauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_WEB_CLIENT_ID" \
--client-secret "$OAUTH2C_WEB_CLIENT_SECRET" \
--grant-type client_credentials \
--auth-method client_secret_basic \
--audience "$OAUTH2C_AUDIENCE" \
--scopes "$OAUTH2C_SCOPE"Learn more about client secret basic
The client sends client_id and client_secret in the token request body.
oauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_POST_CLIENT_ID" \
--client-secret "$OAUTH2C_POST_CLIENT_SECRET" \
--grant-type client_credentials \
--auth-method client_secret_post \
--audience "$OAUTH2C_AUDIENCE" \
--scopes "$OAUTH2C_SCOPE"Learn more about client secret post
Public clients have no secret. PKCE binds the authorization code to the client that started the request.
oauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_SPA_CLIENT_ID" \
--response-types code \
--response-mode query \
--grant-type authorization_code \
--auth-method none \
--audience "$OAUTH2C_AUDIENCE" \
--scopes openid,email \
--pkceLearn more about authorization code flow with PKCE
OpenID Connect nonce associates a client session with an ID Token and
mitigates replay attacks. Authorization requests still send a generated nonce
by default. When --nonce is set, oauth2c uses that value instead. If an ID
Token is returned, oauth2c verifies its signature and iss, aud, exp, and
iat claims. A missing nonce claim is logged as a failed check but the
process still succeeds; a mismatched nonce exits 1.
oauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_SPA_CLIENT_ID" \
--grant-type authorization_code \
--auth-method none \
--audience "$OAUTH2C_AUDIENCE" \
--scopes openid \
--pkce \
--nonce n-0S6_WzA2Mjoauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_SPA_CLIENT_ID" \
--grant-type implicit \
--response-types id_token \
--response-mode form_post \
--audience "$OAUTH2C_AUDIENCE" \
--scopes openid \
--nonce n-0S6_WzA2MjYou can use --callback-tls-cert and --callback-tls-key flags to specify a
TLS certificate and key for the HTTPS callback redirect URL.
oauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_WEB_CLIENT_ID" \
--client-secret "$OAUTH2C_WEB_CLIENT_SECRET" \
--response-types code \
--response-mode query \
--grant-type authorization_code \
--auth-method client_secret_basic \
--audience "$OAUTH2C_AUDIENCE" \
--scopes openid,email,offline_access \
--redirect-url https://localhost:9876/callback \
--callback-tls-cert https://raw.githubusercontent.com/sukujgrg/oauth2c/master/data/cert.pem \
--callback-tls-key https://raw.githubusercontent.com/sukujgrg/oauth2c/master/data/key.pemWhen running behind a TLS-terminating proxy (e.g., nginx, Traefik, or a cloud
load balancer), use --callback-addr to specify the local bind address while
keeping the public HTTPS URL in --redirect-url.
scripts/setup-auth0.sh only registers the localhost callbacks. Add the public
URL to the Auth0 application's Allowed Callback URLs before running this.
oauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_WEB_CLIENT_ID" \
--client-secret "$OAUTH2C_WEB_CLIENT_SECRET" \
--response-types code \
--response-mode query \
--grant-type authorization_code \
--auth-method client_secret_basic \
--audience "$OAUTH2C_AUDIENCE" \
--scopes openid,email,offline_access \
--redirect-url https://example.com/callback \
--callback-addr 0.0.0.0:8080In this configuration:
--redirect-urlis the public URL sent to the IdP in the OAuth request--callback-addris the local address where the callback server binds- The callback server serves HTTP (no TLS certs provided), while the proxy handles TLS termination
If discovery is unavailable, set the endpoints with flags.
oauth2c "$OAUTH2C_ISSUER" \
--client-id "$OAUTH2C_WEB_CLIENT_ID" \
--client-secret "$OAUTH2C_WEB_CLIENT_SECRET" \
--response-types code \
--response-mode query \
--grant-type authorization_code \
--auth-method client_secret_basic \
--audience "$OAUTH2C_AUDIENCE" \
--scopes openid,email,offline_access \
--token-endpoint "$OAUTH2C_ISSUER/oauth/token" \
--authorization-endpoint "$OAUTH2C_ISSUER/authorize"These flows are implemented by oauth2c but are not part of the Auth0 demo. Use an authorization server that supports the relevant spec.
Replace $AS_ISSUER / $AS_CLIENT_ID / $AS_CLIENT_SECRET with values from
that server.
oauth2c "$AS_ISSUER" \
--client-id "$AS_CLIENT_ID" \
--client-secret "$AS_CLIENT_SECRET" \
--grant-type urn:ietf:params:oauth:grant-type:jwt-bearer \
--auth-method client_secret_basic \
--scopes email \
--signing-key https://raw.githubusercontent.com/sukujgrg/oauth2c/master/data/rsa/key.json \
--assertion '{"sub":"jdoe@example.com"}'oauth2c "$AS_ISSUER" \
--client-id "$AS_CLIENT_ID" \
--client-secret "$AS_CLIENT_SECRET" \
--grant-type urn:ietf:params:oauth:grant-type:token-exchange \
--auth-method client_secret_basic \
--scopes email \
--subject-token "$SUBJECT_TOKEN" \
--subject-token-type urn:ietf:params:oauth:token-type:access_token \
--actor-token "$ACTOR_TOKEN" \
--actor-token-type urn:ietf:params:oauth:token-type:access_tokenoauth2c "$AS_ISSUER" \
--client-id "$AS_CLIENT_ID" \
--client-secret "$AS_CLIENT_SECRET" \
--grant-type client_credentials \
--auth-method client_secret_jwt \
--scopes emailoauth2c "$AS_ISSUER" \
--client-id "$AS_CLIENT_ID" \
--signing-key https://raw.githubusercontent.com/sukujgrg/oauth2c/master/data/rsa/key.json \
--grant-type client_credentials \
--auth-method private_key_jwtoauth2c "$AS_ISSUER" \
--client-id "$AS_CLIENT_ID" \
--tls-cert https://raw.githubusercontent.com/sukujgrg/oauth2c/master/data/cert.pem \
--tls-key https://raw.githubusercontent.com/sukujgrg/oauth2c/master/data/key.pem \
--grant-type client_credentials \
--auth-method tls_client_authoauth2c "$AS_ISSUER" \
--client-id "$AS_CLIENT_ID" \
--client-secret "$AS_CLIENT_SECRET" \
--response-types code \
--response-mode query \
--grant-type authorization_code \
--auth-method client_secret_basic \
--scopes openid,email,offline_access \
--request-objectoauth2c "$AS_ISSUER" \
--client-id "$AS_CLIENT_ID" \
--client-secret "$AS_CLIENT_SECRET" \
--response-types code \
--response-mode query \
--grant-type authorization_code \
--auth-method client_secret_basic \
--scopes openid,offline_access \
--claims '{"id_token":{"email": {"essential": true}}}'oauth2c "$AS_ISSUER" \
--client-id "$AS_CLIENT_ID" \
--client-secret "$AS_CLIENT_SECRET" \
--response-types code \
--response-mode query.jwt \
--grant-type authorization_code \
--auth-method client_secret_basic \
--scopes openid,email,offline_accessoauth2c "$AS_ISSUER" \
--client-id "$AS_CLIENT_ID" \
--client-secret "$AS_CLIENT_SECRET" \
--response-types code \
--response-mode query.jwt \
--grant-type authorization_code \
--auth-method client_secret_post \
--scopes openid,email,offline_access \
--encryption-key https://raw.githubusercontent.com/sukujgrg/oauth2c/master/data/rsa/key.jsonoauth2c "$AS_ISSUER" \
--client-id "$AS_CLIENT_ID" \
--client-secret "$AS_CLIENT_SECRET" \
--response-types code \
--response-mode query \
--grant-type authorization_code \
--auth-method client_secret_basic \
--scopes openid,email,offline_access \
--paroauth2c "$AS_ISSUER" \
--client-id "$AS_CLIENT_ID" \
--client-secret "$AS_CLIENT_SECRET" \
--response-types code \
--response-mode query \
--grant-type authorization_code \
--auth-method client_secret_basic \
--scopes openid,email,offline_access \
--signing-key https://raw.githubusercontent.com/sukujgrg/oauth2c/master/data/ps/key.json \
--dpopoauth2c "$AS_ISSUER" \
--client-id "$AS_CLIENT_ID" \
--client-secret "$AS_CLIENT_SECRET" \
--response-types code \
--response-mode query \
--grant-type authorization_code \
--auth-method client_secret_basic \
--rar '[{"type":"payment_initiation","locations":["https://example.com/payments"],"instructedAmount":{"currency":"EUR","amount":"123.50"},"creditorName":"Merchant A","creditorAccount":{"bic":"ABCIDEFFXXX","iban":"DE02100100109307118603"},"remittanceInformationUnstructured":"Ref Number Merchant"}]'oauth2c is released under the
Apache v2.0.
We welcome contributions! If you have an idea for a new feature or have found a bug, please open an issue on GitHub.
go test ./cmd runs the non-browser grants against .env.auth0 when that file
is present, and skips them otherwise.