# openid-client 6.8.8: a six-file ESM package whose quick start stops mid-declaration

> openid-client is panva's OAuth 2 and OpenID Connect client API for JavaScript runtimes, published on npm, JSR and GitHub under MIT. The quick start hands you a Configuration object, the authorization code snippet ends on a half-written type annotation, and the sponsor block in the middle of the file is a pitch from an identity vendor.

**panva/openid-client** — OAuth 2 / OpenID Connect Client API for JavaScript Runtimes

- Repository: https://github.com/panva/openid-client
- Stars: 2,425 · Forks: 407
- Language: TypeScript
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/panva-openid-client

## Discovery returns a Configuration object that every later call expects first

The entire quick start is four declarations and one call. You supply the authorization server's issuer identifier as a URL, the client identifier, and the client secret, and `client.discovery()` resolves to a `client.Configuration`:

```ts
let server!: URL // Authorization Server's Issuer Identifier
let clientId!: string // Client identifier at the Authorization Server
let clientSecret!: string // Client Secret

let config: client.Configuration = await client.discovery(
  server,
  clientId,
  clientSecret,
)
```

That object is the argument every subsequent function takes. `authorizationCodeGrant(config, ...)`, `initiateDeviceAuthorization(config, ...)`, `initiateBackchannelAuthentication(config, ...)` and `fetchProtectedResource(config, ...)` all open with it, so the shape of the whole API follows from this one call: configuration first, then the request-specific values.

The issuer is also a security input rather than a convenience. Authorization Server Issuer Identification is listed among the implemented features, so the identifier used for discovery is the same string the library uses to identify the server. Nothing in the text available here describes a fallback for a server whose metadata lives somewhere other than the standard path, which means discovery has to point at the value the server publishes.

For a public client there is no secret to pass, and the sample shows a `clientSecret` declaration unconditionally. The examples directory holds separate files for the OAuth 2 and OpenID Connect variants of the same flow, plus one file per extension, so the differences between profiles are kept in code rather than in prose.

## The PKCE snippet stops mid-declaration, and the state it later passes is never introduced

The authorization code section states the rule correctly: PKCE must be generated for every redirect to the authorization endpoint, and the code_verifier and state have to be stored in the end-user session so they can be recovered when the user comes back. The example that follows does not finish writing itself. After declaring `redirect_uri`, `scope` and `code_verifier`, it reaches a line that reads `let code_challenge: st` and the code fence closes on the next line:

```ts
let redirect_uri!: string
let scope!: string // Scope of the access request
/**
 * PKCE: The following MUST be generated for every redirect to the
 * authorization_endpoint. You must store the code_verifier and state in the
 * end-user session such that it can be recovered as the user gets redirected
 * from the authorization server back to your application.
 */
let code_verifier: string = client.randomPKCECodeVerifier()
let code_challenge: st
```

The type annotation stops after two characters and the binding is never completed, so nothing in the text shows how the challenge is derived from the verifier. A second gap follows from the callback snippet, which passes `expectedState: state` into `authorizationCodeGrant`. No `state` declaration appears in the section at all, even though the PKCE comment requires one to be generated per redirect and kept in the session.

The callback call itself is complete and shows the shape: `getCurrentUrl()` supplies the current URL with the code and state in it, and the options object carries `pkceCodeVerifier` and `expectedState`. What a reader has to supply from elsewhere is the session handling, the challenge derivation, and the state generation. None of that is a matter of taste. The state check is the parameter that ties the callback to the request your application started.

## Device flow and CIBA both end in a poll that resolves only after the user acts

The device grant starts with `initiateDeviceAuthorization(config, { scope })` and returns three values you display to the user: a user code, a verification URI, and a complete verification URI that carries the code. Polling then happens through `pollDeviceAuthorizationGrant(config, response)`, which the text describes as polling in a regular interval and resolving with tokens only once the end-user authenticates.

