@effected/oidc 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/IdTokenClaims.js +36 -0
- package/IdentityToken.js +74 -0
- package/LICENSE +21 -0
- package/Oidc.js +94 -0
- package/README.md +2 -2
- package/index.d.ts +214 -0
- package/index.js +5 -0
- package/package.json +48 -5
- package/tsdoc-metadata.json +11 -0
package/IdTokenClaims.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { RegisteredClaims } from "@effected/jwt";
|
|
3
|
+
|
|
4
|
+
// src/IdTokenClaims.ts
|
|
5
|
+
/**
|
|
6
|
+
* The claims of an OpenID Connect ID token: RFC 7519's registered claims
|
|
7
|
+
* plus the ID-token claims OpenID Connect Core 1.0 §2 adds.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Every claim is optional, so the schema decodes any provider's token, and
|
|
11
|
+
* the schema is open: claims it does not name survive the decode. A
|
|
12
|
+
* provider's own claims belong in a caller-supplied schema passed to
|
|
13
|
+
* {@link (Oidc:class).verify} or {@link (Oidc:class).decodeUnverified},
|
|
14
|
+
* not in a subclass here. `@effected/github`'s `ActionsOidcClaims` is one.
|
|
15
|
+
*
|
|
16
|
+
* Optional here does not mean optional in a verified token:
|
|
17
|
+
* {@link (Oidc:class).verify} still requires `exp` and checks `iss`, `aud`,
|
|
18
|
+
* `nbf` and `iat` whatever schema the payload is then decoded with.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
const IdTokenClaims = Schema.StructWithRest(Schema.Struct({
|
|
23
|
+
...RegisteredClaims.schema.fields,
|
|
24
|
+
/** Authorized party: the client the token was issued to, when it differs from the audience. */
|
|
25
|
+
azp: Schema.optionalKey(Schema.String),
|
|
26
|
+
/** The value the client sent in its authentication request, echoed back to bind the token to it. */
|
|
27
|
+
nonce: Schema.optionalKey(Schema.String),
|
|
28
|
+
/** When the end user authenticated, in seconds since the epoch. */
|
|
29
|
+
auth_time: Schema.optionalKey(Schema.Finite),
|
|
30
|
+
/** Authentication context class reference. */
|
|
31
|
+
acr: Schema.optionalKey(Schema.String),
|
|
32
|
+
/** Authentication method references, e.g. `pwd` or `mfa`. */
|
|
33
|
+
amr: Schema.optionalKey(Schema.Array(Schema.String))
|
|
34
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
35
|
+
|
|
36
|
+
export { IdTokenClaims };
|
package/IdentityToken.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Redacted, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
// src/IdentityToken.ts
|
|
4
|
+
/**
|
|
5
|
+
* Raised when an identity token cannot be obtained.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* The audience is on the error because "which audience" is the first thing a
|
|
9
|
+
* caller checks when an exchange is refused — a token minted for the wrong one
|
|
10
|
+
* fails at the certificate authority, far from here.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
var IdentityTokenError = class extends Schema.TaggedError()("IdentityTokenError", {
|
|
15
|
+
/** The audience the token was requested for. */
|
|
16
|
+
audience: Schema.String,
|
|
17
|
+
/** The underlying failure, preserved structurally. */
|
|
18
|
+
cause: Schema.Defect()
|
|
19
|
+
}) {
|
|
20
|
+
get message() {
|
|
21
|
+
return `Could not obtain an identity token for the "${this.audience}" audience`;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/** The token every double answers with unless a test says otherwise. */
|
|
25
|
+
const TEST_TOKEN = "test-identity-token";
|
|
26
|
+
/**
|
|
27
|
+
* A source of workload identity tokens (OIDC): the input `@effected/sbom`'s
|
|
28
|
+
* `SigstoreSigner` needs to obtain a signing certificate, and any other
|
|
29
|
+
* consumer needs to present a workload identity.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* In GitHub Actions, provide `ActionsIdentityToken.layer` from
|
|
33
|
+
* `@effected/github-actions`. A consumer that already holds a token uses
|
|
34
|
+
* {@link (IdentityToken:class).layerStatic}; tests use
|
|
35
|
+
* {@link (IdentityToken:class).layerTest}.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { IdentityToken } from "@effected/oidc";
|
|
40
|
+
* import { SigstoreSigner } from "@effected/sbom";
|
|
41
|
+
* import { Layer } from "effect";
|
|
42
|
+
*
|
|
43
|
+
* const token = process.env.OIDC_TOKEN ?? "";
|
|
44
|
+
* const layer = SigstoreSigner.layer.pipe(Layer.provide(IdentityToken.layerStatic(token)));
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
var IdentityToken = class IdentityToken extends Context.Service()("@effected/oidc/IdentityToken") {
|
|
50
|
+
/**
|
|
51
|
+
* A layer answering with a token the caller already holds.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* For a consumer that obtained a token by some other route — a CI system
|
|
55
|
+
* that is not GitHub Actions, or a script that exchanged one itself. The
|
|
56
|
+
* audience is **ignored**, so it is the caller's job to have minted the token
|
|
57
|
+
* for the audience it will be used with; a layer cannot check that, and
|
|
58
|
+
* pretending otherwise would be theatre.
|
|
59
|
+
*/
|
|
60
|
+
static layerStatic = (token) => Layer.succeed(IdentityToken, { token: () => Effect.succeed(typeof token === "string" ? Redacted.make(token) : token) });
|
|
61
|
+
/**
|
|
62
|
+
* An in-memory double.
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* Unlike `@effected/sbom`'s `SigstoreSigner.makeTest`, this one **answers** rather than
|
|
66
|
+
* dying: a fabricated OIDC token is a real answer to "give me a token" in a
|
|
67
|
+
* test, where a fabricated signature would be a lie about cryptography.
|
|
68
|
+
*/
|
|
69
|
+
static makeTest = (overrides = {}) => ({ token: overrides.token ?? (() => Effect.succeed(Redacted.make(TEST_TOKEN))) });
|
|
70
|
+
/** {@link (IdentityToken:class).makeTest} behind a `Layer`. */
|
|
71
|
+
static layerTest = (overrides = {}) => Layer.succeed(IdentityToken, IdentityToken.makeTest(overrides));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export { IdentityToken, IdentityTokenError };
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 C. Spencer Beggs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Oidc.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { JwksResolver, Jws, Jwt, JwtError } from "@effected/jwt";
|
|
3
|
+
|
|
4
|
+
// src/Oidc.ts
|
|
5
|
+
const audiences = (audience) => typeof audience === "string" ? [audience] : audience;
|
|
6
|
+
const decodeUnverified = Effect.fn("Oidc.decodeUnverified")(function* (token, claims) {
|
|
7
|
+
const { payload } = yield* Effect.fromResult(Jws.decodeUnverified(token));
|
|
8
|
+
return yield* Schema.decodeUnknownEffect(claims)(payload).pipe(Effect.mapError((cause) => new JwtError({
|
|
9
|
+
reason: "claims",
|
|
10
|
+
detail: "the payload does not match the expected claims",
|
|
11
|
+
cause
|
|
12
|
+
})));
|
|
13
|
+
});
|
|
14
|
+
const verify = Effect.fn("Oidc.verify")(function* (token, options) {
|
|
15
|
+
const expected = audiences(options.audience);
|
|
16
|
+
yield* Effect.annotateCurrentSpan({
|
|
17
|
+
issuer: options.issuer,
|
|
18
|
+
audience: expected.join(" ")
|
|
19
|
+
});
|
|
20
|
+
if (expected.length === 0 || expected.some((value) => value.length === 0)) return yield* new JwtError({
|
|
21
|
+
reason: "wrongAudience",
|
|
22
|
+
detail: "no audience was given; an OIDC token is never verified without one"
|
|
23
|
+
});
|
|
24
|
+
return yield* Jwt.verify(token, {
|
|
25
|
+
key: JwksResolver.forIssuer(options.issuer),
|
|
26
|
+
claims: options.claims,
|
|
27
|
+
issuer: options.issuer,
|
|
28
|
+
audience: expected,
|
|
29
|
+
...options.clockTolerance !== void 0 ? { clockTolerance: options.clockTolerance } : {}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Read and verify OpenID Connect ID tokens.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Both operations fail with `@effected/jwt`'s `JwtError` and add no error of
|
|
37
|
+
* their own; route on its `reason`, never on its message.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* import { JwksResolver } from "@effected/jwt";
|
|
42
|
+
* import { IdTokenClaims, Oidc } from "@effected/oidc";
|
|
43
|
+
* import { Effect } from "effect";
|
|
44
|
+
*
|
|
45
|
+
* const program = Oidc.verify("eyJ...", {
|
|
46
|
+
* issuer: "https://accounts.example.com",
|
|
47
|
+
* audience: "my-client-id",
|
|
48
|
+
* claims: IdTokenClaims,
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* // JwksResolver.layer needs an HttpClient and a JwksStore, provided once at the edge.
|
|
52
|
+
* export const main = program.pipe(Effect.provide(JwksResolver.layer));
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
var Oidc = class {
|
|
58
|
+
constructor() {}
|
|
59
|
+
/**
|
|
60
|
+
* Decode a token's payload with `claims` **without verifying it**.
|
|
61
|
+
*
|
|
62
|
+
* @remarks
|
|
63
|
+
* `Jws.decodeUnverified` plus a schema decode. No signature, issuer,
|
|
64
|
+
* audience or time check runs, as the name says. For reading a token this
|
|
65
|
+
* process was handed by a runtime it already trusts, never for
|
|
66
|
+
* authorizing anything: the result is whatever the token's author wrote.
|
|
67
|
+
*
|
|
68
|
+
* Fails as `malformed` when the token is not a compact JWS with a JOSE
|
|
69
|
+
* header, and as `claims` when the payload does not decode.
|
|
70
|
+
*
|
|
71
|
+
* @public
|
|
72
|
+
*/
|
|
73
|
+
static decodeUnverified = decodeUnverified;
|
|
74
|
+
/**
|
|
75
|
+
* Verify an ID token from `options.issuer` and decode its payload with
|
|
76
|
+
* `options.claims`.
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* `Jwt.verify` keyed through `JwksResolver.forIssuer(options.issuer)`:
|
|
80
|
+
* the signature against the issuer's discovered JWKS, then `exp`, `nbf`,
|
|
81
|
+
* `iat`, `iss` and `aud`, then the schema decode. An empty audience, or an
|
|
82
|
+
* empty string among several, fails as `wrongAudience` before anything
|
|
83
|
+
* else, with no network call.
|
|
84
|
+
*
|
|
85
|
+
* The key is always looked up for the configured issuer, never for the
|
|
86
|
+
* token's own `iss`, so a token naming another issuer is never checked
|
|
87
|
+
* against that issuer's keys.
|
|
88
|
+
*
|
|
89
|
+
* @public
|
|
90
|
+
*/
|
|
91
|
+
static verify = verify;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export { Oidc };
|
package/README.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @effected/oidc
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OpenID Connect for Effect v4: the IdentityToken issuer contract, ID-token claims, and a discovery-backed verify over @effected/jwt.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { Context, Duration, Effect, Layer, Redacted, Schema } from "effect";
|
|
2
|
+
import { JwksResolver, JwtError } from "@effected/jwt";
|
|
3
|
+
declare const IdentityTokenError_base: Schema.Class<IdentityTokenError, Schema.TaggedStruct<"IdentityTokenError", {
|
|
4
|
+
/** The audience the token was requested for. */
|
|
5
|
+
readonly audience: Schema.String;
|
|
6
|
+
/** The underlying failure, preserved structurally. */
|
|
7
|
+
readonly cause: Schema.Defect;
|
|
8
|
+
}>, import("effect/Cause").YieldableError>;
|
|
9
|
+
/**
|
|
10
|
+
* Raised when an identity token cannot be obtained.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* The audience is on the error because "which audience" is the first thing a
|
|
14
|
+
* caller checks when an exchange is refused — a token minted for the wrong one
|
|
15
|
+
* fails at the certificate authority, far from here.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export declare class IdentityTokenError extends IdentityTokenError_base {
|
|
20
|
+
get message(): string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The contract: one method, one audience, one redacted token.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* Deliberately smaller than any issuer's own surface. An implementation may
|
|
27
|
+
* cache, may decode claims, may do neither — none of that is the contract's
|
|
28
|
+
* business, and a wider contract would make github-actions' issuer the only
|
|
29
|
+
* thing that could satisfy it.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
interface IdentityTokenShape {
|
|
34
|
+
/** A workload identity token for `audience`. */
|
|
35
|
+
readonly token: (audience: string) => Effect.Effect<Redacted.Redacted<string>, IdentityTokenError>;
|
|
36
|
+
}
|
|
37
|
+
declare const IdentityToken_base: Context.ServiceClass<IdentityToken, "@effected/oidc/IdentityToken", IdentityTokenShape>;
|
|
38
|
+
/**
|
|
39
|
+
* A source of workload identity tokens (OIDC): the input `@effected/sbom`'s
|
|
40
|
+
* `SigstoreSigner` needs to obtain a signing certificate, and any other
|
|
41
|
+
* consumer needs to present a workload identity.
|
|
42
|
+
*
|
|
43
|
+
* @remarks
|
|
44
|
+
* In GitHub Actions, provide `ActionsIdentityToken.layer` from
|
|
45
|
+
* `@effected/github-actions`. A consumer that already holds a token uses
|
|
46
|
+
* {@link (IdentityToken:class).layerStatic}; tests use
|
|
47
|
+
* {@link (IdentityToken:class).layerTest}.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { IdentityToken } from "@effected/oidc";
|
|
52
|
+
* import { SigstoreSigner } from "@effected/sbom";
|
|
53
|
+
* import { Layer } from "effect";
|
|
54
|
+
*
|
|
55
|
+
* const token = process.env.OIDC_TOKEN ?? "";
|
|
56
|
+
* const layer = SigstoreSigner.layer.pipe(Layer.provide(IdentityToken.layerStatic(token)));
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
61
|
+
export declare class IdentityToken extends IdentityToken_base {
|
|
62
|
+
/**
|
|
63
|
+
* A layer answering with a token the caller already holds.
|
|
64
|
+
*
|
|
65
|
+
* @remarks
|
|
66
|
+
* For a consumer that obtained a token by some other route — a CI system
|
|
67
|
+
* that is not GitHub Actions, or a script that exchanged one itself. The
|
|
68
|
+
* audience is **ignored**, so it is the caller's job to have minted the token
|
|
69
|
+
* for the audience it will be used with; a layer cannot check that, and
|
|
70
|
+
* pretending otherwise would be theatre.
|
|
71
|
+
*/
|
|
72
|
+
static readonly layerStatic: (token: Redacted.Redacted<string> | string) => Layer.Layer<IdentityToken>;
|
|
73
|
+
/**
|
|
74
|
+
* An in-memory double.
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* Unlike `@effected/sbom`'s `SigstoreSigner.makeTest`, this one **answers** rather than
|
|
78
|
+
* dying: a fabricated OIDC token is a real answer to "give me a token" in a
|
|
79
|
+
* test, where a fabricated signature would be a lie about cryptography.
|
|
80
|
+
*/
|
|
81
|
+
static readonly makeTest: (overrides?: Partial<IdentityTokenShape>) => IdentityTokenShape;
|
|
82
|
+
/** {@link (IdentityToken:class).makeTest} behind a `Layer`. */
|
|
83
|
+
static readonly layerTest: (overrides?: Partial<IdentityTokenShape>) => Layer.Layer<IdentityToken>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The claims of an OpenID Connect ID token: RFC 7519's registered claims
|
|
87
|
+
* plus the ID-token claims OpenID Connect Core 1.0 §2 adds.
|
|
88
|
+
*
|
|
89
|
+
* @remarks
|
|
90
|
+
* Every claim is optional, so the schema decodes any provider's token, and
|
|
91
|
+
* the schema is open: claims it does not name survive the decode. A
|
|
92
|
+
* provider's own claims belong in a caller-supplied schema passed to
|
|
93
|
+
* {@link (Oidc:class).verify} or {@link (Oidc:class).decodeUnverified},
|
|
94
|
+
* not in a subclass here. `@effected/github`'s `ActionsOidcClaims` is one.
|
|
95
|
+
*
|
|
96
|
+
* Optional here does not mean optional in a verified token:
|
|
97
|
+
* {@link (Oidc:class).verify} still requires `exp` and checks `iss`, `aud`,
|
|
98
|
+
* `nbf` and `iat` whatever schema the payload is then decoded with.
|
|
99
|
+
*
|
|
100
|
+
* @public
|
|
101
|
+
*/
|
|
102
|
+
export declare const IdTokenClaims: Schema.StructWithRest<Schema.Struct<{
|
|
103
|
+
readonly iss: Schema.optionalKey<Schema.String>;
|
|
104
|
+
readonly sub: Schema.optionalKey<Schema.String>;
|
|
105
|
+
readonly aud: Schema.optionalKey<Schema.Union<readonly [Schema.String, Schema.$Array<Schema.String>]>>;
|
|
106
|
+
readonly exp: Schema.optionalKey<Schema.Finite>;
|
|
107
|
+
readonly nbf: Schema.optionalKey<Schema.Finite>;
|
|
108
|
+
readonly iat: Schema.optionalKey<Schema.Finite>;
|
|
109
|
+
readonly jti: Schema.optionalKey<Schema.String>;
|
|
110
|
+
/** Authorized party: the client the token was issued to, when it differs from the audience. */
|
|
111
|
+
readonly azp: Schema.optionalKey<Schema.String>;
|
|
112
|
+
/** The value the client sent in its authentication request, echoed back to bind the token to it. */
|
|
113
|
+
readonly nonce: Schema.optionalKey<Schema.String>;
|
|
114
|
+
/** When the end user authenticated, in seconds since the epoch. */
|
|
115
|
+
readonly auth_time: Schema.optionalKey<Schema.Finite>;
|
|
116
|
+
/** Authentication context class reference. */
|
|
117
|
+
readonly acr: Schema.optionalKey<Schema.String>;
|
|
118
|
+
/** Authentication method references, e.g. `pwd` or `mfa`. */
|
|
119
|
+
readonly amr: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
120
|
+
}>, readonly [Schema.$Record<Schema.String, Schema.Unknown>]>;
|
|
121
|
+
/**
|
|
122
|
+
* A decoded {@link (IdTokenClaims:variable)}.
|
|
123
|
+
*
|
|
124
|
+
* @public
|
|
125
|
+
*/
|
|
126
|
+
export type IdTokenClaims = typeof IdTokenClaims.Type;
|
|
127
|
+
/**
|
|
128
|
+
* Options for {@link (Oidc:class).verify}.
|
|
129
|
+
*
|
|
130
|
+
* @typeParam S - The schema the verified payload is decoded with.
|
|
131
|
+
*
|
|
132
|
+
* @public
|
|
133
|
+
*/
|
|
134
|
+
interface OidcVerifyOptions<S extends Schema.Constraint> {
|
|
135
|
+
/**
|
|
136
|
+
* The issuer the token must come from. Its keys are discovered from
|
|
137
|
+
* `<issuer>/.well-known/openid-configuration` and `iss` must equal it.
|
|
138
|
+
*/
|
|
139
|
+
readonly issuer: string;
|
|
140
|
+
/**
|
|
141
|
+
* The audience the token must have been minted for, or any of several.
|
|
142
|
+
* Required and non-empty: an OIDC issuer's one key set signs tokens for
|
|
143
|
+
* every relying party, so without an audience check a token minted for
|
|
144
|
+
* any of them would verify.
|
|
145
|
+
*/
|
|
146
|
+
readonly audience: string | ReadonlyArray<string>;
|
|
147
|
+
/** The schema the payload is decoded with once every check has passed; `IdTokenClaims` for the standard set. */
|
|
148
|
+
readonly claims: S;
|
|
149
|
+
/** Allowed clock skew for `exp`, `nbf` and `iat`; 60 seconds by default. */
|
|
150
|
+
readonly clockTolerance?: Duration.Input | undefined;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Read and verify OpenID Connect ID tokens.
|
|
154
|
+
*
|
|
155
|
+
* @remarks
|
|
156
|
+
* Both operations fail with `@effected/jwt`'s `JwtError` and add no error of
|
|
157
|
+
* their own; route on its `reason`, never on its message.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* import { JwksResolver } from "@effected/jwt";
|
|
162
|
+
* import { IdTokenClaims, Oidc } from "@effected/oidc";
|
|
163
|
+
* import { Effect } from "effect";
|
|
164
|
+
*
|
|
165
|
+
* const program = Oidc.verify("eyJ...", {
|
|
166
|
+
* issuer: "https://accounts.example.com",
|
|
167
|
+
* audience: "my-client-id",
|
|
168
|
+
* claims: IdTokenClaims,
|
|
169
|
+
* });
|
|
170
|
+
*
|
|
171
|
+
* // JwksResolver.layer needs an HttpClient and a JwksStore, provided once at the edge.
|
|
172
|
+
* export const main = program.pipe(Effect.provide(JwksResolver.layer));
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
* @public
|
|
176
|
+
*/
|
|
177
|
+
export declare class Oidc {
|
|
178
|
+
private constructor();
|
|
179
|
+
/**
|
|
180
|
+
* Decode a token's payload with `claims` **without verifying it**.
|
|
181
|
+
*
|
|
182
|
+
* @remarks
|
|
183
|
+
* `Jws.decodeUnverified` plus a schema decode. No signature, issuer,
|
|
184
|
+
* audience or time check runs, as the name says. For reading a token this
|
|
185
|
+
* process was handed by a runtime it already trusts, never for
|
|
186
|
+
* authorizing anything: the result is whatever the token's author wrote.
|
|
187
|
+
*
|
|
188
|
+
* Fails as `malformed` when the token is not a compact JWS with a JOSE
|
|
189
|
+
* header, and as `claims` when the payload does not decode.
|
|
190
|
+
*
|
|
191
|
+
* @public
|
|
192
|
+
*/
|
|
193
|
+
static readonly decodeUnverified: <S extends Schema.Constraint>(token: string, claims: S) => Effect.Effect<S["Type"], JwtError, S["DecodingServices"]>;
|
|
194
|
+
/**
|
|
195
|
+
* Verify an ID token from `options.issuer` and decode its payload with
|
|
196
|
+
* `options.claims`.
|
|
197
|
+
*
|
|
198
|
+
* @remarks
|
|
199
|
+
* `Jwt.verify` keyed through `JwksResolver.forIssuer(options.issuer)`:
|
|
200
|
+
* the signature against the issuer's discovered JWKS, then `exp`, `nbf`,
|
|
201
|
+
* `iat`, `iss` and `aud`, then the schema decode. An empty audience, or an
|
|
202
|
+
* empty string among several, fails as `wrongAudience` before anything
|
|
203
|
+
* else, with no network call.
|
|
204
|
+
*
|
|
205
|
+
* The key is always looked up for the configured issuer, never for the
|
|
206
|
+
* token's own `iss`, so a token naming another issuer is never checked
|
|
207
|
+
* against that issuer's keys.
|
|
208
|
+
*
|
|
209
|
+
* @public
|
|
210
|
+
*/
|
|
211
|
+
static readonly verify: <S extends Schema.Constraint>(token: string, options: OidcVerifyOptions<S>) => Effect.Effect<S["Type"], JwtError, JwksResolver | S["DecodingServices"]>;
|
|
212
|
+
}
|
|
213
|
+
export type { IdentityTokenShape, OidcVerifyOptions };
|
|
214
|
+
//# sourceMappingURL=index.d.ts.map
|
package/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@effected/oidc",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "OpenID Connect for Effect v4: the IdentityToken issuer contract, ID-token claims, and a discovery-backed verify over @effected/jwt.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"oidc",
|
|
8
|
+
"openid-connect",
|
|
9
|
+
"id-token",
|
|
10
|
+
"jwt",
|
|
11
|
+
"jwks",
|
|
12
|
+
"effect",
|
|
13
|
+
"effected"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/oidc#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/spencerbeggs/effected/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/spencerbeggs/effected.git",
|
|
22
|
+
"directory": "packages/oidc"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": {
|
|
26
|
+
"name": "C. Spencer Beggs",
|
|
27
|
+
"email": "spencer@beggs.codes",
|
|
28
|
+
"url": "https://spencerbeg.gs"
|
|
29
|
+
},
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"type": "module",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./index.d.ts",
|
|
35
|
+
"import": "./index.js",
|
|
36
|
+
"default": "./index.js"
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@effected/jwt": "^0.5.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"effect": "^4.0.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=24.11.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
|
2
|
+
// It should be published with your NPM package. It should not be tracked by Git.
|
|
3
|
+
{
|
|
4
|
+
"tsdocVersion": "0.12",
|
|
5
|
+
"toolPackages": [
|
|
6
|
+
{
|
|
7
|
+
"packageName": "@microsoft/api-extractor",
|
|
8
|
+
"packageVersion": "7.59.1"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|