sondahub

sondahub / OAuth 2.0 test server

OAuth 2.0 and OpenID Connect test server

A small, real authorization server for trying a client end to end: the authorization-code flow with PKCE and a consent screen, client credentials, password and refresh grants, dynamic client registration, resource indicators, RS256 tokens you can verify against the JWKS, discovery, userinfo and introspection — and a whole company directory to sign in as. Free, public credentials.

Settings to paste

Discovery
https://api.sondahub.com/.well-known/openid-configuration
Authorization
https://api.sondahub.com/v1/utils/oauth/authorize
Token
https://api.sondahub.com/v1/utils/oauth/token
Userinfo
https://api.sondahub.com/v1/utils/oauth/userinfo
Introspection
https://api.sondahub.com/v1/utils/oauth/introspect
Revocation
https://api.sondahub.com/v1/utils/oauth/revoke
JWKS
https://api.sondahub.com/.well-known/jwks.json
Registration
https://api.sondahub.com/v1/utils/oauth/register
Client
sonda / probe-secret, or register one
User
sonda / probe, or any directory user / probe
Scopes
any; openid adds an id_token; read when none is asked

Public on purpose — this is a playground, and the credentials protect nothing.

Discovery document
curl https://api.sondahub.com/.well-known/openid-configuration

Authorization code with PKCE

1. Send the browser to the authorization URL with a code_challenge. The consent screen asks Allow or Deny (add &auto=1 to skip it and allow). The answer is a redirect to your redirect_uri with code and your state.

https://api.sondahub.com/v1/utils/oauth/authorize?response_type=code&client_id=sonda&redirect_uri=http://127.0.0.1:8080/callback&scope=openid%20read&state=xyz&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256

2. Exchange the code with the verifier the challenge was made from:

Token request
curl https://api.sondahub.com/v1/utils/oauth/token \
  -u sonda:probe-secret \
  -d grant_type=authorization_code \
  -d code=THE_CODE \
  -d redirect_uri=http://127.0.0.1:8080/callback \
  -d code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

The answer: access_token, refresh_token, expires_in, scope, and an id_token when the scope has openid. A wrong verifier answers invalid_grant saying it does not match the challenge.

In Sonda: OAuth 2 in the auth editor, authorization-code grant, the two URLs and the client above, PKCE on — Sonda opens the consent screen and catches the code on its own 127.0.0.1 redirect.

Client credentials and password

Client credentials
curl -u sonda:probe-secret -d grant_type=client_credentials -d scope=read https://api.sondahub.com/v1/utils/oauth/token
Password grant with an id_token
curl -d grant_type=password -d username=sonda -d password=probe \
  -d client_id=sonda -d client_secret=probe-secret -d "scope=openid read" \
  https://api.sondahub.com/v1/utils/oauth/token
Refresh
curl -u sonda:probe-secret -d grant_type=refresh_token -d refresh_token=THE_REFRESH_TOKEN https://api.sondahub.com/v1/utils/oauth/token

Register a client, ask for an audience

Dynamic Client Registration hands out working clients without storing them — the client_id is the signed registration. A resource parameter (RFC 8707) on the authorization and token requests becomes the token's aud, which is how the OAuth-protected MCP server knows a token is meant for it.

Register a public client
curl -X POST https://api.sondahub.com/v1/utils/oauth/register \
  -H "Content-Type: application/json" \
  -d '{"client_name": "My CLI", "redirect_uris": ["http://127.0.0.1:8080/callback"], "token_endpoint_auth_method": "none"}'
A token for a directory person, for one resource
curl -d grant_type=password -d username=clara.brown -d password=probe \
  -d client_id=sonda -d client_secret=probe-secret \
  -d "scope=openid groups mcp:read" -d resource=https://api.sondahub.com/mcp/secure \
  https://api.sondahub.com/v1/utils/oauth/token

Use the token

GET/v1/utils/oauth/protectedNeeds Authorization: Bearer <access_token>. ?scope=write demands that scope and answers 403 insufficient_scope without it.
GET/v1/utils/oauth/userinfoThe OpenID Connect userinfo for the token.
POST/v1/utils/oauth/introspecttoken=… → active and the claims.
Call the protected resource
curl -H "Authorization: Bearer ACCESS_TOKEN" https://api.sondahub.com/v1/utils/oauth/protected

Questions

Which grant types does it support?

authorization_code (with or without PKCE, S256 or plain), client_credentials, password and refresh_token. The client may authenticate with HTTP Basic or with client_id and client_secret in the form.

Which redirect URIs are allowed?

Any absolute URL — a loopback address like http://127.0.0.1:8080/callback for a desktop or CLI app, or your own development URL. The code remembers the redirect URI it was issued for: a token request that names a different one is refused with invalid_grant.

How do I verify the tokens?

Access and ID tokens are RS256 JWTs: fetch the key from https://api.sondahub.com/.well-known/jwks.json (kid sondahub-2026), check iss = https://api.sondahub.com and aud = sondahub. Or POST the token to the introspection endpoint.

Can I register my own client?

Yes, with Dynamic Client Registration (RFC 7591): POST your client metadata to /v1/utils/oauth/register and get a client_id — and a secret, unless you register a public client with token_endpoint_auth_method: none. Nothing is stored: the client_id carries its own signed registration, so it works forever and anywhere the hub runs. Redirect URIs you registered are then enforced.

Can I sign in as someone other than the playground user?

Anyone in the Identity directory: their user_name (clara.brown, say) with the password probe, on the consent screen or in the password grant. Their id token and userinfo carry name, email, department, title and groups; sub is user_{id}, and the SCIM /Me endpoint answers for them.

How long do tokens live?

Authorization codes ten minutes, access tokens an hour, refresh tokens thirty days. Revocation answers 200 but cannot shorten a token’s life: the server keeps no state, so a token lives until it expires.