Skip to content

OAuth2 Health Check

By Romi Fadli, S.Kom. 11 min read Security & Secrets
On this page
A checklist card on the left, a three-part token in the middle with a check mark, and an identity provider marked with a key on the right

OAuth2 Health Check is an interactive Python script that proves an identity provider is configured correctly for a client before any application depends on it. It performs a complete sign-in exactly as an application would, receives the tokens, and checks them the way an application should, so that by the time I write the application's own login code, the provider is no longer a suspect.

OAuth 2.0 is the standard by which an application obtains access on a user's behalf without seeing their password, and OpenID Connect (OIDC) adds a signed statement of who the user is. PKCE is the extension that binds the final step of a sign-in to the client that started it, and JWKS is the published set of public keys a provider signs its tokens with. A failed login in a half-built service can be the provider's configuration or the service's code; this tool removes the first possibility before the second exists.

At a glance

FormAn interactive command-line script, oauth2.py
LanguagePython 3, with requests, PyJWT, and InquirerPy
StatusStable, in use
Source codePrivate
What the tool is, what it is built with, and its state.

What it does

A. Rehearses a complete sign-in

It builds the authorization address the way an application would — Authorization Code flow with PKCE, a fresh state and nonce, and the client's registered redirect address, with spaces encoded as %20 rather than + — and prints the verifier, challenge, state, and nonce with a reminder to keep the verifier, so any step can be repeated by hand. Then it waits while I sign in in a browser. When I paste back the address the provider redirected to, it checks that the issuer is present and is the one it expects, that the state matches what it sent, and that a code came back, and it shows the session state the provider attached. Then it exchanges the code for tokens; when the exchange is refused, it prints the provider's status and error body exactly as they arrived.

B. Validates the tokens it receives

Each token that is a JWT is verified against the provider's published keys, which are fetched once and used for both tokens. The signature and the expiry are always verified, and an expired token gets its own message. The issuer and the audience are verified when they match what the configuration expects; when they do not, that check is skipped with a yellow warning instead of failing, because providers such as Google legitimately differ there — so a warning, not a pass, is the honest verdict. A token missing from the response is reported as missing. It prints the decoded claims next to the verdict, with the expiry in local time, so a failure points straight at the claim that caused it.

C. Checks opaque tokens the way a resource server would

Not every provider issues access tokens that can be decoded. For those, it asks the provider's token-information endpoint whether the token is active and for whom, and then calls the user-information endpoint to prove the token actually grants access — the same two questions a resource server would have to ask. It does this through Google-style token-information and user-information endpoints, because Google offers no public RFC 7662 introspection; a provider that has neither endpoint configured gets its opaque token flagged as one that cannot be validated, never passed silently.

D. Signs out as well as in

A sign-in that works is half of the integration. The tool also performs an RP-Initiated Logout as a separate run: I paste the ID token from the sign-in as the hint, it builds the provider's logout address with that hint and a fresh state, and it validates the address the provider returns to after signing out. On that return the state is required, while a missing issuer is accepted with a note, because Keycloak omits it there.

E. Many providers from one file

One configuration file can hold several providers, each with its endpoints, redirect addresses, clients, and scopes. The tool lets me pick the provider, the client, the flow, the scopes, and the mode from menus, so the same file serves a self-hosted Keycloak and Google alike. A missing file, invalid JSON, or a provider without clients is reported in one plain sentence instead of a traceback, every scope starts ticked, and an empty selection stops the run.

How I use it

There is one command; everything else is chosen interactively.

bash
python oauth2.py --config=my-app.config.json
StageWhat happens
1. Chooseprovider, client, flow (login or logout), scopes, and mode
2. CallbackI sign in in a browser and paste back the redirect address; state and issuer are checked
3. Token exchangethe code and the PKCE verifier are exchanged for tokens
4. Validationeach token is verified and its claims are shown
The four stages of a login run.

Two modes decide where a login run stops. The full mode goes through all four stages. The body-only mode stops after the callback and prints the request body an application would send to finish the sign-in itself, so the exchange can be tested against my own backend instead. The body carries everything my backend needs to finish the exchange, and the code itself is left unspent, because a code can be exchanged only once and my backend must be the one to use it.

The configuration describes each provider once. Client secrets live only in the local file, which never leaves the machine:

json
{
  "issuers": [
    {
      "iss": "https://id.example.com/realms/main",
      "endpoints": {
        "auth": "…/protocol/openid-connect/auth",
        "token": "…/protocol/openid-connect/token",
        "jwks": "…/protocol/openid-connect/certs",
        "logout": "…/protocol/openid-connect/logout"
      },
      "redirect_uri": {
        "login": "https://app.example.com/callback",
        "logout": "https://app.example.com/signed-out"
      },
      "clients": [{ "client_id": "my-app", "client_secret": "YOUR-CLIENT-SECRET" }],
      "scopes": ["openid", "email", "profile"]
    }
  ]
}

These are two real runs against a Keycloak realm, a sign-in and then a sign-out, shortened, with every address, identifier, and token replaced. The tool reports in Indonesian:

bash
$ python oauth2.py --config=my-app.config.json
? Pilih Issuer yang akan digunakan: [1] https://id.example.com/realms/main
? Pilih Client: [1] my-app
? Pilih flow: [1] Login (Authorization Code + PKCE)
? Pilih mode: [1] Proses PKCE lengkap (exchange code → token)

