@effected/oidc

中文文档
0.5.0发布于 2026/11/10MITTypeScriptES Module构建来源已验证近 30 天下载 3813

OpenID Connect for Effect v4: the IdentityToken issuer contract, ID-token claims, and a discovery-backed verify over @effected/jwt.

原始使用文档
文档目录9 节
  1. Why @effected/oidc
  2. Install
  3. What to provide
  4. Verify an ID token
  5. The ID-token claims
  6. The IdentityToken contract
  7. Errors
  8. Testing
  9. License

@effected/oidc

npm License: MIT Node.js %3E%3D24.11.0

OpenID Connect for Effect v4: verify an ID token against its issuer's discovered keys, read the standard ID-token claims, and ask for a workload identity token through one small contract that any runtime can serve. It runs anywhere WebCrypto does, Cloudflare Workers included, with no node: import.

Pre-1.0.0. This package is part of the @effected/* kit, built on stable Effect v4 and still in 0.x development. To keep your effect and @effect/* versions on the line the kit is built and tested against, install @effected/pnpm-plugin-effected.

Stability: unstable. This package's API surface is not yet considered complete and may change across 0.x releases. Pin an exact version. Full policy: release strategy.

Why @effected/oidc

Verifying an OIDC ID token is a JWT verification with three rules that are easy to get wrong: the keys come from the issuer's discovery document, the issuer you trust is the one you configured rather than the one the token claims, and the audience must always be checked, because an issuer's one key set signs tokens for every relying party. Oidc.verify holds all three: it looks keys up for the configured issuer only, and it refuses an empty audience before making any request.

The signature and claim checks run on core effect/jose, through @effected/jwt's discovery-backed JwksResolver. If you hold the issuer's key, or its JWKS URL is fixed in your configuration, core effect/jose (Jwt.verify with Jwks.layerRemote) does the job without this package. Reach for Oidc.verify when the keys are found by discovery, when you want @effected/jwt's safer defaults (60 seconds of clock skew, exp required), or when you want IdTokenClaims and the IdentityToken contract.

For a GitHub Actions OIDC token, use ActionsOidc.verify from @effected/github, which is Oidc.verify pinned to GitHub's issuer with typed claims.

Install

npm install @effected/oidc effect
pnpm add @effected/oidc effect

On Node, requires Node.js >=24.11.0; it also runs on any other WebCrypto runtime, Cloudflare Workers included. effect v4 is the only peer, and @effected/jwt comes with it. Import JwksResolver and JwksStore from @effected/jwt. All @effected/* packages are ESM-only.

What to provide

OperationRequires
Oidc.verifyJwksResolver (from @effected/jwt) and Signature.Signature (from effect/crypto)
Oidc.decodeUnverifiednothing
a program using IdentityTokenan IdentityToken layer (below)
ServiceAny WebCrypto runtime (Workers included)Node
Signature.SignatureSignature.layerWebCrypto.pipe(Layer.provide(SecureRandom.layerWebCrypto))NodeCrypto.layerSignature from @effect/platform-node
JwksResolverJwksResolver.layer, over an HttpClient (FetchHttpClient.layer) and a JwksStore (JwksStore.layerMemory, or a KV-backed store)the same, or NodeHttpClient.layerUndici for the client
IdentityTokenIdentityToken.layerStatic(token); in GitHub Actions, ActionsIdentityToken.layer from @effected/github-actionsthe same
import { JwksResolver, JwksStore } from "@effected/jwt";
import { Layer } from "effect";
import { SecureRandom, Signature } from "effect/crypto";
import { FetchHttpClient } from "effect/http";

const OidcLive = Layer.merge(
  JwksResolver.layer.pipe(Layer.provide(Layer.merge(FetchHttpClient.layer, JwksStore.layerMemory))),
  Signature.layerWebCrypto.pipe(Layer.provide(SecureRandom.layerWebCrypto)),
);

Verify an ID token

import { IdTokenClaims, Oidc } from "@effected/oidc";

const verifyIdToken = (token: string) =>
  Oidc.verify(token, {
    issuer: "https://accounts.example.com",
    audience: "my-client-id",
    claims: IdTokenClaims,
  });
// Effect<IdTokenClaims, JwtError, JwksResolver | Signature>

The keys are discovered from <issuer>/.well-known/openid-configuration, and the discovery document must name that issuer exactly. Then come the signature, exp (required), iss (must equal issuer), aud (must contain one of audience, which may be a list), nbf, your claims schema, and iat not in the future, with 60 seconds of clock skew allowed (clockTolerance changes it).

The ID-token claims

IdTokenClaims is the registered JWT claims (iss, sub, aud, exp, nbf, iat, jti) plus the OpenID Connect ID-token claims azp, nonce, auth_time, acr and amr. Every claim is optional, so it decodes any provider's token, and the schema is open, so claims it does not name survive the decode. Optional in the schema does not mean optional in a verified token: Oidc.verify still requires exp and checks iss and aud.

A provider's own claims belong in your own schema, built on IdTokenClaims's fields:

import { IdTokenClaims, Oidc } from "@effected/oidc";
import { Schema } from "effect";

const GoogleClaims = Schema.StructWithRest(
  Schema.Struct({
    ...IdTokenClaims.schema.fields,
    email: Schema.String,
    email_verified: Schema.Boolean,
  }),
  [Schema.Record(Schema.String, Schema.Unknown)],
);

const verifyGoogle = (token: string) =>
  Oidc.verify(token, { issuer: "https://accounts.google.com", audience: "my-client-id", claims: GoogleClaims });

Oidc.decodeUnverified(token, claims) decodes a token's payload with a schema without verifying anything. It is for reading a token your process was handed by a runtime it already trusts, never for authorizing a request.

The IdentityToken contract

IdentityToken is the other direction: a program that needs to present a workload identity (to exchange it for a signing certificate, say) asks for one token for one audience, and the runtime decides how to get it.

import { IdentityToken } from "@effected/oidc";
import { Effect, Redacted } from "effect";

const presentIdentity = Effect.gen(function* () {
  const identity = yield* IdentityToken;
  const token = yield* identity.token("https://my-service.example");
  return Redacted.value(token);
});

// A token you already hold. The audience is ignored: mint it for the audience it will be used with.
const program = presentIdentity.pipe(Effect.provide(IdentityToken.layerStatic(process.env.OIDC_TOKEN ?? "")));

In GitHub Actions, provide ActionsIdentityToken.layer from @effected/github-actions, which asks the runner's token service. IdentityToken itself imports nothing but effect, so a package that only needs the contract links no JWT code.

Errors

Oidc.verify and Oidc.decodeUnverified fail with @effected/jwt's JwtError and add no error of their own. Route on reason:

reasonMeaning
wrongAudienceaud contains none of the accepted audiences, or is missing; also an empty audience option, refused before any request.
wrongIssueriss is missing or is not the configured issuer.
expired / notYetValidexp has passed, or nbf or iat is in the future, beyond the clock tolerance.
claimsexp is missing, a registered claim has the wrong type, or your claims schema refused the payload.
badSignatureThe signature does not verify against the issuer's key.
unknownKidThe issuer's key set has no key for the token's algorithm and kid, even after one refetch.
jwksFetchThe discovery document or key set could not be fetched, was not https:, or named another issuer.
malformedNot a compact JWS with a JSON-object payload, or alg: "none".
unsupportedAlgorithm / algorithmMismatch / keyAn HMAC algorithm, an alg that is not the key's, or a published key that will not import.

IdentityToken.token fails with IdentityTokenError, carrying the audience the token was requested for and the underlying cause.

Testing

IdentityToken.layerTest() answers every request with a fixed test token; pass { token } to answer differently. To test Oidc.verify, sign with TestIssuer from @effected/jwt/testing and provide its resolverLayer, the real resolver over an in-memory issuer:

import { TestIssuer } from "@effected/jwt/testing";
import { IdTokenClaims, Oidc } from "@effected/oidc";
import { Effect } from "effect";

const test = Effect.gen(function* () {
  const issuer = yield* TestIssuer.make({ issuer: "https://accounts.example.com" });
  const token = yield* issuer.sign({ aud: "my-client-id", sub: "user-1" });
  return yield* Oidc.verify(token, {
    issuer: "https://accounts.example.com",
    audience: "my-client-id",
    claims: IdTokenClaims,
  }).pipe(Effect.provide(issuer.resolverLayer));
});
// provide Signature (see "What to provide") around the test

License

MIT

版本与发布记录

@effected/oidc 共发布 8 个版本,首次发布于 2026/10/23;过去 12 个月发布了 8 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.5.0latest2026/11/103 天前
0.4.02026/11/0310 天前
0.3.02026/11/0112 天前
0.2.02026/10/2915 天前
0.1.22026/10/2420 天前
0.1.12026/10/2420 天前

关于 @effected/oidc 的常见问题

@effected/oidc 是什么?

OpenID Connect for Effect v4: the IdentityToken issuer contract, ID-token claims, and a discovery-backed verify over @effected/jwt.

如何安装 @effected/oidc?

在项目目录运行 npm install @effected/oidc,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:effect(~4.1.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@effected/oidc 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@effected/oidc 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@effected/oidc 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24.11.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@effected/oidc 可以商用吗?

@effected/oidc 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@effected/oidc 的下载量有多少?

根据 npm 官方下载统计,@effected/oidc 在 2026/10/13 – 2026/11/11 这 30 天内被下载 3,813 次,日均约 127 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@effected/oidc 还在维护吗?

最新版本 0.5.0 发布于 2026/11/10(3 天前);至今共发布 8 个版本,其中过去 12 个月发布了 8 个;首次发布于 2026/10/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。