@effect-auth/core 0.1.0-alpha.1 → 0.1.0-alpha.13
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/README.md +790 -73
- package/dist/ApiKey.d.ts +207 -0
- package/dist/ApiKey.d.ts.map +1 -0
- package/dist/ApiKey.js +275 -0
- package/dist/ApiKey.js.map +1 -0
- package/dist/AuditLog.d.ts +335 -1
- package/dist/AuditLog.d.ts.map +1 -1
- package/dist/AuditLog.js +318 -1
- package/dist/AuditLog.js.map +1 -1
- package/dist/AuthConfig.d.ts +42 -0
- package/dist/AuthConfig.d.ts.map +1 -0
- package/dist/AuthConfig.js +43 -0
- package/dist/AuthConfig.js.map +1 -0
- package/dist/AuthFlow.d.ts +462 -36
- package/dist/AuthFlow.d.ts.map +1 -1
- package/dist/AuthFlow.js +657 -5
- package/dist/AuthFlow.js.map +1 -1
- package/dist/AuthKernel.d.ts +3 -0
- package/dist/AuthKernel.d.ts.map +1 -0
- package/dist/AuthKernel.js +9 -0
- package/dist/AuthKernel.js.map +1 -0
- package/dist/Challenge.d.ts +24 -4
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +167 -1
- package/dist/Challenge.js.map +1 -1
- package/dist/Client.d.ts +449 -0
- package/dist/Client.d.ts.map +1 -0
- package/dist/Client.js +985 -0
- package/dist/Client.js.map +1 -0
- package/dist/Crypto.d.ts +6 -1
- package/dist/Crypto.d.ts.map +1 -1
- package/dist/Crypto.js +62 -1
- package/dist/Crypto.js.map +1 -1
- package/dist/DomainVerification.d.ts +200 -0
- package/dist/DomainVerification.d.ts.map +1 -0
- package/dist/DomainVerification.js +304 -0
- package/dist/DomainVerification.js.map +1 -0
- package/dist/EffectQbSqliteStorage.d.ts +985 -0
- package/dist/EffectQbSqliteStorage.d.ts.map +1 -0
- package/dist/EffectQbSqliteStorage.js +2824 -0
- package/dist/EffectQbSqliteStorage.js.map +1 -0
- package/dist/EmailOtp.d.ts +120 -0
- package/dist/EmailOtp.d.ts.map +1 -0
- package/dist/EmailOtp.js +286 -0
- package/dist/EmailOtp.js.map +1 -0
- package/dist/EmailVerification.d.ts +121 -0
- package/dist/EmailVerification.d.ts.map +1 -0
- package/dist/EmailVerification.js +200 -0
- package/dist/EmailVerification.js.map +1 -0
- package/dist/HttpApi/Api.d.ts +2410 -0
- package/dist/HttpApi/Api.d.ts.map +1 -0
- package/dist/HttpApi/Api.js +3026 -0
- package/dist/HttpApi/Api.js.map +1 -0
- package/dist/HttpApi/Endpoints.d.ts +1073 -0
- package/dist/HttpApi/Endpoints.d.ts.map +1 -0
- package/dist/HttpApi/Endpoints.js +428 -0
- package/dist/HttpApi/Endpoints.js.map +1 -0
- package/dist/HttpApi/Errors.d.ts +114 -0
- package/dist/HttpApi/Errors.d.ts.map +1 -0
- package/dist/HttpApi/Errors.js +230 -0
- package/dist/HttpApi/Errors.js.map +1 -0
- package/dist/HttpApi/Schemas.d.ts +1254 -0
- package/dist/HttpApi/Schemas.d.ts.map +1 -0
- package/dist/HttpApi/Schemas.js +862 -0
- package/dist/HttpApi/Schemas.js.map +1 -0
- package/dist/HttpApi/Security.d.ts +159 -0
- package/dist/HttpApi/Security.d.ts.map +1 -0
- package/dist/HttpApi/Security.js +530 -0
- package/dist/HttpApi/Security.js.map +1 -0
- package/dist/HttpApi/Transport.d.ts +65 -0
- package/dist/HttpApi/Transport.d.ts.map +1 -0
- package/dist/HttpApi/Transport.js +311 -0
- package/dist/HttpApi/Transport.js.map +1 -0
- package/dist/HttpApi/index.d.ts +8 -0
- package/dist/HttpApi/index.d.ts.map +1 -0
- package/dist/HttpApi/index.js +7 -0
- package/dist/HttpApi/index.js.map +1 -0
- package/dist/HttpApi.d.ts +2 -0
- package/dist/HttpApi.d.ts.map +1 -0
- package/dist/HttpApi.js +2 -0
- package/dist/HttpApi.js.map +1 -0
- package/dist/Identifiers.d.ts +3 -0
- package/dist/Identifiers.d.ts.map +1 -1
- package/dist/Identifiers.js +2 -0
- package/dist/Identifiers.js.map +1 -1
- package/dist/IncidentAction.d.ts +80 -0
- package/dist/IncidentAction.d.ts.map +1 -0
- package/dist/IncidentAction.js +158 -0
- package/dist/IncidentAction.js.map +1 -0
- package/dist/Internal.d.ts +7 -0
- package/dist/Internal.d.ts.map +1 -0
- package/dist/Internal.js +12 -0
- package/dist/Internal.js.map +1 -0
- package/dist/Jwt.d.ts +281 -0
- package/dist/Jwt.d.ts.map +1 -0
- package/dist/Jwt.js +721 -0
- package/dist/Jwt.js.map +1 -0
- package/dist/LoginApproval.d.ts +314 -0
- package/dist/LoginApproval.d.ts.map +1 -0
- package/dist/LoginApproval.js +551 -0
- package/dist/LoginApproval.js.map +1 -0
- package/dist/LoginNotification.d.ts +183 -0
- package/dist/LoginNotification.d.ts.map +1 -0
- package/dist/LoginNotification.js +329 -0
- package/dist/LoginNotification.js.map +1 -0
- package/dist/LoginRisk.d.ts +344 -0
- package/dist/LoginRisk.d.ts.map +1 -0
- package/dist/LoginRisk.js +484 -0
- package/dist/LoginRisk.js.map +1 -0
- package/dist/MachineAuth.d.ts +73 -0
- package/dist/MachineAuth.d.ts.map +1 -0
- package/dist/MachineAuth.js +147 -0
- package/dist/MachineAuth.js.map +1 -0
- package/dist/MagicLink.d.ts +85 -0
- package/dist/MagicLink.d.ts.map +1 -0
- package/dist/MagicLink.js +206 -0
- package/dist/MagicLink.js.map +1 -0
- package/dist/Mailer.d.ts +147 -0
- package/dist/Mailer.d.ts.map +1 -0
- package/dist/Mailer.js +208 -0
- package/dist/Mailer.js.map +1 -0
- package/dist/OAuth.d.ts +1890 -0
- package/dist/OAuth.d.ts.map +1 -0
- package/dist/OAuth.js +3063 -0
- package/dist/OAuth.js.map +1 -0
- package/dist/Passkey.d.ts +334 -0
- package/dist/Passkey.d.ts.map +1 -0
- package/dist/Passkey.js +422 -0
- package/dist/Passkey.js.map +1 -0
- package/dist/PasskeySimpleWebAuthn.d.ts +11 -0
- package/dist/PasskeySimpleWebAuthn.d.ts.map +1 -0
- package/dist/PasskeySimpleWebAuthn.js +181 -0
- package/dist/PasskeySimpleWebAuthn.js.map +1 -0
- package/dist/Password.d.ts +237 -0
- package/dist/Password.d.ts.map +1 -0
- package/dist/Password.js +641 -0
- package/dist/Password.js.map +1 -0
- package/dist/Policy.d.ts +7 -4
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +28 -5
- package/dist/Policy.js.map +1 -1
- package/dist/Privacy.d.ts +15 -0
- package/dist/Privacy.d.ts.map +1 -1
- package/dist/Privacy.js +58 -1
- package/dist/Privacy.js.map +1 -1
- package/dist/RateLimiter.d.ts +104 -0
- package/dist/RateLimiter.d.ts.map +1 -0
- package/dist/RateLimiter.js +118 -0
- package/dist/RateLimiter.js.map +1 -0
- package/dist/RecoveryCode.d.ts +169 -0
- package/dist/RecoveryCode.d.ts.map +1 -0
- package/dist/RecoveryCode.js +299 -0
- package/dist/RecoveryCode.js.map +1 -0
- package/dist/RefreshToken.d.ts +222 -0
- package/dist/RefreshToken.d.ts.map +1 -0
- package/dist/RefreshToken.js +342 -0
- package/dist/RefreshToken.js.map +1 -0
- package/dist/Retention.d.ts +98 -0
- package/dist/Retention.d.ts.map +1 -0
- package/dist/Retention.js +204 -0
- package/dist/Retention.js.map +1 -0
- package/dist/SecurityTimeline.d.ts +78 -0
- package/dist/SecurityTimeline.d.ts.map +1 -0
- package/dist/SecurityTimeline.js +461 -0
- package/dist/SecurityTimeline.js.map +1 -0
- package/dist/Sessions.d.ts +123 -11
- package/dist/Sessions.d.ts.map +1 -1
- package/dist/Sessions.js +534 -1
- package/dist/Sessions.js.map +1 -1
- package/dist/StepUp.d.ts +44 -0
- package/dist/StepUp.d.ts.map +1 -0
- package/dist/StepUp.js +82 -0
- package/dist/StepUp.js.map +1 -0
- package/dist/Storage.d.ts +73 -5
- package/dist/Storage.d.ts.map +1 -1
- package/dist/Storage.js +11 -1
- package/dist/Storage.js.map +1 -1
- package/dist/StorageMigrations.d.ts +23 -1
- package/dist/StorageMigrations.d.ts.map +1 -1
- package/dist/StorageMigrations.js +462 -2
- package/dist/StorageMigrations.js.map +1 -1
- package/dist/Testing.d.ts +9 -1
- package/dist/Testing.d.ts.map +1 -1
- package/dist/Testing.js +30 -2
- package/dist/Testing.js.map +1 -1
- package/dist/Totp.d.ts +220 -0
- package/dist/Totp.d.ts.map +1 -0
- package/dist/Totp.js +477 -0
- package/dist/Totp.js.map +1 -0
- package/dist/TrustedDevice.d.ts +127 -0
- package/dist/TrustedDevice.d.ts.map +1 -0
- package/dist/TrustedDevice.js +252 -0
- package/dist/TrustedDevice.js.map +1 -0
- package/dist/WaitUntil.d.ts.map +1 -1
- package/dist/WaitUntil.js +1 -1
- package/dist/WaitUntil.js.map +1 -1
- package/dist/Webhook.d.ts +380 -0
- package/dist/Webhook.d.ts.map +1 -0
- package/dist/Webhook.js +592 -0
- package/dist/Webhook.js.map +1 -0
- package/dist/alchemy/cloudflare/Email.d.ts +25 -0
- package/dist/alchemy/cloudflare/Email.d.ts.map +1 -0
- package/dist/alchemy/cloudflare/Email.js +117 -0
- package/dist/alchemy/cloudflare/Email.js.map +1 -0
- package/dist/alchemy/cloudflare/RateLimitDurableObject.d.ts +34 -0
- package/dist/alchemy/cloudflare/RateLimitDurableObject.d.ts.map +1 -0
- package/dist/alchemy/cloudflare/RateLimitDurableObject.js +30 -0
- package/dist/alchemy/cloudflare/RateLimitDurableObject.js.map +1 -0
- package/dist/cloudflare/Email.d.ts +25 -0
- package/dist/cloudflare/Email.d.ts.map +1 -0
- package/dist/cloudflare/Email.js +153 -0
- package/dist/cloudflare/Email.js.map +1 -0
- package/dist/cloudflare/RateLimitDurableObject.d.ts +50 -0
- package/dist/cloudflare/RateLimitDurableObject.d.ts.map +1 -0
- package/dist/cloudflare/RateLimitDurableObject.js +51 -0
- package/dist/cloudflare/RateLimitDurableObject.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/RateLimitDurableObject.d.ts +32 -0
- package/dist/internal/RateLimitDurableObject.d.ts.map +1 -0
- package/dist/internal/RateLimitDurableObject.js +80 -0
- package/dist/internal/RateLimitDurableObject.js.map +1 -0
- package/migrations/0002_auth_password.sql +25 -0
- package/migrations/0003_auth_login_approval_review.sql +22 -0
- package/migrations/0004_auth_trusted_device.sql +12 -0
- package/migrations/0005_auth_passkey.sql +17 -0
- package/migrations/0006_auth_totp_factor.sql +17 -0
- package/migrations/0007_auth_recovery_code.sql +13 -0
- package/migrations/0008_auth_api_key.sql +17 -0
- package/migrations/0009_auth_refresh_token.sql +21 -0
- package/migrations/0010_auth_jwt_revocation.sql +8 -0
- package/migrations/0011_auth_oauth_account.sql +16 -0
- package/migrations/0012_auth_oauth_provider_token_vault.sql +20 -0
- package/migrations/0013_auth_security_timeline.sql +16 -0
- package/migrations/0014_auth_webhook_outbox.sql +16 -0
- package/migrations/0015_auth_webhook_replay.sql +7 -0
- package/migrations/0016_auth_login_risk_history.sql +23 -0
- package/migrations/0017_auth_audit_log.sql +17 -0
- package/migrations/0018_auth_domain_verification.sql +18 -0
- package/migrations/0019_auth_oauth_authorization_code.sql +17 -0
- package/migrations/0020_auth_oauth_provider_mode_token.sql +24 -0
- package/migrations/0021_auth_oauth_client_secret.sql +14 -0
- package/migrations/0022_auth_oauth_client.sql +15 -0
- package/migrations/0023_auth_oauth_consent.sql +15 -0
- package/package.json +162 -26
- package/dist/D1Kysely.d.ts +0 -40
- package/dist/D1Kysely.d.ts.map +0 -1
- package/dist/D1Kysely.js +0 -75
- package/dist/D1Kysely.js.map +0 -1
- package/dist/KyselyStorage.d.ts +0 -41
- package/dist/KyselyStorage.d.ts.map +0 -1
- package/dist/KyselyStorage.js +0 -176
- package/dist/KyselyStorage.js.map +0 -1
package/dist/Jwt.js
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
import { Brand, Context, Data, Duration, Effect, Encoding, Layer, Option, Redacted, Result, } from "effect";
|
|
2
|
+
import { Crypto } from "./Crypto.js";
|
|
3
|
+
import { UnixMillis } from "./Identifiers.js";
|
|
4
|
+
import { currentUnixMillis, timingSafeEqualString } from "./Internal.js";
|
|
5
|
+
export const JwtKeyId = Brand.nominal();
|
|
6
|
+
export const JwtToken = Brand.nominal();
|
|
7
|
+
export const JwtAlgorithms = ["HS256", "RS256", "ES256", "EdDSA"];
|
|
8
|
+
export class JwtError extends Data.TaggedError("JwtError") {
|
|
9
|
+
}
|
|
10
|
+
export class JwtKeys extends Context.Service()("auth/JwtKeys") {
|
|
11
|
+
}
|
|
12
|
+
(function (JwtKeys) {
|
|
13
|
+
JwtKeys.make = (service) => JwtKeys.of(service);
|
|
14
|
+
})(JwtKeys || (JwtKeys = {}));
|
|
15
|
+
export class JwtIssuer extends Context.Service()("auth/JwtIssuer") {
|
|
16
|
+
}
|
|
17
|
+
(function (JwtIssuer) {
|
|
18
|
+
JwtIssuer.make = (service) => JwtIssuer.of(service);
|
|
19
|
+
})(JwtIssuer || (JwtIssuer = {}));
|
|
20
|
+
export class JwtVerifier extends Context.Service()("auth/JwtVerifier") {
|
|
21
|
+
}
|
|
22
|
+
(function (JwtVerifier) {
|
|
23
|
+
JwtVerifier.make = (service) => JwtVerifier.of(service);
|
|
24
|
+
})(JwtVerifier || (JwtVerifier = {}));
|
|
25
|
+
export class JwtSignature extends Context.Service()("auth/JwtSignature") {
|
|
26
|
+
}
|
|
27
|
+
(function (JwtSignature) {
|
|
28
|
+
JwtSignature.make = (service) => JwtSignature.of(service);
|
|
29
|
+
})(JwtSignature || (JwtSignature = {}));
|
|
30
|
+
export class Jwks extends Context.Service()("auth/Jwks") {
|
|
31
|
+
}
|
|
32
|
+
(function (Jwks) {
|
|
33
|
+
Jwks.make = (service) => Jwks.of(service);
|
|
34
|
+
})(Jwks || (Jwks = {}));
|
|
35
|
+
export class JwtRevocationStore extends Context.Service()("auth/JwtRevocationStore") {
|
|
36
|
+
}
|
|
37
|
+
(function (JwtRevocationStore) {
|
|
38
|
+
JwtRevocationStore.make = (service) => JwtRevocationStore.of(service);
|
|
39
|
+
})(JwtRevocationStore || (JwtRevocationStore = {}));
|
|
40
|
+
export class JwtRevocation extends Context.Service()("auth/JwtRevocation") {
|
|
41
|
+
}
|
|
42
|
+
(function (JwtRevocation) {
|
|
43
|
+
JwtRevocation.make = (service) => JwtRevocation.of(service);
|
|
44
|
+
})(JwtRevocation || (JwtRevocation = {}));
|
|
45
|
+
const textEncoder = new TextEncoder();
|
|
46
|
+
const textDecoder = new TextDecoder();
|
|
47
|
+
const jwtError = (operation, message, cause) => new JwtError({
|
|
48
|
+
operation,
|
|
49
|
+
message,
|
|
50
|
+
...(cause === undefined ? {} : { cause }),
|
|
51
|
+
});
|
|
52
|
+
const isJwtAlgorithm = (value) => typeof value === "string" && JwtAlgorithms.includes(value);
|
|
53
|
+
const signatureService = (dependencies) => {
|
|
54
|
+
if (dependencies.signature !== undefined) {
|
|
55
|
+
return dependencies.signature;
|
|
56
|
+
}
|
|
57
|
+
if (dependencies.crypto !== undefined) {
|
|
58
|
+
return makeJwtSignature({ crypto: dependencies.crypto });
|
|
59
|
+
}
|
|
60
|
+
throw jwtError("signature", "JWT signature service is not available");
|
|
61
|
+
};
|
|
62
|
+
const keyInfo = (key) => ({
|
|
63
|
+
id: key.id,
|
|
64
|
+
alg: key.alg,
|
|
65
|
+
status: key.status,
|
|
66
|
+
...(key.createdAt === undefined ? {} : { createdAt: key.createdAt }),
|
|
67
|
+
...(key.expiresAt === undefined ? {} : { expiresAt: key.expiresAt }),
|
|
68
|
+
...(key.metadata === undefined ? {} : { metadata: key.metadata }),
|
|
69
|
+
});
|
|
70
|
+
const numericDate = (millis) => Math.floor(Number(millis) / 1000);
|
|
71
|
+
const addDuration = (millis, duration) => UnixMillis(Number(millis) + Duration.toMillis(duration));
|
|
72
|
+
const isUsableAt = (key, now) => now === undefined || key.expiresAt === undefined || Number(now) < Number(key.expiresAt);
|
|
73
|
+
const isSigningKey = (key, alg, now) => key.alg === alg &&
|
|
74
|
+
key.status === "active" &&
|
|
75
|
+
(key.alg !== "HS256" || key.secret !== undefined) &&
|
|
76
|
+
isUsableAt(key, now);
|
|
77
|
+
const isVerificationKey = (key, input) => key.id === input.kid &&
|
|
78
|
+
key.status !== "disabled" &&
|
|
79
|
+
(key.alg !== "HS256" || key.secret !== undefined) &&
|
|
80
|
+
(input.alg === undefined || key.alg === input.alg) &&
|
|
81
|
+
isUsableAt(key, input.now);
|
|
82
|
+
const privateJwkMembers = new Set([
|
|
83
|
+
"d",
|
|
84
|
+
"p",
|
|
85
|
+
"q",
|
|
86
|
+
"dp",
|
|
87
|
+
"dq",
|
|
88
|
+
"qi",
|
|
89
|
+
"oth",
|
|
90
|
+
"k",
|
|
91
|
+
]);
|
|
92
|
+
const publicJwkFromKey = (key) => {
|
|
93
|
+
if (key.status === "disabled" || key.publicJwk === undefined) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const jwk = {};
|
|
97
|
+
for (const [name, value] of Object.entries(key.publicJwk)) {
|
|
98
|
+
if (!privateJwkMembers.has(name) && value !== undefined) {
|
|
99
|
+
jwk[name] = value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
...jwk,
|
|
104
|
+
kty: String(jwk.kty ?? key.publicJwk.kty),
|
|
105
|
+
kid: String(jwk.kid ?? key.id),
|
|
106
|
+
alg: String(jwk.alg ?? key.alg),
|
|
107
|
+
use: String(jwk.use ?? "sig"),
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
export const makeJwtKeysMemory = (records) => {
|
|
111
|
+
const keys = records.map((record) => ({ ...record }));
|
|
112
|
+
return JwtKeys.make({
|
|
113
|
+
selectSigningKey: (input) => {
|
|
114
|
+
const alg = input?.alg ?? "HS256";
|
|
115
|
+
const key = keys.find((record) => isSigningKey(record, alg, input?.now));
|
|
116
|
+
return Effect.succeed(key === undefined ? Option.none() : Option.some(key));
|
|
117
|
+
},
|
|
118
|
+
findVerificationKey: (input) => {
|
|
119
|
+
const key = keys.find((record) => isVerificationKey(record, input));
|
|
120
|
+
return Effect.succeed(key === undefined ? Option.none() : Option.some(key));
|
|
121
|
+
},
|
|
122
|
+
listPublicJwks: (input) => Effect.succeed(keys
|
|
123
|
+
.filter((key) => isUsableAt(key, input?.now))
|
|
124
|
+
.flatMap((key) => {
|
|
125
|
+
const publicJwk = publicJwkFromKey(key);
|
|
126
|
+
return publicJwk === undefined ? [] : [publicJwk];
|
|
127
|
+
})),
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
export const JwtKeysMemoryLive = (records) => Layer.succeed(JwtKeys)(makeJwtKeysMemory(records));
|
|
131
|
+
const cloneRevocationRecord = (record) => ({ ...record });
|
|
132
|
+
export const makeJwtRevocationStoreMemory = () => {
|
|
133
|
+
const records = new Map();
|
|
134
|
+
return JwtRevocationStore.make({
|
|
135
|
+
revoke: (record) => Effect.sync(() => {
|
|
136
|
+
records.set(record.jwtId, cloneRevocationRecord(record));
|
|
137
|
+
}),
|
|
138
|
+
find: (jwtId) => Effect.sync(() => {
|
|
139
|
+
const record = records.get(jwtId);
|
|
140
|
+
return record === undefined
|
|
141
|
+
? Option.none()
|
|
142
|
+
: Option.some(cloneRevocationRecord(record));
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
export const JwtRevocationStoreMemoryLive = Layer.sync(JwtRevocationStore)(makeJwtRevocationStoreMemory);
|
|
147
|
+
const normalizedJwtId = (jwtId) => {
|
|
148
|
+
const value = jwtId.trim();
|
|
149
|
+
return value.length === 0
|
|
150
|
+
? Effect.fail(jwtError("revocation", "JWT ID must not be empty"))
|
|
151
|
+
: Effect.succeed(value);
|
|
152
|
+
};
|
|
153
|
+
const revocationRecordActiveAt = (record, now) => record.expiresAt === undefined || Number(now) < Number(record.expiresAt);
|
|
154
|
+
const jwtIdFromClaims = (claims) => {
|
|
155
|
+
const jwtId = typeof claims.jti === "string" ? claims.jti.trim() : "";
|
|
156
|
+
return jwtId.length === 0 ? undefined : jwtId;
|
|
157
|
+
};
|
|
158
|
+
const expiresAtFromClaims = (claims) => typeof claims.exp === "number" ? UnixMillis(claims.exp * 1000) : undefined;
|
|
159
|
+
export const makeJwtRevocation = (dependencies) => JwtRevocation.make({
|
|
160
|
+
revokeJwtId: Effect.fn("auth.jwt.revocation.revoke_jwt_id")(function* (input) {
|
|
161
|
+
const jwtId = yield* normalizedJwtId(input.jwtId);
|
|
162
|
+
const revokedAt = input.revokedAt ?? (yield* currentUnixMillis);
|
|
163
|
+
const record = {
|
|
164
|
+
jwtId,
|
|
165
|
+
revokedAt,
|
|
166
|
+
...(input.expiresAt === undefined ? {} : { expiresAt: input.expiresAt }),
|
|
167
|
+
...(input.reason === undefined ? {} : { reason: input.reason }),
|
|
168
|
+
};
|
|
169
|
+
yield* dependencies.store.revoke(record);
|
|
170
|
+
return record;
|
|
171
|
+
}),
|
|
172
|
+
isRevoked: Effect.fn("auth.jwt.revocation.is_revoked")(function* (input) {
|
|
173
|
+
const jwtId = yield* normalizedJwtId(input.jwtId);
|
|
174
|
+
const now = input.now ?? (yield* currentUnixMillis);
|
|
175
|
+
const record = yield* dependencies.store.find(jwtId);
|
|
176
|
+
return Option.isSome(record) && revocationRecordActiveAt(record.value, now);
|
|
177
|
+
}),
|
|
178
|
+
introspect: Effect.fn("auth.jwt.revocation.introspect")(function* (input) {
|
|
179
|
+
const verified = yield* dependencies.verifier.verify(input);
|
|
180
|
+
if (!verified.valid) {
|
|
181
|
+
return {
|
|
182
|
+
active: false,
|
|
183
|
+
reason: verified.reason,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const jwtId = jwtIdFromClaims(verified.claims);
|
|
187
|
+
const expiresAt = expiresAtFromClaims(verified.claims);
|
|
188
|
+
if (jwtId !== undefined) {
|
|
189
|
+
const now = input.now ?? (yield* currentUnixMillis);
|
|
190
|
+
const record = yield* dependencies.store.find(jwtId);
|
|
191
|
+
if (Option.isSome(record) && revocationRecordActiveAt(record.value, now)) {
|
|
192
|
+
return {
|
|
193
|
+
active: false,
|
|
194
|
+
reason: "revoked",
|
|
195
|
+
jwtId,
|
|
196
|
+
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
197
|
+
revokedAt: record.value.revokedAt,
|
|
198
|
+
...(record.value.reason === undefined
|
|
199
|
+
? {}
|
|
200
|
+
: { revokedReason: record.value.reason }),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
active: true,
|
|
206
|
+
header: verified.header,
|
|
207
|
+
claims: verified.claims,
|
|
208
|
+
key: verified.key,
|
|
209
|
+
...(jwtId === undefined ? {} : { jwtId }),
|
|
210
|
+
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
211
|
+
revoked: false,
|
|
212
|
+
};
|
|
213
|
+
}),
|
|
214
|
+
});
|
|
215
|
+
const encodeJson = (value) => Encoding.encodeBase64Url(textEncoder.encode(JSON.stringify(value)));
|
|
216
|
+
const decodeJsonObject = (value) => {
|
|
217
|
+
const bytes = Result.match(Encoding.decodeBase64Url(value), {
|
|
218
|
+
onSuccess: (decoded) => decoded,
|
|
219
|
+
onFailure: () => undefined,
|
|
220
|
+
});
|
|
221
|
+
if (bytes === undefined) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const parsed = JSON.parse(textDecoder.decode(bytes));
|
|
226
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
227
|
+
? parsed
|
|
228
|
+
: undefined;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const decodeHeader = (part) => {
|
|
235
|
+
const value = decodeJsonObject(part);
|
|
236
|
+
if (value === undefined || typeof value.kid !== "string") {
|
|
237
|
+
return "malformed";
|
|
238
|
+
}
|
|
239
|
+
if (!isJwtAlgorithm(value.alg)) {
|
|
240
|
+
return "unsupported_alg";
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
typ: "JWT",
|
|
244
|
+
alg: value.alg,
|
|
245
|
+
kid: JwtKeyId(value.kid),
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
const decodeClaims = (part) => {
|
|
249
|
+
const value = decodeJsonObject(part);
|
|
250
|
+
return value === undefined ? undefined : value;
|
|
251
|
+
};
|
|
252
|
+
const signHs256 = (crypto, key, data, operation) => crypto.hmacSha256({ key, data }).pipe(Effect.mapError((cause) => jwtError(operation, "Failed to calculate JWT signature", cause)));
|
|
253
|
+
const getJwtWebCrypto = (options) => {
|
|
254
|
+
const crypto = options?.crypto ?? globalThis.crypto;
|
|
255
|
+
if (crypto === undefined) {
|
|
256
|
+
throw jwtError("signature", "Web Crypto is not available");
|
|
257
|
+
}
|
|
258
|
+
return crypto;
|
|
259
|
+
};
|
|
260
|
+
const rs256ImportAlgorithm = {
|
|
261
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
262
|
+
hash: "SHA-256",
|
|
263
|
+
};
|
|
264
|
+
const es256ImportAlgorithm = {
|
|
265
|
+
name: "ECDSA",
|
|
266
|
+
namedCurve: "P-256",
|
|
267
|
+
};
|
|
268
|
+
const es256SignAlgorithm = {
|
|
269
|
+
name: "ECDSA",
|
|
270
|
+
hash: "SHA-256",
|
|
271
|
+
};
|
|
272
|
+
const ed25519ImportAlgorithm = {
|
|
273
|
+
name: "Ed25519",
|
|
274
|
+
};
|
|
275
|
+
const jwkForImport = (jwk) => ({ ...jwk });
|
|
276
|
+
const decodeBase64UrlBytes = (value) => Result.match(Encoding.decodeBase64Url(value), {
|
|
277
|
+
onSuccess: (bytes) => bytes,
|
|
278
|
+
onFailure: () => undefined,
|
|
279
|
+
});
|
|
280
|
+
const readDerLength = (signature, offset) => {
|
|
281
|
+
const first = signature[offset];
|
|
282
|
+
if (first === undefined) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
if (first < 0x80) {
|
|
286
|
+
return [first, offset + 1];
|
|
287
|
+
}
|
|
288
|
+
const byteLength = first & 0x7f;
|
|
289
|
+
if (byteLength === 0 || byteLength > 2 || offset + byteLength >= signature.length) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
let length = 0;
|
|
293
|
+
for (let index = 0; index < byteLength; index += 1) {
|
|
294
|
+
length = (length << 8) | (signature[offset + 1 + index] ?? 0);
|
|
295
|
+
}
|
|
296
|
+
return [length, offset + 1 + byteLength];
|
|
297
|
+
};
|
|
298
|
+
const normalizeDerInteger = (value, byteLength) => {
|
|
299
|
+
let offset = 0;
|
|
300
|
+
while (offset < value.length - 1 && value[offset] === 0) {
|
|
301
|
+
offset += 1;
|
|
302
|
+
}
|
|
303
|
+
const normalized = value.slice(offset);
|
|
304
|
+
if (normalized.length > byteLength) {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
const output = new Uint8Array(byteLength);
|
|
308
|
+
output.set(normalized, byteLength - normalized.length);
|
|
309
|
+
return output;
|
|
310
|
+
};
|
|
311
|
+
const derEcdsaSignatureToJose = (signature, byteLength) => {
|
|
312
|
+
if (signature.length === byteLength * 2) {
|
|
313
|
+
return signature;
|
|
314
|
+
}
|
|
315
|
+
if (signature[0] !== 0x30) {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
const sequenceLength = readDerLength(signature, 1);
|
|
319
|
+
if (sequenceLength === undefined) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
const [length, sequenceOffset] = sequenceLength;
|
|
323
|
+
let offset = sequenceOffset;
|
|
324
|
+
if (offset + length !== signature.length || signature[offset] !== 0x02) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
const rLength = readDerLength(signature, offset + 1);
|
|
328
|
+
if (rLength === undefined) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
const [rSize, rOffset] = rLength;
|
|
332
|
+
const r = normalizeDerInteger(signature.slice(rOffset, rOffset + rSize), byteLength);
|
|
333
|
+
offset = rOffset + rSize;
|
|
334
|
+
if (r === undefined || signature[offset] !== 0x02) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
const sLength = readDerLength(signature, offset + 1);
|
|
338
|
+
if (sLength === undefined) {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
const [sSize, sOffset] = sLength;
|
|
342
|
+
const s = normalizeDerInteger(signature.slice(sOffset, sOffset + sSize), byteLength);
|
|
343
|
+
if (s === undefined || sOffset + sSize !== signature.length) {
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
const output = new Uint8Array(byteLength * 2);
|
|
347
|
+
output.set(r, 0);
|
|
348
|
+
output.set(s, byteLength);
|
|
349
|
+
return output;
|
|
350
|
+
};
|
|
351
|
+
const derInteger = (value) => {
|
|
352
|
+
let offset = 0;
|
|
353
|
+
while (offset < value.length - 1 && value[offset] === 0) {
|
|
354
|
+
offset += 1;
|
|
355
|
+
}
|
|
356
|
+
const normalized = value.slice(offset);
|
|
357
|
+
const needsPadding = (normalized[0] ?? 0) >= 0x80;
|
|
358
|
+
const output = new Uint8Array(normalized.length + (needsPadding ? 3 : 2));
|
|
359
|
+
output[0] = 0x02;
|
|
360
|
+
output[1] = normalized.length + (needsPadding ? 1 : 0);
|
|
361
|
+
if (needsPadding) {
|
|
362
|
+
output[2] = 0;
|
|
363
|
+
output.set(normalized, 3);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
output.set(normalized, 2);
|
|
367
|
+
}
|
|
368
|
+
return output;
|
|
369
|
+
};
|
|
370
|
+
const joseEcdsaSignatureToDer = (signature, byteLength) => {
|
|
371
|
+
if (signature.length !== byteLength * 2) {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
const r = derInteger(signature.slice(0, byteLength));
|
|
375
|
+
const s = derInteger(signature.slice(byteLength));
|
|
376
|
+
const payloadLength = r.length + s.length;
|
|
377
|
+
const output = new Uint8Array(2 + payloadLength);
|
|
378
|
+
output[0] = 0x30;
|
|
379
|
+
output[1] = payloadLength;
|
|
380
|
+
output.set(r, 2);
|
|
381
|
+
output.set(s, 2 + r.length);
|
|
382
|
+
return output;
|
|
383
|
+
};
|
|
384
|
+
export const makeJwtSignature = (dependencies) => JwtSignature.make({
|
|
385
|
+
supports: (alg) => alg === "HS256",
|
|
386
|
+
canSign: (input) => input.alg === "HS256" && input.key.secret !== undefined,
|
|
387
|
+
canVerify: (input) => input.alg === "HS256" && input.key.secret !== undefined,
|
|
388
|
+
sign: Effect.fn("auth.jwt.signature.sign")(function* (input) {
|
|
389
|
+
if (input.alg !== "HS256" || input.key.secret === undefined) {
|
|
390
|
+
return yield* jwtError("signature", "JWT signature algorithm is not supported");
|
|
391
|
+
}
|
|
392
|
+
return yield* signHs256(dependencies.crypto, input.key.secret, input.signingInput, "signature");
|
|
393
|
+
}),
|
|
394
|
+
verify: Effect.fn("auth.jwt.signature.verify")(function* (input) {
|
|
395
|
+
if (input.alg !== "HS256" || input.key.secret === undefined) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
const expectedSignature = yield* signHs256(dependencies.crypto, input.key.secret, input.signingInput, "signature");
|
|
399
|
+
return yield* timingSafeEqualString(dependencies.crypto, expectedSignature, input.signature).pipe(Effect.mapError((cause) => jwtError("signature", "Failed to compare JWT signature", cause)));
|
|
400
|
+
}),
|
|
401
|
+
});
|
|
402
|
+
export const makeJwtWebCryptoRs256Signature = (options) => JwtSignature.make({
|
|
403
|
+
supports: (alg) => alg === "RS256",
|
|
404
|
+
canSign: (input) => input.alg === "RS256" && input.key.privateJwk !== undefined,
|
|
405
|
+
canVerify: (input) => input.alg === "RS256" && input.key.publicJwk !== undefined,
|
|
406
|
+
sign: Effect.fn("auth.jwt.signature.webcrypto_rs256.sign")(function* (input) {
|
|
407
|
+
const { privateJwk } = input.key;
|
|
408
|
+
if (input.alg !== "RS256" || privateJwk === undefined) {
|
|
409
|
+
return yield* jwtError("signature", "JWT RS256 private key is not available");
|
|
410
|
+
}
|
|
411
|
+
return yield* Effect.tryPromise({
|
|
412
|
+
try: async () => {
|
|
413
|
+
const crypto = getJwtWebCrypto(options);
|
|
414
|
+
const key = await crypto.subtle.importKey("jwk", jwkForImport(privateJwk), rs256ImportAlgorithm, false, ["sign"]);
|
|
415
|
+
const signature = await crypto.subtle.sign(rs256ImportAlgorithm, key, textEncoder.encode(input.signingInput));
|
|
416
|
+
return Encoding.encodeBase64Url(new Uint8Array(signature));
|
|
417
|
+
},
|
|
418
|
+
catch: (cause) => jwtError("signature", "Failed to sign RS256 JWT", cause),
|
|
419
|
+
});
|
|
420
|
+
}),
|
|
421
|
+
verify: Effect.fn("auth.jwt.signature.webcrypto_rs256.verify")(function* (input) {
|
|
422
|
+
const { publicJwk } = input.key;
|
|
423
|
+
if (input.alg !== "RS256" || publicJwk === undefined) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
const signature = decodeBase64UrlBytes(input.signature);
|
|
427
|
+
if (signature === undefined) {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
return yield* Effect.tryPromise({
|
|
431
|
+
try: async () => {
|
|
432
|
+
const crypto = getJwtWebCrypto(options);
|
|
433
|
+
const key = await crypto.subtle.importKey("jwk", jwkForImport(publicJwk), rs256ImportAlgorithm, false, ["verify"]);
|
|
434
|
+
return await crypto.subtle.verify(rs256ImportAlgorithm, key, signature, textEncoder.encode(input.signingInput));
|
|
435
|
+
},
|
|
436
|
+
catch: (cause) => jwtError("signature", "Failed to verify RS256 JWT", cause),
|
|
437
|
+
});
|
|
438
|
+
}),
|
|
439
|
+
});
|
|
440
|
+
export const makeJwtWebCryptoEs256Signature = (options) => JwtSignature.make({
|
|
441
|
+
supports: (alg) => alg === "ES256",
|
|
442
|
+
canSign: (input) => input.alg === "ES256" && input.key.privateJwk !== undefined,
|
|
443
|
+
canVerify: (input) => input.alg === "ES256" && input.key.publicJwk !== undefined,
|
|
444
|
+
sign: Effect.fn("auth.jwt.signature.webcrypto_es256.sign")(function* (input) {
|
|
445
|
+
const { privateJwk } = input.key;
|
|
446
|
+
if (input.alg !== "ES256" || privateJwk === undefined) {
|
|
447
|
+
return yield* jwtError("signature", "JWT ES256 private key is not available");
|
|
448
|
+
}
|
|
449
|
+
return yield* Effect.tryPromise({
|
|
450
|
+
try: async () => {
|
|
451
|
+
const crypto = getJwtWebCrypto(options);
|
|
452
|
+
const key = await crypto.subtle.importKey("jwk", jwkForImport(privateJwk), es256ImportAlgorithm, false, ["sign"]);
|
|
453
|
+
const signature = new Uint8Array(await crypto.subtle.sign(es256SignAlgorithm, key, textEncoder.encode(input.signingInput)));
|
|
454
|
+
const joseSignature = derEcdsaSignatureToJose(signature, 32);
|
|
455
|
+
if (joseSignature === undefined) {
|
|
456
|
+
throw new Error("Invalid ES256 signature encoding");
|
|
457
|
+
}
|
|
458
|
+
return Encoding.encodeBase64Url(joseSignature);
|
|
459
|
+
},
|
|
460
|
+
catch: (cause) => jwtError("signature", "Failed to sign ES256 JWT", cause),
|
|
461
|
+
});
|
|
462
|
+
}),
|
|
463
|
+
verify: Effect.fn("auth.jwt.signature.webcrypto_es256.verify")(function* (input) {
|
|
464
|
+
const { publicJwk } = input.key;
|
|
465
|
+
if (input.alg !== "ES256" || publicJwk === undefined) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
const signature = decodeBase64UrlBytes(input.signature);
|
|
469
|
+
if (signature === undefined || signature.length !== 64) {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
return yield* Effect.tryPromise({
|
|
473
|
+
try: async () => {
|
|
474
|
+
const crypto = getJwtWebCrypto(options);
|
|
475
|
+
const key = await crypto.subtle.importKey("jwk", jwkForImport(publicJwk), es256ImportAlgorithm, false, ["verify"]);
|
|
476
|
+
const signingInput = textEncoder.encode(input.signingInput);
|
|
477
|
+
try {
|
|
478
|
+
if (await crypto.subtle.verify(es256SignAlgorithm, key, signature, signingInput)) {
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
// Some WebCrypto implementations expect DER-encoded ECDSA signatures.
|
|
484
|
+
}
|
|
485
|
+
const derSignature = joseEcdsaSignatureToDer(signature, 32);
|
|
486
|
+
return derSignature === undefined
|
|
487
|
+
? false
|
|
488
|
+
: await crypto.subtle.verify(es256SignAlgorithm, key, derSignature, signingInput);
|
|
489
|
+
},
|
|
490
|
+
catch: (cause) => jwtError("signature", "Failed to verify ES256 JWT", cause),
|
|
491
|
+
});
|
|
492
|
+
}),
|
|
493
|
+
});
|
|
494
|
+
export const makeJwtWebCryptoEdDsaSignature = (options) => JwtSignature.make({
|
|
495
|
+
supports: (alg) => alg === "EdDSA",
|
|
496
|
+
canSign: (input) => input.alg === "EdDSA" && input.key.privateJwk !== undefined,
|
|
497
|
+
canVerify: (input) => input.alg === "EdDSA" && input.key.publicJwk !== undefined,
|
|
498
|
+
sign: Effect.fn("auth.jwt.signature.webcrypto_eddsa.sign")(function* (input) {
|
|
499
|
+
const { privateJwk } = input.key;
|
|
500
|
+
if (input.alg !== "EdDSA" || privateJwk === undefined) {
|
|
501
|
+
return yield* jwtError("signature", "JWT EdDSA private key is not available");
|
|
502
|
+
}
|
|
503
|
+
return yield* Effect.tryPromise({
|
|
504
|
+
try: async () => {
|
|
505
|
+
const crypto = getJwtWebCrypto(options);
|
|
506
|
+
const key = await crypto.subtle.importKey("jwk", jwkForImport(privateJwk), ed25519ImportAlgorithm, false, ["sign"]);
|
|
507
|
+
const signature = await crypto.subtle.sign(ed25519ImportAlgorithm, key, textEncoder.encode(input.signingInput));
|
|
508
|
+
return Encoding.encodeBase64Url(new Uint8Array(signature));
|
|
509
|
+
},
|
|
510
|
+
catch: (cause) => jwtError("signature", "Failed to sign EdDSA JWT", cause),
|
|
511
|
+
});
|
|
512
|
+
}),
|
|
513
|
+
verify: Effect.fn("auth.jwt.signature.webcrypto_eddsa.verify")(function* (input) {
|
|
514
|
+
const { publicJwk } = input.key;
|
|
515
|
+
if (input.alg !== "EdDSA" || publicJwk === undefined) {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
const signature = decodeBase64UrlBytes(input.signature);
|
|
519
|
+
if (signature === undefined || signature.length !== 64) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
return yield* Effect.tryPromise({
|
|
523
|
+
try: async () => {
|
|
524
|
+
const crypto = getJwtWebCrypto(options);
|
|
525
|
+
const key = await crypto.subtle.importKey("jwk", jwkForImport(publicJwk), ed25519ImportAlgorithm, false, ["verify"]);
|
|
526
|
+
return await crypto.subtle.verify(ed25519ImportAlgorithm, key, signature, textEncoder.encode(input.signingInput));
|
|
527
|
+
},
|
|
528
|
+
catch: (cause) => jwtError("signature", "Failed to verify EdDSA JWT", cause),
|
|
529
|
+
});
|
|
530
|
+
}),
|
|
531
|
+
});
|
|
532
|
+
const audienceValues = (value) => {
|
|
533
|
+
if (typeof value === "string") {
|
|
534
|
+
return [value];
|
|
535
|
+
}
|
|
536
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
|
537
|
+
return value;
|
|
538
|
+
}
|
|
539
|
+
return [];
|
|
540
|
+
};
|
|
541
|
+
const clockToleranceSeconds = (input) => input.clockTolerance === undefined
|
|
542
|
+
? 0
|
|
543
|
+
: Math.ceil(Duration.toMillis(input.clockTolerance) / 1000);
|
|
544
|
+
const validateClaims = (claims, input, now) => {
|
|
545
|
+
const nowSeconds = numericDate(now);
|
|
546
|
+
const toleranceSeconds = clockToleranceSeconds(input);
|
|
547
|
+
if (claims.exp !== undefined &&
|
|
548
|
+
(typeof claims.exp !== "number" || nowSeconds - toleranceSeconds >= claims.exp)) {
|
|
549
|
+
return "expired";
|
|
550
|
+
}
|
|
551
|
+
if (claims.nbf !== undefined &&
|
|
552
|
+
(typeof claims.nbf !== "number" || nowSeconds + toleranceSeconds < claims.nbf)) {
|
|
553
|
+
return "not_yet_valid";
|
|
554
|
+
}
|
|
555
|
+
if (input.issuer !== undefined && claims.iss !== input.issuer) {
|
|
556
|
+
return "issuer_mismatch";
|
|
557
|
+
}
|
|
558
|
+
if (input.audience !== undefined) {
|
|
559
|
+
const expected = audienceValues(input.audience);
|
|
560
|
+
const actual = audienceValues(claims.aud);
|
|
561
|
+
if (expected.length === 0 ||
|
|
562
|
+
actual.length === 0 ||
|
|
563
|
+
!expected.some((audience) => actual.includes(audience))) {
|
|
564
|
+
return "audience_mismatch";
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return undefined;
|
|
568
|
+
};
|
|
569
|
+
const issuedClaims = (input, issuedAt) => {
|
|
570
|
+
const expiresAt = input.expiresAt ??
|
|
571
|
+
(input.expiresIn === undefined ? undefined : addDuration(issuedAt, input.expiresIn));
|
|
572
|
+
return {
|
|
573
|
+
...input.claims,
|
|
574
|
+
...(input.issuer === undefined ? {} : { iss: input.issuer }),
|
|
575
|
+
...(input.subject === undefined ? {} : { sub: input.subject }),
|
|
576
|
+
...(input.audience === undefined ? {} : { aud: input.audience }),
|
|
577
|
+
...(expiresAt === undefined ? {} : { exp: numericDate(expiresAt) }),
|
|
578
|
+
...(input.notBefore === undefined ? {} : { nbf: numericDate(input.notBefore) }),
|
|
579
|
+
iat: numericDate(issuedAt),
|
|
580
|
+
...(input.jwtId === undefined ? {} : { jti: input.jwtId }),
|
|
581
|
+
};
|
|
582
|
+
};
|
|
583
|
+
export const makeJwtIssuer = (dependencies) => {
|
|
584
|
+
const signature = signatureService(dependencies);
|
|
585
|
+
return JwtIssuer.make({
|
|
586
|
+
issue: Effect.fn("auth.jwt.issue")(function* (input) {
|
|
587
|
+
const issuedAt = input.issuedAt ?? (yield* currentUnixMillis);
|
|
588
|
+
const alg = input.alg ?? "HS256";
|
|
589
|
+
if (!signature.supports(alg)) {
|
|
590
|
+
return yield* jwtError("issue", "JWT algorithm is not supported");
|
|
591
|
+
}
|
|
592
|
+
const key = yield* dependencies.keys.selectSigningKey({
|
|
593
|
+
alg,
|
|
594
|
+
now: issuedAt,
|
|
595
|
+
});
|
|
596
|
+
if (Option.isNone(key)) {
|
|
597
|
+
return yield* jwtError("issue", "No active JWT signing key is available");
|
|
598
|
+
}
|
|
599
|
+
if (!signature.canSign({ alg: key.value.alg, key: key.value })) {
|
|
600
|
+
return yield* jwtError("issue", "Selected JWT signing key is not supported by the signature service");
|
|
601
|
+
}
|
|
602
|
+
const header = {
|
|
603
|
+
typ: "JWT",
|
|
604
|
+
alg: key.value.alg,
|
|
605
|
+
kid: key.value.id,
|
|
606
|
+
};
|
|
607
|
+
const claims = issuedClaims(input, issuedAt);
|
|
608
|
+
const signingInput = `${encodeJson(header)}.${encodeJson(claims)}`;
|
|
609
|
+
const tokenSignature = yield* signature.sign({
|
|
610
|
+
alg: key.value.alg,
|
|
611
|
+
key: key.value,
|
|
612
|
+
signingInput,
|
|
613
|
+
});
|
|
614
|
+
return {
|
|
615
|
+
token: Redacted.make(JwtToken(`${signingInput}.${tokenSignature}`)),
|
|
616
|
+
header,
|
|
617
|
+
claims,
|
|
618
|
+
key: keyInfo(key.value),
|
|
619
|
+
};
|
|
620
|
+
}),
|
|
621
|
+
});
|
|
622
|
+
};
|
|
623
|
+
export const makeJwtVerifier = (dependencies) => {
|
|
624
|
+
const signature = signatureService(dependencies);
|
|
625
|
+
return JwtVerifier.make({
|
|
626
|
+
verify: Effect.fn("auth.jwt.verify")(function* (input) {
|
|
627
|
+
const token = Redacted.value(input.token).trim();
|
|
628
|
+
const parts = token.split(".");
|
|
629
|
+
if (parts.length !== 3) {
|
|
630
|
+
return { valid: false, reason: "malformed" };
|
|
631
|
+
}
|
|
632
|
+
const [headerPart, payloadPart, signaturePart] = parts;
|
|
633
|
+
const header = decodeHeader(headerPart);
|
|
634
|
+
if (typeof header === "string") {
|
|
635
|
+
return { valid: false, reason: header };
|
|
636
|
+
}
|
|
637
|
+
if (!signature.supports(header.alg)) {
|
|
638
|
+
return {
|
|
639
|
+
valid: false,
|
|
640
|
+
reason: "unsupported_alg",
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const now = input.now ?? (yield* currentUnixMillis);
|
|
644
|
+
const key = yield* dependencies.keys.findVerificationKey({
|
|
645
|
+
kid: header.kid,
|
|
646
|
+
alg: header.alg,
|
|
647
|
+
now,
|
|
648
|
+
});
|
|
649
|
+
if (Option.isNone(key)) {
|
|
650
|
+
return { valid: false, reason: "unknown_key" };
|
|
651
|
+
}
|
|
652
|
+
if (!signature.canVerify({ alg: header.alg, key: key.value })) {
|
|
653
|
+
return { valid: false, reason: "unknown_key" };
|
|
654
|
+
}
|
|
655
|
+
const signatureMatches = yield* signature.verify({
|
|
656
|
+
alg: header.alg,
|
|
657
|
+
key: key.value,
|
|
658
|
+
signingInput: `${headerPart}.${payloadPart}`,
|
|
659
|
+
signature: signaturePart,
|
|
660
|
+
});
|
|
661
|
+
if (!signatureMatches) {
|
|
662
|
+
return {
|
|
663
|
+
valid: false,
|
|
664
|
+
reason: "invalid_signature",
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const claims = decodeClaims(payloadPart);
|
|
668
|
+
if (claims === undefined) {
|
|
669
|
+
return { valid: false, reason: "malformed" };
|
|
670
|
+
}
|
|
671
|
+
const invalidReason = validateClaims(claims, input, now);
|
|
672
|
+
if (invalidReason !== undefined) {
|
|
673
|
+
return { valid: false, reason: invalidReason };
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
valid: true,
|
|
677
|
+
header,
|
|
678
|
+
claims,
|
|
679
|
+
key: keyInfo(key.value),
|
|
680
|
+
};
|
|
681
|
+
}),
|
|
682
|
+
});
|
|
683
|
+
};
|
|
684
|
+
export const makeJwks = (dependencies) => Jwks.make({
|
|
685
|
+
document: Effect.fn("auth.jwks.document")(function* (input) {
|
|
686
|
+
const now = input?.now ?? (yield* currentUnixMillis);
|
|
687
|
+
const keys = yield* dependencies.keys.listPublicJwks({ now });
|
|
688
|
+
return { keys };
|
|
689
|
+
}),
|
|
690
|
+
});
|
|
691
|
+
export const JwtSignatureLive = Layer.effect(JwtSignature)(Effect.gen(function* () {
|
|
692
|
+
const crypto = yield* Crypto;
|
|
693
|
+
return makeJwtSignature({ crypto });
|
|
694
|
+
}));
|
|
695
|
+
export const JwtWebCryptoRs256SignatureLive = (options) => Layer.succeed(JwtSignature)(makeJwtWebCryptoRs256Signature(options));
|
|
696
|
+
export const JwtWebCryptoEs256SignatureLive = (options) => Layer.succeed(JwtSignature)(makeJwtWebCryptoEs256Signature(options));
|
|
697
|
+
export const JwtWebCryptoEdDsaSignatureLive = (options) => Layer.succeed(JwtSignature)(makeJwtWebCryptoEdDsaSignature(options));
|
|
698
|
+
export const JwtIssuerWithSignatureLive = Layer.effect(JwtIssuer)(Effect.gen(function* () {
|
|
699
|
+
const keys = yield* JwtKeys;
|
|
700
|
+
const signature = yield* JwtSignature;
|
|
701
|
+
return makeJwtIssuer({ keys, signature });
|
|
702
|
+
}));
|
|
703
|
+
export const JwtVerifierWithSignatureLive = Layer.effect(JwtVerifier)(Effect.gen(function* () {
|
|
704
|
+
const keys = yield* JwtKeys;
|
|
705
|
+
const signature = yield* JwtSignature;
|
|
706
|
+
return makeJwtVerifier({ keys, signature });
|
|
707
|
+
}));
|
|
708
|
+
export const JwtIssuerLive = JwtIssuerWithSignatureLive.pipe(Layer.provide(JwtSignatureLive));
|
|
709
|
+
export const JwtVerifierLive = JwtVerifierWithSignatureLive.pipe(Layer.provide(JwtSignatureLive));
|
|
710
|
+
export const JwksLive = Layer.effect(Jwks)(Effect.gen(function* () {
|
|
711
|
+
const keys = yield* JwtKeys;
|
|
712
|
+
return makeJwks({ keys });
|
|
713
|
+
}));
|
|
714
|
+
export const JwtRevocationLive = Layer.effect(JwtRevocation)(Effect.gen(function* () {
|
|
715
|
+
const verifier = yield* JwtVerifier;
|
|
716
|
+
const store = yield* JwtRevocationStore;
|
|
717
|
+
return makeJwtRevocation({ verifier, store });
|
|
718
|
+
}));
|
|
719
|
+
export const JwtWithSignatureLive = Layer.mergeAll(JwtIssuerWithSignatureLive, JwtVerifierWithSignatureLive, JwksLive);
|
|
720
|
+
export const JwtLive = JwtWithSignatureLive.pipe(Layer.provide(JwtSignatureLive));
|
|
721
|
+
//# sourceMappingURL=Jwt.js.map
|