CIBA has the same shape with a different precondition. One of `login_hint`, `id_token_hint` or `login_hint_token` must be provided, so the flow cannot start without a way to identify the user, and `initiateBackchannelAuthentication(config, { scope, login_hint })` returns the handle that the poll consumes. Ping Mode adds an optional branch whose callback implementation is described as framework specific and therefore out of scope, which is an unusual admission for a library quick start to make about its own feature list.

The CIBA snippet ends the same way the PKCE one did. It declares the result type and awaits `pollBackchannelAuthenticationGrant` without the parentheses that make it a call, and the sentence after it stops after only resolve with tokens. So the two flows that require a user to do something outside the browser are also the two whose examples stop earliest.

Elsewhere the same pattern completes cleanly: `fetchProtectedResource(config, tokens.access_token, new URL('https://rs.example.com/api'), 'GET')` shows the access token, the resource URL and the method as separate arguments, and returns a `Response` you parse yourself.

## The published package is six files with no CommonJS entry and no runtime dependencies

The manifest is explicit about what ships. The `files` array enumerates exactly six build outputs, three for the main entry and three for the Passport subpath, each as JavaScript, a source map and a type declaration:

```json
"files": [
  "build/index.js",
  "build/index.js.map",
  "build/index.d.ts",
  "build/passport.js",
  "build/passport.js.map",
  "build/passport.d.ts"
],
```

The `exports` map has three keys: the root, `./passport`, and `./package.json`. There is no `require` condition anywhere in it, `type` is set to `module`, and `sideEffects` is false, so the package is ESM only and safe to import for types alone. The examples section labels its import as ESM and carries a footnote marker for CommonJS whose text is not present in the part available here.

Distribution runs through three channels, npmjs.com, jsr.io under the `@panva/openid-client` scope, and github.com, with a `jsr.json` at the repository root for the JSR side. The build script treats the second channel as part of verification rather than as a separate concern:

```json
"build": "npm run generate-build && tsc -p test && npm run --silent check-build && npx --yes jsr publish --dry-run --allow-dirty"
```

A dry run of the JSR publish is the last step of a build, and `tsc -p test` compiles the tests as part of it, so a type error in the test suite stops the build rather than surfacing later. A separate `check:packaging` script runs `publint --strict` and a tool at `tools/attw-check.js`, which is the standard pair for catching a broken exports map and a types condition that does not resolve.

## An identity vendor's pitch occupies the space between the feature list and the certification

The file has a Sponsor section that is not a list of sponsors in the usual sense. It renders a light and dark image of an Auth0 by Okta logo, then tells the reader that if they want to quickly add authentication to JavaScript apps they should check out Auth0's JavaScript SDK and free plan, and links a signup under a sponsor reference. The Help section above it asks for GitHub Sponsors to keep maintaining and improving the module.

Set against that, the software itself takes no position on which authorization server you use. The features are protocols and profiles: authorization server metadata discovery, the authorization code flow profiled under OpenID Connect 1.0, OAuth 2.0, OAuth 2.1, FAPI 1.0 Advanced and FAPI 2.0, then refresh token, device authorization, CIBA and client credentials grants, DPoP, introspection and revocation, pushed authorization requests, UserInfo and protected resource requests, JWT secured variants of introspection, JARM, JAR and UserInfo, and dynamic client registration.

The certification statement sits directly under the sponsor block. Filip Skokan has certified that the software conforms to the Basic, FAPI 1.0 and FAPI 2.0 Relying Party Conformance Profiles of OpenID Connect, and a certification image is committed at the repository root as `OpenID_Certified.png`. So the file both recommends a hosted authorization server and certifies against profile specifications that any server would have to meet, and the two sit close enough together that reading the sponsorship as an endorsement of the certification would be a mistake.

## Four test systems sit in the repository, and the extensions are documented as generated diffs

The root holds a `conformance/` directory for the OpenID conformance suite, `playwright.config.ts` for browser runs, `ava.config.mjs` for unit tests, a `tap/` directory for a second runner, and `test-d/` for type level tests, with separate `tsconfig.json`, `tsconfig.passport.json` and `tsconfig.docs.json` files to match. Two shell scripts at the top, `.node_flags.sh` and `.electron_flags.sh`, carry the runtime flags for the Node and Electron passes, so the same library is exercised in three JavaScript hosts rather than one.