Paste redirect URL callback: https://app.example.com/callback?state=<state>&iss=…&code=<code>
[✓] Validasi berhasil
[✓] Token Exchange berhasil

── access_token ──
[✓] Signature valid  │  Expire: 2026-10-02 12:11:24 WIB
  "iss": "https://id.example.com/realms/main",
  "aud": ["my-app", "account"],
  "azp": "my-app",
── id_token ──
[✓] Signature valid  │  Expire: 2026-10-02 12:11:24 WIB

$ python oauth2.py --config=my-app.config.json
? Pilih flow: [2] Logout (RP-Initiated Logout)
[~] 'iss' tidak disertakan pada callback logout (wajar untuk Keycloak)
[✓] Logout callback tervalidasi (state cocok)

Design decisions

It rehearses the whole sign-in, so the provider is proven before the application exists. Checking that a provider's discovery address answers proves almost nothing. I made the tool walk the same path an application walks — PKCE with a verifier of the length the standard requires and the S256 challenge method, a fresh state, the registered redirect address, a real browser sign-in, and a real token exchange. When it passes, every failure that comes later belongs to the application, which halves the search before it starts.

It checks what an application must check, and says why when something is off. Signatures are checked against the provider's published keys, picking the key named in the token, skipping keys meant for encryption, and falling back to the first usable signing key when the token names none or none matches. Only RS and ES algorithms of 256, 384, and 512 bits are accepted — never none and never a shared-secret HS algorithm — which closes the algorithm-confusion attack in which a token signed with the public key as an HMAC secret would otherwise verify. The keys are fetched with an explicit User-Agent, because some providers sit behind a firewall that refuses the default one of a Python library. When the client is not in the token's audience but is its authorized party, the tool accepts it and says so, instead of failing silently or passing silently — the distinction that decides whether an application's own check will pass.

It treats providers as data, so a new one costs a few lines of configuration. Providers differ in small, irritating ways: one issues decodable access tokens, another opaque ones; one names its keys, another does not; one returns the issuer on every callback, another omits it on logout. Rather than branching on provider names, the tool reads what each provider declares and adapts — accepting a logout return without an issuer, and using token-information and user-information endpoints when a token cannot be decoded. Guides for the provider side, such as adding the client to a Keycloak token's audience or a 2048-bit signing key, live next to the script.

Secrets stay on the machine, and the rule covers every name a secret file can have. Every configuration file that can hold a secret is ignored by version control; only an example with an empty secret is committed. Tokens are printed to the terminal only and never written to disk.

It stops where the next component begins. A full run tests the provider; a body-only run hands the finished callback to my own backend; a logout run closes the session it opened. Each mode ends exactly where another part of the system takes over, so a failure is always traced to one side of a boundary.

Specifications

InputsA JSON configuration of providers: issuer, endpoints, redirect addresses, clients, and scopes; the redirect address pasted back after signing in
OutputsAuthorization and logout addresses, callback verdicts, tokens and their decoded claims, a verdict per token, or a request body in body-only mode; the verdict is shown in the output
FlowsAuthorization Code with PKCE; RP-Initiated Logout
StandardsOAuth 2.0 (RFC 6749), PKCE (RFC 7636), JWT (RFC 7519), JWK and JWKS (RFC 7517), OpenID Connect Core and RP-Initiated Logout
SignaturesRS256, RS384, RS512, ES256, ES384, ES512
DependenciesPython 3, requests, PyJWT, InquirerPy
SafetyClient secrets stay in the local configuration, and every secret-bearing configuration name is ignored by version control; tokens are printed to the terminal only and never stored
ClientsConfidential clients authenticated with a client secret
Provider guidesNotes on the Keycloak audience mapper and on adding a 2048-bit RSA signing key
Works withStandard OpenID Connect providers, including Keycloak and Google
Inputs, outputs, and standards of the tool.

The story behind the tool, and the disagreements between providers it uncovered.

It follows the shape I give every small tool: one file, one job, and the same order of sections every time — though, being interactive, it reports through its messages rather than an exit code.

The gateway and sign-in stack whose identity provider this check proves ready first.

The story of an audience sent as a list — the same lesson behind the audience and authorized-party rule here.

Terms

Authorization Code flow
The sign-in flow in which the provider returns a short-lived code to the client, which then exchanges it for tokens.
PKCE
Proof Key for Code Exchange: a secret verifier created by the client, whose hash is sent at the start and the original at the end, so a stolen code is useless.
ID token
A signed token, issued by an OpenID Connect provider, that states who the user is and for which client.
Opaque token
An access token that cannot be decoded by the client and must be checked by asking the provider.
Audience and authorized party
The claims that name who a token is for (aud) and which client requested it (azp).
RP-Initiated Logout
Signing a user out by sending them to the provider's logout address with a hint of the session to end.

Technologies covered

  • Python
  • Keycloak
  • JWT
  • OAuth 2.0
  • OpenID Connect
  • PKCE

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

A purple root circle linked to a blue and an orange intermediate circle, which lead to a signed document with a green seal and to a clock.

Aurograf

DevOps

The digital signature platform of the City Government of South Tangerang: signers prove who they are, sign PDFs to PAdES B-LTA with certificates from the platform's own authorities, and anyone can verify the result — version 2, which I designed, built and operate.

10 min read Read more →

Related Articles