Formatting is checked by a script that finds every TypeScript and JavaScript file across `src`, `test`, `test-d`, `examples`, `tap`, `conformance` and `tools` and pipes the list into `prettier --check`. There is a `patches/` directory for dependency patches, a `tools/` directory for the packaging checks, `typedoc.json` for the API reference, and release automation in `.postbump.cjs`, `.postchangelog.cjs` and `.versionrc.json`.

The examples are arranged the same way. Each extension is a source file plus a diff: `dpop.ts` with `dpop.diff`, `jar.ts` with `jar.diff`, `jarm.ts` with `jarm.diff`, `par.ts` with `par.diff`, and the OpenID Connect flow with `oidc.diff` beside the plain `oauth.ts`. A script in the examples directory regenerates those diffs, and `check-examples.sh` at the root sits next to them. The consequence for a reader is concrete: the OAuth and OpenID Connect examples can be compared line by line, and each extension is documented as the delta from the authorization code flow rather than as a separate tutorial.

## Conclusion

openid-client suits a TypeScript service that has to speak a specific OAuth or OpenID profile rather than a generic one, because the flows in scope are the ones with conformance requirements attached, and the package is small enough to read: six published files, a sideEffects flag of false, and a build that compiles the tests before it publishes anything. Two things decide whether it fits. The first is that discovery returns a Configuration object and every other call takes it as its first argument, so the issuer identifier has to be correct before anything else works, including issuer identification. The second is that the quick start is a sketch: the authorization code example ends on an unterminated declaration and never introduces the state value that the token exchange later passes as expectedState, while the CIBA poll is written without its call parentheses. Copy the PKCE storage requirement, not the snippet. Confirm your runtime is on the ESM-only exports map, confirm your authorization server is one of the certified profiles if conformance matters to you, and read the sponsorship section with the knowledge that its advice points at a hosted authorization server rather than at anything in this repository.

## FAQ

### What is openid-client used for?

It provides APIs for the most common OAuth 2 and OpenID Connect authentication and authorization flows, and is designed for JavaScript runtimes such as Node.js, browsers, Deno and Cloudflare Workers. Integration starts from client.discovery(), which returns a Configuration object.

### Does openid-client support CommonJS?

The manifest sets type to module and the exports map offers only the root entry, the ./passport subpath and ./package.json, with no require condition. The files array lists ESM build output with source maps and type declarations, and the examples heading is labelled as an ESM import.

### Which OAuth flows does openid-client implement?

Authorization Code Flow, Refresh Token, Device Authorization, Client-Initiated Backchannel Authentication and Client Credentials grants, plus DPoP, token introspection and revocation, pushed authorization requests, UserInfo and protected resource requests, dynamic client registration, and JWT secured variants of introspection, JARM, JAR and UserInfo.

### Where is openid-client published?

It is distributed through npmjs.com, jsr.io under the @panva/openid-client scope, and github.com. The build script ends with npx --yes jsr publish --dry-run --allow-dirty, so a dry run of the JSR publish is the final step of a build.

### Is openid-client an OpenID certified implementation?

Filip Skokan has certified that the software conforms to the Basic, FAPI 1.0 and FAPI 2.0 Relying Party Conformance Profiles of OpenID Connect, and a certification image is committed at the repository root as OpenID_Certified.png.

### How does openid-client handle PKCE and state?

The documentation requires a code_verifier and state to be generated for every redirect to the authorization endpoint and stored in the end-user session so they can be recovered on the way back. The token exchange then passes them as pkceCodeVerifier and expectedState, with the verifier produced by client.randomPKCECodeVerifier().

## Sources

- [Issues](https://github.com/panva/openid-client/issues)
- [License: MIT](https://github.com/panva/openid-client/blob/main/LICENSE)
- [panva/openid-client on GitHub](https://github.com/panva/openid-client)
- [README](https://github.com/panva/openid-client/blob/main/README.md)
- [Releases](https://github.com/panva/openid-client/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/panva-openid-client
