@effect-auth/core 0.1.0-alpha.10 → 0.1.0-alpha.14
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 +65 -4
- package/dist/AuditLog.d.ts +60 -0
- package/dist/AuditLog.d.ts.map +1 -1
- package/dist/AuditLog.js +105 -0
- package/dist/AuditLog.js.map +1 -1
- package/dist/AuthKernel.d.ts +1 -1
- package/dist/Client.d.ts +72 -1
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +218 -1
- package/dist/Client.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 +389 -4
- package/dist/EffectQbSqliteStorage.d.ts.map +1 -1
- package/dist/EffectQbSqliteStorage.js +1191 -4
- package/dist/EffectQbSqliteStorage.js.map +1 -1
- package/dist/EmailOtp.d.ts +2 -2
- package/dist/HttpApi/Api.d.ts +477 -23
- package/dist/HttpApi/Api.d.ts.map +1 -1
- package/dist/HttpApi/Api.js +705 -4
- package/dist/HttpApi/Api.js.map +1 -1
- package/dist/HttpApi/Endpoints.d.ts +133 -5
- package/dist/HttpApi/Endpoints.d.ts.map +1 -1
- package/dist/HttpApi/Endpoints.js +57 -2
- package/dist/HttpApi/Endpoints.js.map +1 -1
- package/dist/HttpApi/Errors.d.ts +32 -0
- package/dist/HttpApi/Errors.d.ts.map +1 -1
- package/dist/HttpApi/Errors.js +46 -0
- package/dist/HttpApi/Errors.js.map +1 -1
- package/dist/HttpApi/Schemas.d.ts +149 -8
- package/dist/HttpApi/Schemas.d.ts.map +1 -1
- package/dist/HttpApi/Schemas.js +128 -0
- package/dist/HttpApi/Schemas.js.map +1 -1
- 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/Jwt.d.ts +10 -0
- package/dist/Jwt.d.ts.map +1 -1
- package/dist/Jwt.js +209 -0
- package/dist/Jwt.js.map +1 -1
- package/dist/LoginNotification.d.ts +1 -1
- package/dist/LoginRisk.d.ts +272 -1
- package/dist/LoginRisk.d.ts.map +1 -1
- package/dist/LoginRisk.js +427 -0
- package/dist/LoginRisk.js.map +1 -1
- 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 +2 -2
- package/dist/OAuth.d.ts +957 -1
- package/dist/OAuth.d.ts.map +1 -1
- package/dist/OAuth.js +1403 -1
- package/dist/OAuth.js.map +1 -1
- package/dist/Password.d.ts +2 -2
- 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 +8 -2
- package/dist/SecurityTimeline.d.ts.map +1 -1
- package/dist/SecurityTimeline.js +75 -0
- package/dist/SecurityTimeline.js.map +1 -1
- package/dist/StorageMigrations.d.ts +11 -1
- package/dist/StorageMigrations.d.ts.map +1 -1
- package/dist/StorageMigrations.js +206 -0
- package/dist/StorageMigrations.js.map +1 -1
- package/dist/Webhook.d.ts +310 -2
- package/dist/Webhook.d.ts.map +1 -1
- package/dist/Webhook.js +511 -1
- package/dist/Webhook.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- 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 +18 -2
package/dist/OAuth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Brand, Context, Data, Duration, Effect, Encoding, Layer, Option, Redacted, } from "effect";
|
|
2
2
|
import { Challenge } from "./Challenge.js";
|
|
3
3
|
import { Crypto } from "./Crypto.js";
|
|
4
|
-
import { ChallengeId, Email, OAuthAccountId, OAuthProviderId, UnixMillis, UserId, } from "./Identifiers.js";
|
|
4
|
+
import { ChallengeId, Email, OAuthAccountId, OAuthClientId, OAuthProviderId, UnixMillis, UserId, } from "./Identifiers.js";
|
|
5
5
|
import { currentUnixMillis } from "./Internal.js";
|
|
6
6
|
import { JwtAlgorithms, JwtError, JwtKeyId, JwtKeys, JwtVerifier, } from "./Jwt.js";
|
|
7
7
|
export const OAuthStateToken = Brand.nominal();
|
|
@@ -17,8 +17,14 @@ export const defaultOAuthLinkConfirmationTtl = Duration.minutes(15);
|
|
|
17
17
|
export const defaultOAuthJwksCacheTtl = Duration.minutes(10);
|
|
18
18
|
export const defaultOAuthStateSecretBytes = 32;
|
|
19
19
|
export const defaultOAuthCodeVerifierBytes = 32;
|
|
20
|
+
export const defaultOAuthAuthorizationCodeBytes = 32;
|
|
21
|
+
export const defaultOAuthAuthorizationCodeTtl = Duration.minutes(5);
|
|
20
22
|
export const defaultOAuthLinkConfirmationSecretBytes = 32;
|
|
21
23
|
export const defaultOAuthNonceBytes = 16;
|
|
24
|
+
export const defaultOAuthProviderModeAccessTokenBytes = 32;
|
|
25
|
+
export const defaultOAuthProviderModeRefreshTokenBytes = 32;
|
|
26
|
+
export const defaultOAuthProviderModeAccessTokenTtl = Duration.hours(1);
|
|
27
|
+
export const defaultOAuthProviderModeRefreshTokenTtl = Duration.days(30);
|
|
22
28
|
export const appleSignInProviderId = OAuthProviderId("apple");
|
|
23
29
|
export const appleSignInIssuer = "https://appleid.apple.com";
|
|
24
30
|
export const appleSignInAuthorizationEndpoint = "https://appleid.apple.com/auth/authorize";
|
|
@@ -70,8 +76,160 @@ export const microsoftOidcJwksUri = (tenant) => `${microsoftOidcLoginBaseUrl}/${
|
|
|
70
76
|
export const microsoftOidcIssuer = (tenant) => `${microsoftOidcLoginBaseUrl}/${microsoftOidcTenantPathSegment(tenant)}/v2.0`;
|
|
71
77
|
export class OAuthProviderError extends Data.TaggedError("OAuthProviderError") {
|
|
72
78
|
}
|
|
79
|
+
export const OAuthClientSecretPrefix = Brand.nominal();
|
|
80
|
+
export const OAuthClientSecret = Brand.nominal();
|
|
81
|
+
export const OAuthClientSecretHash = Brand.nominal();
|
|
82
|
+
export const defaultOAuthClientSecretPrefixBytes = 8;
|
|
83
|
+
export const defaultOAuthClientSecretBytes = 32;
|
|
84
|
+
export class OAuthClientRegistryError extends Data.TaggedError("OAuthClientRegistryError") {
|
|
85
|
+
}
|
|
86
|
+
export class OAuthClientRegistry extends Context.Service()("auth/OAuthClientRegistry") {
|
|
87
|
+
}
|
|
88
|
+
(function (OAuthClientRegistry) {
|
|
89
|
+
OAuthClientRegistry.make = (service) => OAuthClientRegistry.of(service);
|
|
90
|
+
})(OAuthClientRegistry || (OAuthClientRegistry = {}));
|
|
91
|
+
export class OAuthClientStoreError extends Data.TaggedError("OAuthClientStoreError") {
|
|
92
|
+
}
|
|
93
|
+
export class OAuthClientStore extends Context.Service()("auth/OAuthClientStore") {
|
|
94
|
+
}
|
|
95
|
+
(function (OAuthClientStore) {
|
|
96
|
+
OAuthClientStore.make = (service) => OAuthClientStore.of(service);
|
|
97
|
+
})(OAuthClientStore || (OAuthClientStore = {}));
|
|
98
|
+
export class OAuthConsentStoreError extends Data.TaggedError("OAuthConsentStoreError") {
|
|
99
|
+
}
|
|
100
|
+
export class OAuthConsentStore extends Context.Service()("auth/OAuthConsentStore") {
|
|
101
|
+
}
|
|
102
|
+
(function (OAuthConsentStore) {
|
|
103
|
+
OAuthConsentStore.make = (service) => OAuthConsentStore.of(service);
|
|
104
|
+
})(OAuthConsentStore || (OAuthConsentStore = {}));
|
|
105
|
+
export class OAuthClientSecretError extends Data.TaggedError("OAuthClientSecretError") {
|
|
106
|
+
}
|
|
107
|
+
export class OAuthClientSecrets extends Context.Service()("auth/OAuthClientSecrets") {
|
|
108
|
+
}
|
|
109
|
+
(function (OAuthClientSecrets) {
|
|
110
|
+
OAuthClientSecrets.make = (service) => OAuthClientSecrets.of(service);
|
|
111
|
+
})(OAuthClientSecrets || (OAuthClientSecrets = {}));
|
|
112
|
+
export class OAuthClientSecretStoreError extends Data.TaggedError("OAuthClientSecretStoreError") {
|
|
113
|
+
}
|
|
114
|
+
export class OAuthClientSecretStore extends Context.Service()("auth/OAuthClientSecretStore") {
|
|
115
|
+
}
|
|
116
|
+
(function (OAuthClientSecretStore) {
|
|
117
|
+
OAuthClientSecretStore.make = (service) => OAuthClientSecretStore.of(service);
|
|
118
|
+
})(OAuthClientSecretStore || (OAuthClientSecretStore = {}));
|
|
119
|
+
export class OAuthClientSecretVerificationError extends Data.TaggedError("OAuthClientSecretVerificationError") {
|
|
120
|
+
}
|
|
121
|
+
export class OAuthClientSecretVerification extends Context.Service()("auth/OAuthClientSecretVerification") {
|
|
122
|
+
}
|
|
123
|
+
(function (OAuthClientSecretVerification) {
|
|
124
|
+
OAuthClientSecretVerification.make = (service) => OAuthClientSecretVerification.of(service);
|
|
125
|
+
})(OAuthClientSecretVerification || (OAuthClientSecretVerification = {}));
|
|
126
|
+
export class OAuthAuthorizationCodeStoreError extends Data.TaggedError("OAuthAuthorizationCodeStoreError") {
|
|
127
|
+
}
|
|
128
|
+
export class OAuthAuthorizationCodeStore extends Context.Service()("auth/OAuthAuthorizationCodeStore") {
|
|
129
|
+
}
|
|
130
|
+
(function (OAuthAuthorizationCodeStore) {
|
|
131
|
+
OAuthAuthorizationCodeStore.make = (service) => OAuthAuthorizationCodeStore.of(service);
|
|
132
|
+
})(OAuthAuthorizationCodeStore || (OAuthAuthorizationCodeStore = {}));
|
|
133
|
+
export class OAuthAuthorizationCodeGrantError extends Data.TaggedError("OAuthAuthorizationCodeGrantError") {
|
|
134
|
+
}
|
|
135
|
+
export class OAuthAuthorizationCodeGrant extends Context.Service()("auth/OAuthAuthorizationCodeGrant") {
|
|
136
|
+
}
|
|
137
|
+
(function (OAuthAuthorizationCodeGrant) {
|
|
138
|
+
OAuthAuthorizationCodeGrant.make = (service) => OAuthAuthorizationCodeGrant.of(service);
|
|
139
|
+
})(OAuthAuthorizationCodeGrant || (OAuthAuthorizationCodeGrant = {}));
|
|
140
|
+
export class OAuthAuthorizationCodeTokenIssuerError extends Data.TaggedError("OAuthAuthorizationCodeTokenIssuerError") {
|
|
141
|
+
}
|
|
142
|
+
export class OAuthAuthorizationCodeTokenIssuer extends Context.Service()("auth/OAuthAuthorizationCodeTokenIssuer") {
|
|
143
|
+
}
|
|
144
|
+
(function (OAuthAuthorizationCodeTokenIssuer) {
|
|
145
|
+
OAuthAuthorizationCodeTokenIssuer.make = (service) => OAuthAuthorizationCodeTokenIssuer.of(service);
|
|
146
|
+
})(OAuthAuthorizationCodeTokenIssuer || (OAuthAuthorizationCodeTokenIssuer = {}));
|
|
147
|
+
export class OAuthAuthorizationCodeTokenGrantError extends Data.TaggedError("OAuthAuthorizationCodeTokenGrantError") {
|
|
148
|
+
}
|
|
149
|
+
export class OAuthAuthorizationCodeTokenGrant extends Context.Service()("auth/OAuthAuthorizationCodeTokenGrant") {
|
|
150
|
+
}
|
|
151
|
+
(function (OAuthAuthorizationCodeTokenGrant) {
|
|
152
|
+
OAuthAuthorizationCodeTokenGrant.make = (service) => OAuthAuthorizationCodeTokenGrant.of(service);
|
|
153
|
+
})(OAuthAuthorizationCodeTokenGrant || (OAuthAuthorizationCodeTokenGrant = {}));
|
|
154
|
+
export class OAuthRefreshTokenIssuerError extends Data.TaggedError("OAuthRefreshTokenIssuerError") {
|
|
155
|
+
}
|
|
156
|
+
export class OAuthRefreshTokenIssuer extends Context.Service()("auth/OAuthRefreshTokenIssuer") {
|
|
157
|
+
}
|
|
158
|
+
(function (OAuthRefreshTokenIssuer) {
|
|
159
|
+
OAuthRefreshTokenIssuer.make = (service) => OAuthRefreshTokenIssuer.of(service);
|
|
160
|
+
})(OAuthRefreshTokenIssuer || (OAuthRefreshTokenIssuer = {}));
|
|
161
|
+
export class OAuthRefreshTokenGrantError extends Data.TaggedError("OAuthRefreshTokenGrantError") {
|
|
162
|
+
}
|
|
163
|
+
export class OAuthRefreshTokenGrant extends Context.Service()("auth/OAuthRefreshTokenGrant") {
|
|
164
|
+
}
|
|
165
|
+
(function (OAuthRefreshTokenGrant) {
|
|
166
|
+
OAuthRefreshTokenGrant.make = (service) => OAuthRefreshTokenGrant.of(service);
|
|
167
|
+
})(OAuthRefreshTokenGrant || (OAuthRefreshTokenGrant = {}));
|
|
168
|
+
export class OAuthClientCredentialsTokenIssuerError extends Data.TaggedError("OAuthClientCredentialsTokenIssuerError") {
|
|
169
|
+
}
|
|
170
|
+
export class OAuthClientCredentialsTokenIssuer extends Context.Service()("auth/OAuthClientCredentialsTokenIssuer") {
|
|
171
|
+
}
|
|
172
|
+
(function (OAuthClientCredentialsTokenIssuer) {
|
|
173
|
+
OAuthClientCredentialsTokenIssuer.make = (service) => OAuthClientCredentialsTokenIssuer.of(service);
|
|
174
|
+
})(OAuthClientCredentialsTokenIssuer || (OAuthClientCredentialsTokenIssuer = {}));
|
|
175
|
+
export class OAuthClientCredentialsGrantError extends Data.TaggedError("OAuthClientCredentialsGrantError") {
|
|
176
|
+
}
|
|
177
|
+
export class OAuthClientCredentialsGrant extends Context.Service()("auth/OAuthClientCredentialsGrant") {
|
|
178
|
+
}
|
|
179
|
+
(function (OAuthClientCredentialsGrant) {
|
|
180
|
+
OAuthClientCredentialsGrant.make = (service) => OAuthClientCredentialsGrant.of(service);
|
|
181
|
+
})(OAuthClientCredentialsGrant || (OAuthClientCredentialsGrant = {}));
|
|
182
|
+
export class OAuthTokenRevokerError extends Data.TaggedError("OAuthTokenRevokerError") {
|
|
183
|
+
}
|
|
184
|
+
export class OAuthTokenRevoker extends Context.Service()("auth/OAuthTokenRevoker") {
|
|
185
|
+
}
|
|
186
|
+
(function (OAuthTokenRevoker) {
|
|
187
|
+
OAuthTokenRevoker.make = (service) => OAuthTokenRevoker.of(service);
|
|
188
|
+
})(OAuthTokenRevoker || (OAuthTokenRevoker = {}));
|
|
189
|
+
export class OAuthTokenRevocationError extends Data.TaggedError("OAuthTokenRevocationError") {
|
|
190
|
+
}
|
|
191
|
+
export class OAuthTokenRevocation extends Context.Service()("auth/OAuthTokenRevocation") {
|
|
192
|
+
}
|
|
193
|
+
(function (OAuthTokenRevocation) {
|
|
194
|
+
OAuthTokenRevocation.make = (service) => OAuthTokenRevocation.of(service);
|
|
195
|
+
})(OAuthTokenRevocation || (OAuthTokenRevocation = {}));
|
|
196
|
+
export class OAuthTokenIntrospectorError extends Data.TaggedError("OAuthTokenIntrospectorError") {
|
|
197
|
+
}
|
|
198
|
+
export class OAuthTokenIntrospector extends Context.Service()("auth/OAuthTokenIntrospector") {
|
|
199
|
+
}
|
|
200
|
+
(function (OAuthTokenIntrospector) {
|
|
201
|
+
OAuthTokenIntrospector.make = (service) => OAuthTokenIntrospector.of(service);
|
|
202
|
+
})(OAuthTokenIntrospector || (OAuthTokenIntrospector = {}));
|
|
203
|
+
export class OAuthTokenIntrospectionError extends Data.TaggedError("OAuthTokenIntrospectionError") {
|
|
204
|
+
}
|
|
205
|
+
export class OAuthTokenIntrospection extends Context.Service()("auth/OAuthTokenIntrospection") {
|
|
206
|
+
}
|
|
207
|
+
(function (OAuthTokenIntrospection) {
|
|
208
|
+
OAuthTokenIntrospection.make = (service) => OAuthTokenIntrospection.of(service);
|
|
209
|
+
})(OAuthTokenIntrospection || (OAuthTokenIntrospection = {}));
|
|
210
|
+
export class OAuthProviderAuthorizationPolicyError extends Data.TaggedError("OAuthProviderAuthorizationPolicyError") {
|
|
211
|
+
}
|
|
212
|
+
export class OAuthProviderAuthorizationPolicy extends Context.Service()("auth/OAuthProviderAuthorizationPolicy") {
|
|
213
|
+
}
|
|
214
|
+
(function (OAuthProviderAuthorizationPolicy) {
|
|
215
|
+
OAuthProviderAuthorizationPolicy.make = (service) => OAuthProviderAuthorizationPolicy.of(service);
|
|
216
|
+
})(OAuthProviderAuthorizationPolicy || (OAuthProviderAuthorizationPolicy = {}));
|
|
217
|
+
export class OAuthProviderAuthorizationError extends Data.TaggedError("OAuthProviderAuthorizationError") {
|
|
218
|
+
}
|
|
219
|
+
export class OAuthProviderAuthorization extends Context.Service()("auth/OAuthProviderAuthorization") {
|
|
220
|
+
}
|
|
221
|
+
(function (OAuthProviderAuthorization) {
|
|
222
|
+
OAuthProviderAuthorization.make = (service) => OAuthProviderAuthorization.of(service);
|
|
223
|
+
})(OAuthProviderAuthorization || (OAuthProviderAuthorization = {}));
|
|
73
224
|
export class OAuthUserInfoError extends Data.TaggedError("OAuthUserInfoError") {
|
|
74
225
|
}
|
|
226
|
+
export class OAuthProviderModeTokenStoreError extends Data.TaggedError("OAuthProviderModeTokenStoreError") {
|
|
227
|
+
}
|
|
228
|
+
export class OAuthProviderModeTokenStore extends Context.Service()("auth/OAuthProviderModeTokenStore") {
|
|
229
|
+
}
|
|
230
|
+
(function (OAuthProviderModeTokenStore) {
|
|
231
|
+
OAuthProviderModeTokenStore.make = (service) => OAuthProviderModeTokenStore.of(service);
|
|
232
|
+
})(OAuthProviderModeTokenStore || (OAuthProviderModeTokenStore = {}));
|
|
75
233
|
export class OAuthProviderTokenVaultError extends Data.TaggedError("OAuthProviderTokenVaultError") {
|
|
76
234
|
}
|
|
77
235
|
export class OAuthProviderTokenVault extends Context.Service()("auth/OAuthProviderTokenVault") {
|
|
@@ -148,6 +306,43 @@ export class OAuthLinkConfirmation extends Context.Service()("auth/OAuthLinkConf
|
|
|
148
306
|
OAuthLinkConfirmation.make = (service) => OAuthLinkConfirmation.of(service);
|
|
149
307
|
})(OAuthLinkConfirmation || (OAuthLinkConfirmation = {}));
|
|
150
308
|
const oauthStateError = (operation, message, cause) => new OAuthStateError({ operation, message, cause });
|
|
309
|
+
const oauthAuthorizationCodeGrantError = (operation, message, cause) => new OAuthAuthorizationCodeGrantError({ operation, message, cause });
|
|
310
|
+
const oauthAuthorizationCodeTokenGrantError = (operation, message, cause) => new OAuthAuthorizationCodeTokenGrantError({ operation, message, cause });
|
|
311
|
+
const oauthAuthorizationCodeTokenIssuerError = (message, cause) => new OAuthAuthorizationCodeTokenIssuerError({
|
|
312
|
+
operation: "issue",
|
|
313
|
+
message,
|
|
314
|
+
...(cause === undefined ? {} : { cause }),
|
|
315
|
+
});
|
|
316
|
+
const oauthRefreshTokenGrantError = (operation, message, cause) => new OAuthRefreshTokenGrantError({ operation, message, cause });
|
|
317
|
+
const oauthRefreshTokenIssuerError = (message, cause) => new OAuthRefreshTokenIssuerError({
|
|
318
|
+
operation: "issue",
|
|
319
|
+
message,
|
|
320
|
+
...(cause === undefined ? {} : { cause }),
|
|
321
|
+
});
|
|
322
|
+
const oauthClientCredentialsGrantError = (operation, message, cause) => new OAuthClientCredentialsGrantError({ operation, message, cause });
|
|
323
|
+
const oauthClientCredentialsTokenIssuerError = (message, cause) => new OAuthClientCredentialsTokenIssuerError({
|
|
324
|
+
operation: "issue",
|
|
325
|
+
message,
|
|
326
|
+
...(cause === undefined ? {} : { cause }),
|
|
327
|
+
});
|
|
328
|
+
const oauthClientRegistryError = (operation, message, cause) => new OAuthClientRegistryError({
|
|
329
|
+
operation,
|
|
330
|
+
message,
|
|
331
|
+
...(cause === undefined ? {} : { cause }),
|
|
332
|
+
});
|
|
333
|
+
const oauthClientSecretError = (operation, message, cause) => new OAuthClientSecretError({
|
|
334
|
+
operation,
|
|
335
|
+
message,
|
|
336
|
+
...(cause === undefined ? {} : { cause }),
|
|
337
|
+
});
|
|
338
|
+
const oauthClientSecretVerificationError = (message, cause) => new OAuthClientSecretVerificationError({
|
|
339
|
+
operation: "authenticate-client",
|
|
340
|
+
message,
|
|
341
|
+
...(cause === undefined ? {} : { cause }),
|
|
342
|
+
});
|
|
343
|
+
const oauthTokenRevocationError = (operation, message, cause) => new OAuthTokenRevocationError({ operation, message, cause });
|
|
344
|
+
const oauthTokenIntrospectionError = (operation, message, cause) => new OAuthTokenIntrospectionError({ operation, message, cause });
|
|
345
|
+
const oauthProviderAuthorizationError = (operation, message, cause) => new OAuthProviderAuthorizationError({ operation, message, cause });
|
|
151
346
|
const oauthTokenExchangeError = (operation, message, cause) => new OAuthTokenExchangeError({ operation, message, cause });
|
|
152
347
|
const oauthProviderTokenLifecycleError = (operation, message, cause) => new OAuthProviderTokenLifecycleError({ operation, message, cause });
|
|
153
348
|
const oidcIdTokenVerifierError = (operation, message, cause) => new OidcIdTokenVerifierError({ operation, message, cause });
|
|
@@ -187,7 +382,56 @@ const validateByteLength = (value, operation, label) => Number.isInteger(value)
|
|
|
187
382
|
const validateOAuthLinkConfirmationSecretBytes = (value) => Number.isInteger(value) && value >= 16 && value <= 128
|
|
188
383
|
? Effect.succeed(value)
|
|
189
384
|
: Effect.fail(oauthLinkConfirmationError("start", "OAuth link confirmation secret byte length must be an integer between 16 and 128"));
|
|
385
|
+
const validateOAuthAuthorizationCodeBytes = (value) => Number.isInteger(value) && value >= 16 && value <= 128
|
|
386
|
+
? Effect.succeed(value)
|
|
387
|
+
: Effect.fail(oauthAuthorizationCodeGrantError("issue", "OAuth authorization code byte length must be an integer between 16 and 128"));
|
|
190
388
|
const metadataValue = (value) => value === undefined ? undefined : structuredClone(value);
|
|
389
|
+
const validateOAuthClientSecretByteLength = (value, operation, label) => Number.isInteger(value) && value >= 4 && value <= 128
|
|
390
|
+
? Effect.succeed(value)
|
|
391
|
+
: Effect.fail(oauthClientSecretError(operation, `${label} byte length must be an integer between 4 and 128`));
|
|
392
|
+
const parseOAuthClientSecretValue = (secret) => {
|
|
393
|
+
const value = secret.trim();
|
|
394
|
+
const parts = value.split(".");
|
|
395
|
+
if (parts.length !== 2 || parts[0] === "" || parts[1] === "") {
|
|
396
|
+
return Effect.fail(oauthClientSecretError("parse", "OAuth client secret must use '<prefix>.<secret>' format"));
|
|
397
|
+
}
|
|
398
|
+
return Effect.succeed(OAuthClientSecretPrefix(parts[0]));
|
|
399
|
+
};
|
|
400
|
+
const hashOAuthClientSecretValue = (dependencies, secret, operation) => dependencies.crypto.digestSha256({ data: `oauth-client-secret:${secret}` }).pipe(Effect.map((hash) => OAuthClientSecretHash(`sha256:${hash}`)), Effect.mapError((cause) => oauthClientSecretError(operation, "Failed to hash OAuth client secret", cause)));
|
|
401
|
+
export const makeOAuthClientSecrets = (dependencies) => OAuthClientSecrets.make({
|
|
402
|
+
generate: Effect.fn("auth.oauth.client_secret.generate")(function* (input) {
|
|
403
|
+
const prefixBytes = yield* validateOAuthClientSecretByteLength(input?.prefixBytes ?? defaultOAuthClientSecretPrefixBytes, "generate", "OAuth client secret prefix");
|
|
404
|
+
const secretBytes = yield* validateOAuthClientSecretByteLength(input?.secretBytes ?? defaultOAuthClientSecretBytes, "generate", "OAuth client secret");
|
|
405
|
+
const prefix = yield* dependencies.crypto.randomToken(prefixBytes).pipe(Effect.map(OAuthClientSecretPrefix), Effect.mapError((cause) => oauthClientSecretError("generate", "Failed to generate OAuth client secret prefix", cause)));
|
|
406
|
+
const secret = yield* dependencies.crypto.randomToken(secretBytes).pipe(Effect.map((value) => OAuthClientSecret(`${prefix}.${value}`)), Effect.mapError((cause) => oauthClientSecretError("generate", "Failed to generate OAuth client secret", cause)));
|
|
407
|
+
return {
|
|
408
|
+
prefix,
|
|
409
|
+
secret: Redacted.make(secret),
|
|
410
|
+
};
|
|
411
|
+
}),
|
|
412
|
+
parse: Effect.fn("auth.oauth.client_secret.parse")(function* (secret) {
|
|
413
|
+
return yield* parseOAuthClientSecretValue(Redacted.value(secret));
|
|
414
|
+
}),
|
|
415
|
+
hash: Effect.fn("auth.oauth.client_secret.hash")(function* (input) {
|
|
416
|
+
const secret = Redacted.value(input.secret).trim();
|
|
417
|
+
yield* parseOAuthClientSecretValue(secret).pipe(Effect.mapError((cause) => oauthClientSecretError("hash", "Cannot hash malformed OAuth client secret", cause)));
|
|
418
|
+
return yield* hashOAuthClientSecretValue(dependencies, secret, "hash");
|
|
419
|
+
}),
|
|
420
|
+
verify: Effect.fn("auth.oauth.client_secret.verify")(function* (input) {
|
|
421
|
+
const secret = Redacted.value(input.secret).trim();
|
|
422
|
+
const prefix = yield* parseOAuthClientSecretValue(secret).pipe(Effect.catchTag("OAuthClientSecretError", () => Effect.succeed(undefined)));
|
|
423
|
+
if (prefix === undefined) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
const hash = yield* hashOAuthClientSecretValue(dependencies, secret, "verify");
|
|
427
|
+
return yield* dependencies.crypto
|
|
428
|
+
.timingSafeEqual({
|
|
429
|
+
left: textEncoder.encode(hash),
|
|
430
|
+
right: textEncoder.encode(input.hash),
|
|
431
|
+
})
|
|
432
|
+
.pipe(Effect.mapError((cause) => oauthClientSecretError("verify", "Failed to compare OAuth client secret hashes", cause)));
|
|
433
|
+
}),
|
|
434
|
+
});
|
|
191
435
|
export const makeAppleSignInProvider = (input) => ({
|
|
192
436
|
id: input.id ?? appleSignInProviderId,
|
|
193
437
|
clientId: input.clientId,
|
|
@@ -335,6 +579,142 @@ const cloneProviderConfig = (provider) => ({
|
|
|
335
579
|
: { authorizationParams: { ...provider.authorizationParams } }),
|
|
336
580
|
...(provider.metadata === undefined ? {} : { metadata: metadataValue(provider.metadata) }),
|
|
337
581
|
});
|
|
582
|
+
const cloneOAuthClientRecord = (client) => ({
|
|
583
|
+
...client,
|
|
584
|
+
redirectUris: [...client.redirectUris],
|
|
585
|
+
allowedGrantTypes: [...client.allowedGrantTypes],
|
|
586
|
+
allowedResponseTypes: [...client.allowedResponseTypes],
|
|
587
|
+
...(client.allowedScopes === undefined
|
|
588
|
+
? {}
|
|
589
|
+
: { allowedScopes: [...client.allowedScopes] }),
|
|
590
|
+
...(client.metadata === undefined ? {} : { metadata: metadataValue(client.metadata) }),
|
|
591
|
+
});
|
|
592
|
+
const validateOAuthClientRedirectUriRecord = (client, input) => {
|
|
593
|
+
if (client === undefined) {
|
|
594
|
+
return { valid: false, reason: "unknown_client" };
|
|
595
|
+
}
|
|
596
|
+
if (client.status !== "active") {
|
|
597
|
+
return { valid: false, reason: "disabled_client" };
|
|
598
|
+
}
|
|
599
|
+
const responseType = input.responseType ?? "code";
|
|
600
|
+
if (!client.allowedResponseTypes.includes(responseType)) {
|
|
601
|
+
return { valid: false, reason: "unsupported_response_type" };
|
|
602
|
+
}
|
|
603
|
+
if (input.grantType !== undefined &&
|
|
604
|
+
!client.allowedGrantTypes.includes(input.grantType)) {
|
|
605
|
+
return { valid: false, reason: "unsupported_grant_type" };
|
|
606
|
+
}
|
|
607
|
+
if (!client.redirectUris.includes(input.redirectUri)) {
|
|
608
|
+
return { valid: false, reason: "redirect_uri_mismatch" };
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
valid: true,
|
|
612
|
+
client: cloneOAuthClientRecord(client),
|
|
613
|
+
};
|
|
614
|
+
};
|
|
615
|
+
const cloneOAuthConsentRecord = (record) => ({
|
|
616
|
+
userId: record.userId,
|
|
617
|
+
clientId: record.clientId,
|
|
618
|
+
scopes: [...record.scopes],
|
|
619
|
+
grantedAt: record.grantedAt,
|
|
620
|
+
...(record.expiresAt === undefined ? {} : { expiresAt: record.expiresAt }),
|
|
621
|
+
...(record.revokedAt === undefined ? {} : { revokedAt: record.revokedAt }),
|
|
622
|
+
...(record.metadata === undefined
|
|
623
|
+
? {}
|
|
624
|
+
: { metadata: metadataValue(record.metadata) }),
|
|
625
|
+
});
|
|
626
|
+
const oauthConsentCoversScopes = (record, scopes = []) => scopes.every((scope) => record.scopes.includes(scope));
|
|
627
|
+
const isOAuthConsentActive = (record, now, scopes = []) => record.revokedAt === undefined &&
|
|
628
|
+
(record.expiresAt === undefined || Number(record.expiresAt) > Number(now)) &&
|
|
629
|
+
oauthConsentCoversScopes(record, scopes);
|
|
630
|
+
const cloneOAuthClientSecretRecord = (record) => ({
|
|
631
|
+
...record,
|
|
632
|
+
authenticationMethods: [...record.authenticationMethods],
|
|
633
|
+
...(record.metadata === undefined ? {} : { metadata: metadataValue(record.metadata) }),
|
|
634
|
+
});
|
|
635
|
+
const cloneAuthorizationCodeRecord = (record) => ({
|
|
636
|
+
...record,
|
|
637
|
+
scopes: [...record.scopes],
|
|
638
|
+
...(record.metadata === undefined ? {} : { metadata: metadataValue(record.metadata) }),
|
|
639
|
+
});
|
|
640
|
+
const cloneOAuthTokenResponse = (tokens) => ({
|
|
641
|
+
accessToken: tokens.accessToken,
|
|
642
|
+
tokenType: tokens.tokenType,
|
|
643
|
+
...(tokens.expiresIn === undefined ? {} : { expiresIn: tokens.expiresIn }),
|
|
644
|
+
...(tokens.refreshToken === undefined ? {} : { refreshToken: tokens.refreshToken }),
|
|
645
|
+
...(tokens.idToken === undefined ? {} : { idToken: tokens.idToken }),
|
|
646
|
+
...(tokens.scopes === undefined ? {} : { scopes: [...tokens.scopes] }),
|
|
647
|
+
...(tokens.extra === undefined ? {} : { extra: metadataValue(tokens.extra) }),
|
|
648
|
+
});
|
|
649
|
+
const cloneOAuthTokenIntrospectionResponse = (response) => response.active
|
|
650
|
+
? {
|
|
651
|
+
active: true,
|
|
652
|
+
...(response.scopes === undefined ? {} : { scopes: [...response.scopes] }),
|
|
653
|
+
...(response.clientId === undefined ? {} : { clientId: response.clientId }),
|
|
654
|
+
...(response.username === undefined ? {} : { username: response.username }),
|
|
655
|
+
...(response.tokenType === undefined ? {} : { tokenType: response.tokenType }),
|
|
656
|
+
...(response.expiresAt === undefined ? {} : { expiresAt: response.expiresAt }),
|
|
657
|
+
...(response.issuedAt === undefined ? {} : { issuedAt: response.issuedAt }),
|
|
658
|
+
...(response.notBefore === undefined ? {} : { notBefore: response.notBefore }),
|
|
659
|
+
...(response.subject === undefined ? {} : { subject: response.subject }),
|
|
660
|
+
...(response.audience === undefined
|
|
661
|
+
? {}
|
|
662
|
+
: {
|
|
663
|
+
audience: Array.isArray(response.audience)
|
|
664
|
+
? [...response.audience]
|
|
665
|
+
: response.audience,
|
|
666
|
+
}),
|
|
667
|
+
...(response.issuer === undefined ? {} : { issuer: response.issuer }),
|
|
668
|
+
...(response.jwtId === undefined ? {} : { jwtId: response.jwtId }),
|
|
669
|
+
...(response.extra === undefined ? {} : { extra: metadataValue(response.extra) }),
|
|
670
|
+
}
|
|
671
|
+
: { active: false };
|
|
672
|
+
const cloneOAuthProviderModeTokenRecord = (record) => ({
|
|
673
|
+
...record,
|
|
674
|
+
scopes: [...record.scopes],
|
|
675
|
+
...(record.audience === undefined
|
|
676
|
+
? {}
|
|
677
|
+
: {
|
|
678
|
+
audience: Array.isArray(record.audience)
|
|
679
|
+
? [...record.audience]
|
|
680
|
+
: record.audience,
|
|
681
|
+
}),
|
|
682
|
+
...(record.metadata === undefined ? {} : { metadata: metadataValue(record.metadata) }),
|
|
683
|
+
});
|
|
684
|
+
const consumedAuthorizationCode = (record, consumedAt) => ({
|
|
685
|
+
clientId: record.clientId,
|
|
686
|
+
subject: record.subject,
|
|
687
|
+
redirectUri: record.redirectUri,
|
|
688
|
+
scopes: [...record.scopes],
|
|
689
|
+
...(record.codeChallenge === undefined
|
|
690
|
+
? {}
|
|
691
|
+
: { codeChallenge: record.codeChallenge }),
|
|
692
|
+
...(record.codeChallengeMethod === undefined
|
|
693
|
+
? {}
|
|
694
|
+
: { codeChallengeMethod: record.codeChallengeMethod }),
|
|
695
|
+
issuedAt: record.issuedAt,
|
|
696
|
+
expiresAt: record.expiresAt,
|
|
697
|
+
consumedAt,
|
|
698
|
+
...(record.metadata === undefined ? {} : { metadata: metadataValue(record.metadata) }),
|
|
699
|
+
});
|
|
700
|
+
const issuedAuthorizationCode = (input) => ({
|
|
701
|
+
code: Redacted.make(input.code),
|
|
702
|
+
clientId: input.record.clientId,
|
|
703
|
+
subject: input.record.subject,
|
|
704
|
+
redirectUri: input.record.redirectUri,
|
|
705
|
+
scopes: [...input.record.scopes],
|
|
706
|
+
...(input.record.codeChallenge === undefined
|
|
707
|
+
? {}
|
|
708
|
+
: { codeChallenge: input.record.codeChallenge }),
|
|
709
|
+
...(input.record.codeChallengeMethod === undefined
|
|
710
|
+
? {}
|
|
711
|
+
: { codeChallengeMethod: input.record.codeChallengeMethod }),
|
|
712
|
+
issuedAt: input.record.issuedAt,
|
|
713
|
+
expiresAt: input.record.expiresAt,
|
|
714
|
+
...(input.record.metadata === undefined
|
|
715
|
+
? {}
|
|
716
|
+
: { metadata: metadataValue(input.record.metadata) }),
|
|
717
|
+
});
|
|
338
718
|
const encodeOAuthState = (input) => OAuthStateToken(`${input.challengeId}.${Redacted.value(input.secret)}`);
|
|
339
719
|
export const parseOAuthState = (state) => {
|
|
340
720
|
const parts = String(state).split(".");
|
|
@@ -1404,6 +1784,707 @@ const makeOAuthPkceWithCrypto = (dependencies, input) => Effect.gen(function* ()
|
|
|
1404
1784
|
codeChallengeMethod: "S256",
|
|
1405
1785
|
};
|
|
1406
1786
|
});
|
|
1787
|
+
const authorizationCodeHash = (crypto, code, operation) => crypto.digestSha256({ data: String(code) }).pipe(Effect.mapError((cause) => oauthAuthorizationCodeGrantError(operation, "Failed to hash OAuth authorization code", cause)));
|
|
1788
|
+
const pkceChallengeForVerifier = (crypto, verifier) => crypto.digestSha256({ data: Redacted.value(verifier) }).pipe(Effect.map(OAuthCodeChallenge), Effect.mapError((cause) => oauthAuthorizationCodeGrantError("consume", "Failed to calculate OAuth PKCE challenge", cause)));
|
|
1789
|
+
const timingSafeEqualString = (crypto, left, right) => crypto
|
|
1790
|
+
.timingSafeEqual({
|
|
1791
|
+
left: textEncoder.encode(left),
|
|
1792
|
+
right: textEncoder.encode(right),
|
|
1793
|
+
})
|
|
1794
|
+
.pipe(Effect.mapError((cause) => oauthAuthorizationCodeGrantError("consume", "Failed to compare OAuth PKCE challenge", cause)));
|
|
1795
|
+
const authorizationCodeValue = (code) => OAuthAuthorizationCode(String(Redacted.value(code)).trim());
|
|
1796
|
+
const authorizationCodeExpiresAt = (input, issuedAt, defaultTtl) => input.expiresAt ??
|
|
1797
|
+
UnixMillis(Number(issuedAt) + Duration.toMillis(input.expiresIn ?? defaultTtl));
|
|
1798
|
+
export const makeOAuthAuthorizationCodeGrant = (dependencies) => {
|
|
1799
|
+
const codeBytes = dependencies.codeBytes ?? defaultOAuthAuthorizationCodeBytes;
|
|
1800
|
+
const codeTtl = dependencies.codeTtl ?? defaultOAuthAuthorizationCodeTtl;
|
|
1801
|
+
return OAuthAuthorizationCodeGrant.make({
|
|
1802
|
+
issue: Effect.fn("auth.oauth.authorization_code.issue")(function* (input) {
|
|
1803
|
+
const issuedAt = input.issuedAt ?? (yield* currentUnixMillis);
|
|
1804
|
+
const expiresAt = authorizationCodeExpiresAt(input, issuedAt, codeTtl);
|
|
1805
|
+
const code = input.code === undefined
|
|
1806
|
+
? OAuthAuthorizationCode(yield* Effect.gen(function* () {
|
|
1807
|
+
const validatedCodeBytes = yield* validateOAuthAuthorizationCodeBytes(codeBytes);
|
|
1808
|
+
return yield* dependencies.crypto
|
|
1809
|
+
.randomToken(validatedCodeBytes)
|
|
1810
|
+
.pipe(Effect.mapError((cause) => oauthAuthorizationCodeGrantError("issue", "Failed to generate OAuth authorization code", cause)));
|
|
1811
|
+
}))
|
|
1812
|
+
: authorizationCodeValue(input.code);
|
|
1813
|
+
const codeHash = yield* authorizationCodeHash(dependencies.crypto, code, "issue");
|
|
1814
|
+
const codeChallenge = input.codeChallenge === undefined
|
|
1815
|
+
? undefined
|
|
1816
|
+
: OAuthCodeChallenge(String(input.codeChallenge));
|
|
1817
|
+
const record = {
|
|
1818
|
+
codeHash,
|
|
1819
|
+
clientId: input.clientId,
|
|
1820
|
+
subject: input.subject,
|
|
1821
|
+
redirectUri: input.redirectUri,
|
|
1822
|
+
scopes: [...(input.scopes ?? [])],
|
|
1823
|
+
...(codeChallenge === undefined
|
|
1824
|
+
? {}
|
|
1825
|
+
: {
|
|
1826
|
+
codeChallenge,
|
|
1827
|
+
codeChallengeMethod: input.codeChallengeMethod ?? "S256",
|
|
1828
|
+
}),
|
|
1829
|
+
issuedAt,
|
|
1830
|
+
expiresAt,
|
|
1831
|
+
...(input.metadata === undefined
|
|
1832
|
+
? {}
|
|
1833
|
+
: { metadata: metadataValue(input.metadata) }),
|
|
1834
|
+
};
|
|
1835
|
+
yield* dependencies.store.upsert(record).pipe(Effect.mapError((cause) => oauthAuthorizationCodeGrantError("issue", "Failed to store OAuth authorization code", cause)));
|
|
1836
|
+
return issuedAuthorizationCode({ code, record });
|
|
1837
|
+
}),
|
|
1838
|
+
consume: Effect.fn("auth.oauth.authorization_code.consume")(function* (input) {
|
|
1839
|
+
const now = input.now ?? (yield* currentUnixMillis);
|
|
1840
|
+
const codeHash = yield* authorizationCodeHash(dependencies.crypto, authorizationCodeValue(input.code), "consume");
|
|
1841
|
+
const recordOption = yield* dependencies.store.findByHash({ codeHash }).pipe(Effect.mapError((cause) => oauthAuthorizationCodeGrantError("consume", "Failed to load OAuth authorization code", cause)));
|
|
1842
|
+
if (Option.isNone(recordOption)) {
|
|
1843
|
+
return { valid: false, reason: "unknown_code" };
|
|
1844
|
+
}
|
|
1845
|
+
const record = recordOption.value;
|
|
1846
|
+
if (record.consumedAt !== undefined) {
|
|
1847
|
+
return { valid: false, reason: "already_consumed" };
|
|
1848
|
+
}
|
|
1849
|
+
if (Number(now) >= Number(record.expiresAt)) {
|
|
1850
|
+
return { valid: false, reason: "expired" };
|
|
1851
|
+
}
|
|
1852
|
+
if (record.clientId !== input.clientId) {
|
|
1853
|
+
return { valid: false, reason: "client_mismatch" };
|
|
1854
|
+
}
|
|
1855
|
+
if (record.redirectUri !== input.redirectUri) {
|
|
1856
|
+
return { valid: false, reason: "redirect_uri_mismatch" };
|
|
1857
|
+
}
|
|
1858
|
+
if (record.codeChallenge !== undefined) {
|
|
1859
|
+
if (input.codeVerifier === undefined) {
|
|
1860
|
+
return { valid: false, reason: "pkce_required" };
|
|
1861
|
+
}
|
|
1862
|
+
const actualChallenge = yield* pkceChallengeForVerifier(dependencies.crypto, input.codeVerifier);
|
|
1863
|
+
const pkceMatches = yield* timingSafeEqualString(dependencies.crypto, record.codeChallenge, actualChallenge);
|
|
1864
|
+
if (!pkceMatches) {
|
|
1865
|
+
return { valid: false, reason: "pkce_mismatch" };
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
const consumed = yield* dependencies.store
|
|
1869
|
+
.markConsumed({ codeHash, consumedAt: now })
|
|
1870
|
+
.pipe(Effect.mapError((cause) => oauthAuthorizationCodeGrantError("consume", "Failed to consume OAuth authorization code", cause)));
|
|
1871
|
+
if (consumed.status === "missing") {
|
|
1872
|
+
return { valid: false, reason: "unknown_code" };
|
|
1873
|
+
}
|
|
1874
|
+
if (consumed.status === "already_consumed") {
|
|
1875
|
+
return { valid: false, reason: "already_consumed" };
|
|
1876
|
+
}
|
|
1877
|
+
return {
|
|
1878
|
+
valid: true,
|
|
1879
|
+
code: consumedAuthorizationCode(consumed.record, now),
|
|
1880
|
+
};
|
|
1881
|
+
}),
|
|
1882
|
+
});
|
|
1883
|
+
};
|
|
1884
|
+
const oauthAuthorizationCodeTokenInvalid = (reason) => ({ valid: false, reason });
|
|
1885
|
+
const oauthRefreshTokenInvalid = (reason) => ({ valid: false, reason });
|
|
1886
|
+
const oauthRefreshTokenIssueInvalid = (reason) => ({ valid: false, reason });
|
|
1887
|
+
const oauthClientCredentialsInvalid = (reason) => ({ valid: false, reason });
|
|
1888
|
+
const oauthClientCredentialsIssueInvalid = (reason) => ({ valid: false, reason });
|
|
1889
|
+
const oauthTokenRevocationInvalid = (reason) => ({ valid: false, reason });
|
|
1890
|
+
const oauthTokenIntrospectionInvalid = (reason) => ({ valid: false, reason });
|
|
1891
|
+
const oauthTokenGrantString = (value) => {
|
|
1892
|
+
const trimmed = value?.trim();
|
|
1893
|
+
return trimmed === undefined || trimmed === "" ? undefined : trimmed;
|
|
1894
|
+
};
|
|
1895
|
+
const oauthTokenGrantScopes = (scopes) => {
|
|
1896
|
+
const values = typeof scopes === "string" ? scopes.split(/\s+/) : scopes ?? [];
|
|
1897
|
+
const unique = new Set();
|
|
1898
|
+
for (const value of values) {
|
|
1899
|
+
const scope = value.trim();
|
|
1900
|
+
if (scope !== "") {
|
|
1901
|
+
unique.add(scope);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return [...unique];
|
|
1905
|
+
};
|
|
1906
|
+
const oauthTokenGrantScopesAllowed = (scopes, allowedScopes) => allowedScopes === undefined || scopes.every((scope) => allowedScopes.includes(scope));
|
|
1907
|
+
const validateOAuthProviderModeOpaqueTokenBytes = (value, label, error) => Number.isInteger(value) && value >= 16 && value <= 128
|
|
1908
|
+
? Effect.succeed(value)
|
|
1909
|
+
: Effect.fail(error(`${label} byte length must be an integer between 16 and 128`));
|
|
1910
|
+
const generateOAuthProviderModeOpaqueToken = (crypto, byteLength, brand, label, error) => Effect.gen(function* () {
|
|
1911
|
+
const validByteLength = yield* validateOAuthProviderModeOpaqueTokenBytes(byteLength, label, error);
|
|
1912
|
+
const token = yield* crypto.randomToken(validByteLength).pipe(Effect.mapError((cause) => error(`Failed to generate OAuth ${label} token`, cause)));
|
|
1913
|
+
return Redacted.make(brand(token));
|
|
1914
|
+
});
|
|
1915
|
+
const oauthProviderModeOpaqueTokenHash = (crypto, token, error) => crypto.digestSha256({ data: Redacted.value(token) }).pipe(Effect.map((digest) => `sha256:${digest}`), Effect.mapError((cause) => error("Failed to hash OAuth provider-mode token", cause)));
|
|
1916
|
+
const oauthProviderModeOpaqueTokenAudience = (audience) => audience === undefined ? undefined : Array.isArray(audience) ? [...audience] : audience;
|
|
1917
|
+
const oauthProviderModeOpaqueTokenRecord = (dependencies, input) => ({
|
|
1918
|
+
tokenHash: input.tokenHash,
|
|
1919
|
+
tokenType: input.tokenType,
|
|
1920
|
+
clientId: input.clientId,
|
|
1921
|
+
subject: input.subject,
|
|
1922
|
+
scopes: [...input.scopes],
|
|
1923
|
+
issuedAt: input.issuedAt,
|
|
1924
|
+
expiresAt: input.expiresAt,
|
|
1925
|
+
...(dependencies.issuer === undefined ? {} : { issuer: dependencies.issuer }),
|
|
1926
|
+
...(dependencies.audience === undefined
|
|
1927
|
+
? {}
|
|
1928
|
+
: { audience: oauthProviderModeOpaqueTokenAudience(dependencies.audience) }),
|
|
1929
|
+
...(dependencies.metadata === undefined
|
|
1930
|
+
? {}
|
|
1931
|
+
: { metadata: metadataValue(dependencies.metadata) }),
|
|
1932
|
+
});
|
|
1933
|
+
const oauthProviderModeOpaqueExpiresAt = (issuedAt, ttl) => UnixMillis(Number(issuedAt) + Duration.toMillis(ttl));
|
|
1934
|
+
const oauthProviderModeOpaqueExpiresInSeconds = (issuedAt, expiresAt) => Math.max(0, Math.floor((Number(expiresAt) - Number(issuedAt)) / 1000));
|
|
1935
|
+
const oauthProviderModeOpaqueTokenResponse = (input) => ({
|
|
1936
|
+
accessToken: input.accessToken,
|
|
1937
|
+
tokenType: "Bearer",
|
|
1938
|
+
expiresIn: oauthProviderModeOpaqueExpiresInSeconds(input.accessTokenIssuedAt, input.accessTokenExpiresAt),
|
|
1939
|
+
...(input.refreshToken === undefined
|
|
1940
|
+
? {}
|
|
1941
|
+
: { refreshToken: input.refreshToken }),
|
|
1942
|
+
...(input.scopes.length === 0 ? {} : { scopes: [...input.scopes] }),
|
|
1943
|
+
});
|
|
1944
|
+
const oauthProviderModeOpaqueStoreToken = (store, record, error) => store.upsert(record).pipe(Effect.mapError((cause) => error("Failed to store OAuth provider-mode token", cause)));
|
|
1945
|
+
const oauthProviderModeScopesSubset = (scopes, grantedScopes) => scopes.every((scope) => grantedScopes.includes(scope));
|
|
1946
|
+
const oauthTokenGrantClientId = (value) => {
|
|
1947
|
+
const clientId = oauthTokenGrantString(value === undefined ? undefined : String(value));
|
|
1948
|
+
return clientId === undefined ? undefined : OAuthClientId(clientId);
|
|
1949
|
+
};
|
|
1950
|
+
const oauthAuthorizationCodeConsumeFailureToTokenFailure = (reason) => {
|
|
1951
|
+
switch (reason) {
|
|
1952
|
+
case "unknown_code":
|
|
1953
|
+
case "already_consumed":
|
|
1954
|
+
case "expired":
|
|
1955
|
+
case "client_mismatch":
|
|
1956
|
+
case "redirect_uri_mismatch":
|
|
1957
|
+
case "pkce_required":
|
|
1958
|
+
case "pkce_mismatch": {
|
|
1959
|
+
return "invalid_grant";
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
export const makeOAuthAuthorizationCodeTokenGrant = (dependencies) => OAuthAuthorizationCodeTokenGrant.make({
|
|
1964
|
+
exchange: Effect.fn("auth.oauth.authorization_code_token.exchange")(function* (input) {
|
|
1965
|
+
if (input.grantType !== "authorization_code") {
|
|
1966
|
+
return oauthAuthorizationCodeTokenInvalid("unsupported_grant_type");
|
|
1967
|
+
}
|
|
1968
|
+
const { code } = input;
|
|
1969
|
+
const clientId = oauthTokenGrantClientId(input.clientId);
|
|
1970
|
+
const redirectUri = oauthTokenGrantString(input.redirectUri);
|
|
1971
|
+
if (code === undefined || clientId === undefined || redirectUri === undefined) {
|
|
1972
|
+
return oauthAuthorizationCodeTokenInvalid("invalid_request");
|
|
1973
|
+
}
|
|
1974
|
+
const clientOption = yield* dependencies.clients.find({ clientId }).pipe(Effect.mapError((cause) => oauthAuthorizationCodeTokenGrantError("exchange", "Failed to load OAuth client", cause)));
|
|
1975
|
+
if (Option.isNone(clientOption) || clientOption.value.status !== "active") {
|
|
1976
|
+
return oauthAuthorizationCodeTokenInvalid("invalid_client");
|
|
1977
|
+
}
|
|
1978
|
+
const client = clientOption.value;
|
|
1979
|
+
if (!client.allowedGrantTypes.includes("authorization_code")) {
|
|
1980
|
+
return oauthAuthorizationCodeTokenInvalid("unauthorized_client");
|
|
1981
|
+
}
|
|
1982
|
+
if (client.type === "confidential" && input.clientAuthenticated !== true) {
|
|
1983
|
+
return oauthAuthorizationCodeTokenInvalid("invalid_client");
|
|
1984
|
+
}
|
|
1985
|
+
const now = input.now ?? (yield* currentUnixMillis);
|
|
1986
|
+
const consumed = yield* dependencies.authorizationCodes
|
|
1987
|
+
.consume({
|
|
1988
|
+
code,
|
|
1989
|
+
clientId,
|
|
1990
|
+
redirectUri,
|
|
1991
|
+
...(input.codeVerifier === undefined
|
|
1992
|
+
? {}
|
|
1993
|
+
: { codeVerifier: input.codeVerifier }),
|
|
1994
|
+
now,
|
|
1995
|
+
})
|
|
1996
|
+
.pipe(Effect.mapError((cause) => oauthAuthorizationCodeTokenGrantError("exchange", "Failed to consume OAuth authorization code", cause)));
|
|
1997
|
+
if (!consumed.valid) {
|
|
1998
|
+
return oauthAuthorizationCodeTokenInvalid(oauthAuthorizationCodeConsumeFailureToTokenFailure(consumed.reason));
|
|
1999
|
+
}
|
|
2000
|
+
const tokens = yield* dependencies.issuer
|
|
2001
|
+
.issue({ client, code: consumed.code, now })
|
|
2002
|
+
.pipe(Effect.mapError((cause) => oauthAuthorizationCodeTokenGrantError("exchange", "Failed to issue OAuth tokens", cause)));
|
|
2003
|
+
return {
|
|
2004
|
+
valid: true,
|
|
2005
|
+
client: cloneOAuthClientRecord(client),
|
|
2006
|
+
code: consumed.code,
|
|
2007
|
+
tokens: cloneOAuthTokenResponse(tokens),
|
|
2008
|
+
};
|
|
2009
|
+
}),
|
|
2010
|
+
});
|
|
2011
|
+
export const makeOAuthRefreshTokenGrant = (dependencies) => OAuthRefreshTokenGrant.make({
|
|
2012
|
+
exchange: Effect.fn("auth.oauth.refresh_token.exchange")(function* (input) {
|
|
2013
|
+
if (input.grantType !== "refresh_token") {
|
|
2014
|
+
return oauthRefreshTokenInvalid("unsupported_grant_type");
|
|
2015
|
+
}
|
|
2016
|
+
const { refreshToken } = input;
|
|
2017
|
+
const clientId = oauthTokenGrantClientId(input.clientId);
|
|
2018
|
+
if (refreshToken === undefined || clientId === undefined) {
|
|
2019
|
+
return oauthRefreshTokenInvalid("invalid_request");
|
|
2020
|
+
}
|
|
2021
|
+
const clientOption = yield* dependencies.clients.find({ clientId }).pipe(Effect.mapError((cause) => oauthRefreshTokenGrantError("exchange", "Failed to load OAuth client", cause)));
|
|
2022
|
+
if (Option.isNone(clientOption) || clientOption.value.status !== "active") {
|
|
2023
|
+
return oauthRefreshTokenInvalid("invalid_client");
|
|
2024
|
+
}
|
|
2025
|
+
const client = clientOption.value;
|
|
2026
|
+
if (!client.allowedGrantTypes.includes("refresh_token")) {
|
|
2027
|
+
return oauthRefreshTokenInvalid("unauthorized_client");
|
|
2028
|
+
}
|
|
2029
|
+
if (client.type === "confidential" && input.clientAuthenticated !== true) {
|
|
2030
|
+
return oauthRefreshTokenInvalid("invalid_client");
|
|
2031
|
+
}
|
|
2032
|
+
const scopes = oauthTokenGrantScopes(input.scopes);
|
|
2033
|
+
if (!oauthTokenGrantScopesAllowed(scopes, client.allowedScopes)) {
|
|
2034
|
+
return oauthRefreshTokenInvalid("invalid_scope");
|
|
2035
|
+
}
|
|
2036
|
+
const now = input.now ?? (yield* currentUnixMillis);
|
|
2037
|
+
const issued = yield* dependencies.issuer
|
|
2038
|
+
.issue({
|
|
2039
|
+
client,
|
|
2040
|
+
refreshToken,
|
|
2041
|
+
...(scopes.length === 0 ? {} : { scopes }),
|
|
2042
|
+
now,
|
|
2043
|
+
})
|
|
2044
|
+
.pipe(Effect.mapError((cause) => oauthRefreshTokenGrantError("exchange", "Failed to issue OAuth tokens", cause)));
|
|
2045
|
+
if (!issued.valid) {
|
|
2046
|
+
return oauthRefreshTokenInvalid(issued.reason);
|
|
2047
|
+
}
|
|
2048
|
+
return {
|
|
2049
|
+
valid: true,
|
|
2050
|
+
client: cloneOAuthClientRecord(client),
|
|
2051
|
+
tokens: cloneOAuthTokenResponse(issued.tokens),
|
|
2052
|
+
};
|
|
2053
|
+
}),
|
|
2054
|
+
});
|
|
2055
|
+
export const makeOAuthClientCredentialsGrant = (dependencies) => OAuthClientCredentialsGrant.make({
|
|
2056
|
+
exchange: Effect.fn("auth.oauth.client_credentials.exchange")(function* (input) {
|
|
2057
|
+
if (input.grantType !== "client_credentials") {
|
|
2058
|
+
return oauthClientCredentialsInvalid("unsupported_grant_type");
|
|
2059
|
+
}
|
|
2060
|
+
const clientId = oauthTokenGrantClientId(input.clientId);
|
|
2061
|
+
if (clientId === undefined) {
|
|
2062
|
+
return oauthClientCredentialsInvalid("invalid_request");
|
|
2063
|
+
}
|
|
2064
|
+
const clientOption = yield* dependencies.clients.find({ clientId }).pipe(Effect.mapError((cause) => oauthClientCredentialsGrantError("exchange", "Failed to load OAuth client", cause)));
|
|
2065
|
+
if (Option.isNone(clientOption) || clientOption.value.status !== "active") {
|
|
2066
|
+
return oauthClientCredentialsInvalid("invalid_client");
|
|
2067
|
+
}
|
|
2068
|
+
const client = clientOption.value;
|
|
2069
|
+
if (!client.allowedGrantTypes.includes("client_credentials")) {
|
|
2070
|
+
return oauthClientCredentialsInvalid("unauthorized_client");
|
|
2071
|
+
}
|
|
2072
|
+
if (client.type !== "confidential") {
|
|
2073
|
+
return oauthClientCredentialsInvalid("unauthorized_client");
|
|
2074
|
+
}
|
|
2075
|
+
if (input.clientAuthenticated !== true) {
|
|
2076
|
+
return oauthClientCredentialsInvalid("invalid_client");
|
|
2077
|
+
}
|
|
2078
|
+
const scopes = oauthTokenGrantScopes(input.scopes);
|
|
2079
|
+
if (!oauthTokenGrantScopesAllowed(scopes, client.allowedScopes)) {
|
|
2080
|
+
return oauthClientCredentialsInvalid("invalid_scope");
|
|
2081
|
+
}
|
|
2082
|
+
const now = input.now ?? (yield* currentUnixMillis);
|
|
2083
|
+
const issued = yield* dependencies.issuer
|
|
2084
|
+
.issue({
|
|
2085
|
+
client,
|
|
2086
|
+
...(scopes.length === 0 ? {} : { scopes }),
|
|
2087
|
+
now,
|
|
2088
|
+
})
|
|
2089
|
+
.pipe(Effect.mapError((cause) => oauthClientCredentialsGrantError("exchange", "Failed to issue OAuth tokens", cause)));
|
|
2090
|
+
if (!issued.valid) {
|
|
2091
|
+
return oauthClientCredentialsInvalid(issued.reason);
|
|
2092
|
+
}
|
|
2093
|
+
return {
|
|
2094
|
+
valid: true,
|
|
2095
|
+
client: cloneOAuthClientRecord(client),
|
|
2096
|
+
tokens: cloneOAuthTokenResponse(issued.tokens),
|
|
2097
|
+
};
|
|
2098
|
+
}),
|
|
2099
|
+
});
|
|
2100
|
+
export const makeOAuthProviderModeOpaqueTokenIssuer = (dependencies) => {
|
|
2101
|
+
const accessTokenBytes = dependencies.accessTokenBytes ?? defaultOAuthProviderModeAccessTokenBytes;
|
|
2102
|
+
const refreshTokenBytes = dependencies.refreshTokenBytes ?? defaultOAuthProviderModeRefreshTokenBytes;
|
|
2103
|
+
const accessTokenTtl = dependencies.accessTokenTtl ?? defaultOAuthProviderModeAccessTokenTtl;
|
|
2104
|
+
const refreshTokenTtl = dependencies.refreshTokenTtl ?? defaultOAuthProviderModeRefreshTokenTtl;
|
|
2105
|
+
return {
|
|
2106
|
+
authorizationCode: OAuthAuthorizationCodeTokenIssuer.make({
|
|
2107
|
+
issue: Effect.fn("auth.oauth.provider_mode_opaque.authorization_code.issue")(function* (input) {
|
|
2108
|
+
const scopes = [...input.code.scopes];
|
|
2109
|
+
const accessToken = yield* generateOAuthProviderModeOpaqueToken(dependencies.crypto, accessTokenBytes, OAuthAccessToken, "access", oauthAuthorizationCodeTokenIssuerError);
|
|
2110
|
+
const accessTokenHash = yield* oauthProviderModeOpaqueTokenHash(dependencies.crypto, accessToken, oauthAuthorizationCodeTokenIssuerError);
|
|
2111
|
+
const accessTokenExpiresAt = oauthProviderModeOpaqueExpiresAt(input.now, accessTokenTtl);
|
|
2112
|
+
const accessTokenRecord = oauthProviderModeOpaqueTokenRecord(dependencies, {
|
|
2113
|
+
tokenHash: accessTokenHash,
|
|
2114
|
+
tokenType: "access_token",
|
|
2115
|
+
clientId: input.client.id,
|
|
2116
|
+
subject: input.code.subject,
|
|
2117
|
+
scopes,
|
|
2118
|
+
issuedAt: input.now,
|
|
2119
|
+
expiresAt: accessTokenExpiresAt,
|
|
2120
|
+
});
|
|
2121
|
+
yield* oauthProviderModeOpaqueStoreToken(dependencies.tokenStore, accessTokenRecord, oauthAuthorizationCodeTokenIssuerError);
|
|
2122
|
+
if (!input.client.allowedGrantTypes.includes("refresh_token")) {
|
|
2123
|
+
return oauthProviderModeOpaqueTokenResponse({
|
|
2124
|
+
accessToken,
|
|
2125
|
+
accessTokenIssuedAt: input.now,
|
|
2126
|
+
accessTokenExpiresAt,
|
|
2127
|
+
scopes,
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
const refreshToken = yield* generateOAuthProviderModeOpaqueToken(dependencies.crypto, refreshTokenBytes, OAuthRefreshToken, "refresh", oauthAuthorizationCodeTokenIssuerError);
|
|
2131
|
+
const refreshTokenHash = yield* oauthProviderModeOpaqueTokenHash(dependencies.crypto, refreshToken, oauthAuthorizationCodeTokenIssuerError);
|
|
2132
|
+
const refreshTokenRecord = oauthProviderModeOpaqueTokenRecord(dependencies, {
|
|
2133
|
+
tokenHash: refreshTokenHash,
|
|
2134
|
+
tokenType: "refresh_token",
|
|
2135
|
+
clientId: input.client.id,
|
|
2136
|
+
subject: input.code.subject,
|
|
2137
|
+
scopes,
|
|
2138
|
+
issuedAt: input.now,
|
|
2139
|
+
expiresAt: oauthProviderModeOpaqueExpiresAt(input.now, refreshTokenTtl),
|
|
2140
|
+
});
|
|
2141
|
+
yield* oauthProviderModeOpaqueStoreToken(dependencies.tokenStore, refreshTokenRecord, oauthAuthorizationCodeTokenIssuerError);
|
|
2142
|
+
return oauthProviderModeOpaqueTokenResponse({
|
|
2143
|
+
accessToken,
|
|
2144
|
+
accessTokenIssuedAt: input.now,
|
|
2145
|
+
accessTokenExpiresAt,
|
|
2146
|
+
refreshToken,
|
|
2147
|
+
scopes,
|
|
2148
|
+
});
|
|
2149
|
+
}),
|
|
2150
|
+
}),
|
|
2151
|
+
refreshToken: OAuthRefreshTokenIssuer.make({
|
|
2152
|
+
issue: Effect.fn("auth.oauth.provider_mode_opaque.refresh_token.issue")(function* (input) {
|
|
2153
|
+
const tokenHash = yield* oauthProviderModeOpaqueTokenHash(dependencies.crypto, input.refreshToken, oauthRefreshTokenIssuerError);
|
|
2154
|
+
const recordOption = yield* dependencies.tokenStore
|
|
2155
|
+
.findByHash({ tokenHash })
|
|
2156
|
+
.pipe(Effect.mapError((cause) => oauthRefreshTokenIssuerError("Failed to load OAuth provider-mode refresh token", cause)));
|
|
2157
|
+
if (Option.isNone(recordOption)) {
|
|
2158
|
+
return oauthRefreshTokenIssueInvalid("invalid_grant");
|
|
2159
|
+
}
|
|
2160
|
+
const record = recordOption.value;
|
|
2161
|
+
if (record.tokenType !== "refresh_token" ||
|
|
2162
|
+
record.clientId !== input.client.id ||
|
|
2163
|
+
record.revokedAt !== undefined ||
|
|
2164
|
+
record.rotatedAt !== undefined ||
|
|
2165
|
+
Number(input.now) >= Number(record.expiresAt)) {
|
|
2166
|
+
return oauthRefreshTokenIssueInvalid("invalid_grant");
|
|
2167
|
+
}
|
|
2168
|
+
const requestedScopes = oauthTokenGrantScopes(input.scopes);
|
|
2169
|
+
if (!oauthTokenGrantScopesAllowed(requestedScopes, input.client.allowedScopes) ||
|
|
2170
|
+
!oauthProviderModeScopesSubset(requestedScopes, record.scopes)) {
|
|
2171
|
+
return oauthRefreshTokenIssueInvalid("invalid_scope");
|
|
2172
|
+
}
|
|
2173
|
+
const scopes = requestedScopes.length === 0 ? [...record.scopes] : requestedScopes;
|
|
2174
|
+
const accessToken = yield* generateOAuthProviderModeOpaqueToken(dependencies.crypto, accessTokenBytes, OAuthAccessToken, "access", oauthRefreshTokenIssuerError);
|
|
2175
|
+
const accessTokenHash = yield* oauthProviderModeOpaqueTokenHash(dependencies.crypto, accessToken, oauthRefreshTokenIssuerError);
|
|
2176
|
+
const refreshToken = yield* generateOAuthProviderModeOpaqueToken(dependencies.crypto, refreshTokenBytes, OAuthRefreshToken, "refresh", oauthRefreshTokenIssuerError);
|
|
2177
|
+
const refreshTokenHash = yield* oauthProviderModeOpaqueTokenHash(dependencies.crypto, refreshToken, oauthRefreshTokenIssuerError);
|
|
2178
|
+
const accessTokenExpiresAt = oauthProviderModeOpaqueExpiresAt(input.now, accessTokenTtl);
|
|
2179
|
+
yield* oauthProviderModeOpaqueStoreToken(dependencies.tokenStore, oauthProviderModeOpaqueTokenRecord(dependencies, {
|
|
2180
|
+
tokenHash: accessTokenHash,
|
|
2181
|
+
tokenType: "access_token",
|
|
2182
|
+
clientId: input.client.id,
|
|
2183
|
+
subject: record.subject,
|
|
2184
|
+
scopes,
|
|
2185
|
+
issuedAt: input.now,
|
|
2186
|
+
expiresAt: accessTokenExpiresAt,
|
|
2187
|
+
}), oauthRefreshTokenIssuerError);
|
|
2188
|
+
yield* oauthProviderModeOpaqueStoreToken(dependencies.tokenStore, oauthProviderModeOpaqueTokenRecord(dependencies, {
|
|
2189
|
+
tokenHash: refreshTokenHash,
|
|
2190
|
+
tokenType: "refresh_token",
|
|
2191
|
+
clientId: input.client.id,
|
|
2192
|
+
subject: record.subject,
|
|
2193
|
+
scopes,
|
|
2194
|
+
issuedAt: input.now,
|
|
2195
|
+
expiresAt: oauthProviderModeOpaqueExpiresAt(input.now, refreshTokenTtl),
|
|
2196
|
+
}), oauthRefreshTokenIssuerError);
|
|
2197
|
+
const rotated = yield* dependencies.tokenStore
|
|
2198
|
+
.rotateRefreshToken({
|
|
2199
|
+
tokenHash,
|
|
2200
|
+
replacedByTokenHash: refreshTokenHash,
|
|
2201
|
+
rotatedAt: input.now,
|
|
2202
|
+
})
|
|
2203
|
+
.pipe(Effect.mapError((cause) => oauthRefreshTokenIssuerError("Failed to rotate OAuth provider-mode refresh token", cause)));
|
|
2204
|
+
if (Option.isNone(rotated)) {
|
|
2205
|
+
return oauthRefreshTokenIssueInvalid("invalid_grant");
|
|
2206
|
+
}
|
|
2207
|
+
return {
|
|
2208
|
+
valid: true,
|
|
2209
|
+
tokens: oauthProviderModeOpaqueTokenResponse({
|
|
2210
|
+
accessToken,
|
|
2211
|
+
accessTokenIssuedAt: input.now,
|
|
2212
|
+
accessTokenExpiresAt,
|
|
2213
|
+
refreshToken,
|
|
2214
|
+
scopes,
|
|
2215
|
+
}),
|
|
2216
|
+
};
|
|
2217
|
+
}),
|
|
2218
|
+
}),
|
|
2219
|
+
clientCredentials: OAuthClientCredentialsTokenIssuer.make({
|
|
2220
|
+
issue: Effect.fn("auth.oauth.provider_mode_opaque.client_credentials.issue")(function* (input) {
|
|
2221
|
+
const scopes = [...(input.scopes ?? [])];
|
|
2222
|
+
if (!oauthTokenGrantScopesAllowed(scopes, input.client.allowedScopes)) {
|
|
2223
|
+
return oauthClientCredentialsIssueInvalid("invalid_scope");
|
|
2224
|
+
}
|
|
2225
|
+
const subject = yield* Effect.try({
|
|
2226
|
+
try: () => dependencies.clientCredentialsSubject === undefined
|
|
2227
|
+
? String(input.client.id)
|
|
2228
|
+
: dependencies.clientCredentialsSubject(input),
|
|
2229
|
+
catch: (cause) => oauthClientCredentialsTokenIssuerError("Failed to resolve OAuth client credentials subject", cause),
|
|
2230
|
+
}).pipe(Effect.flatMap((value) => {
|
|
2231
|
+
const resolvedSubject = oauthTokenGrantString(value);
|
|
2232
|
+
return resolvedSubject === undefined
|
|
2233
|
+
? Effect.fail(oauthClientCredentialsTokenIssuerError("OAuth client credentials subject must not be empty"))
|
|
2234
|
+
: Effect.succeed(resolvedSubject);
|
|
2235
|
+
}));
|
|
2236
|
+
const accessToken = yield* generateOAuthProviderModeOpaqueToken(dependencies.crypto, accessTokenBytes, OAuthAccessToken, "access", oauthClientCredentialsTokenIssuerError);
|
|
2237
|
+
const accessTokenHash = yield* oauthProviderModeOpaqueTokenHash(dependencies.crypto, accessToken, oauthClientCredentialsTokenIssuerError);
|
|
2238
|
+
const accessTokenExpiresAt = oauthProviderModeOpaqueExpiresAt(input.now, accessTokenTtl);
|
|
2239
|
+
yield* oauthProviderModeOpaqueStoreToken(dependencies.tokenStore, oauthProviderModeOpaqueTokenRecord(dependencies, {
|
|
2240
|
+
tokenHash: accessTokenHash,
|
|
2241
|
+
tokenType: "access_token",
|
|
2242
|
+
clientId: input.client.id,
|
|
2243
|
+
subject,
|
|
2244
|
+
scopes,
|
|
2245
|
+
issuedAt: input.now,
|
|
2246
|
+
expiresAt: accessTokenExpiresAt,
|
|
2247
|
+
}), oauthClientCredentialsTokenIssuerError);
|
|
2248
|
+
return {
|
|
2249
|
+
valid: true,
|
|
2250
|
+
tokens: oauthProviderModeOpaqueTokenResponse({
|
|
2251
|
+
accessToken,
|
|
2252
|
+
accessTokenIssuedAt: input.now,
|
|
2253
|
+
accessTokenExpiresAt,
|
|
2254
|
+
scopes,
|
|
2255
|
+
}),
|
|
2256
|
+
};
|
|
2257
|
+
}),
|
|
2258
|
+
}),
|
|
2259
|
+
};
|
|
2260
|
+
};
|
|
2261
|
+
export const makeOAuthClientSecretVerification = (dependencies) => OAuthClientSecretVerification.make({
|
|
2262
|
+
authenticateClient: Effect.fn("auth.oauth.client_secret.authenticate_client")(function* (input) {
|
|
2263
|
+
const prefix = yield* dependencies.clientSecrets.parse(input.clientSecret).pipe(Effect.catchTag("OAuthClientSecretError", () => Effect.succeed(undefined)));
|
|
2264
|
+
if (prefix === undefined) {
|
|
2265
|
+
return false;
|
|
2266
|
+
}
|
|
2267
|
+
const secret = yield* dependencies.secretStore
|
|
2268
|
+
.findByClientIdAndPrefix({ clientId: input.clientId, prefix })
|
|
2269
|
+
.pipe(Effect.mapError((cause) => oauthClientSecretVerificationError("Failed to load OAuth client secret", cause)));
|
|
2270
|
+
if (Option.isNone(secret) || secret.value.revokedAt !== undefined) {
|
|
2271
|
+
return false;
|
|
2272
|
+
}
|
|
2273
|
+
if (!secret.value.authenticationMethods.includes(input.method)) {
|
|
2274
|
+
return false;
|
|
2275
|
+
}
|
|
2276
|
+
const now = input.now ?? (yield* currentUnixMillis);
|
|
2277
|
+
if (secret.value.expiresAt !== undefined &&
|
|
2278
|
+
Number(secret.value.expiresAt) <= Number(now)) {
|
|
2279
|
+
return false;
|
|
2280
|
+
}
|
|
2281
|
+
const valid = yield* dependencies.clientSecrets
|
|
2282
|
+
.verify({ secret: input.clientSecret, hash: secret.value.secretHash })
|
|
2283
|
+
.pipe(Effect.mapError((cause) => oauthClientSecretVerificationError("Failed to verify OAuth client secret", cause)));
|
|
2284
|
+
if (!valid) {
|
|
2285
|
+
return false;
|
|
2286
|
+
}
|
|
2287
|
+
const updated = yield* dependencies.secretStore
|
|
2288
|
+
.markUsed({
|
|
2289
|
+
clientId: input.clientId,
|
|
2290
|
+
prefix,
|
|
2291
|
+
lastUsedAt: now,
|
|
2292
|
+
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
|
|
2293
|
+
})
|
|
2294
|
+
.pipe(Effect.mapError((cause) => oauthClientSecretVerificationError("Failed to mark OAuth client secret as used", cause)));
|
|
2295
|
+
return Option.isSome(updated);
|
|
2296
|
+
}),
|
|
2297
|
+
});
|
|
2298
|
+
export const makeOAuthTokenRevocation = (dependencies) => OAuthTokenRevocation.make({
|
|
2299
|
+
revoke: Effect.fn("auth.oauth.token_revocation.revoke")(function* (input) {
|
|
2300
|
+
const { token } = input;
|
|
2301
|
+
const tokenValue = token === undefined ? undefined : String(Redacted.value(token)).trim();
|
|
2302
|
+
const clientId = oauthTokenGrantClientId(input.clientId);
|
|
2303
|
+
const tokenTypeHint = oauthTokenGrantString(input.tokenTypeHint);
|
|
2304
|
+
if (token === undefined || tokenValue === undefined || clientId === undefined) {
|
|
2305
|
+
return oauthTokenRevocationInvalid("invalid_request");
|
|
2306
|
+
}
|
|
2307
|
+
if (tokenTypeHint !== undefined &&
|
|
2308
|
+
tokenTypeHint !== "access_token" &&
|
|
2309
|
+
tokenTypeHint !== "refresh_token") {
|
|
2310
|
+
return oauthTokenRevocationInvalid("unsupported_token_type");
|
|
2311
|
+
}
|
|
2312
|
+
const clientOption = yield* dependencies.clients.find({ clientId }).pipe(Effect.mapError((cause) => oauthTokenRevocationError("revoke", "Failed to load OAuth client", cause)));
|
|
2313
|
+
if (Option.isNone(clientOption) || clientOption.value.status !== "active") {
|
|
2314
|
+
return oauthTokenRevocationInvalid("invalid_client");
|
|
2315
|
+
}
|
|
2316
|
+
const client = clientOption.value;
|
|
2317
|
+
if (client.type !== "confidential" || input.clientAuthenticated !== true) {
|
|
2318
|
+
return oauthTokenRevocationInvalid("invalid_client");
|
|
2319
|
+
}
|
|
2320
|
+
const now = input.now ?? (yield* currentUnixMillis);
|
|
2321
|
+
yield* dependencies.revoker
|
|
2322
|
+
.revoke({
|
|
2323
|
+
client,
|
|
2324
|
+
token,
|
|
2325
|
+
...(tokenTypeHint === undefined ? {} : { tokenTypeHint }),
|
|
2326
|
+
now,
|
|
2327
|
+
})
|
|
2328
|
+
.pipe(Effect.mapError((cause) => oauthTokenRevocationError("revoke", "Failed to revoke OAuth token", cause)));
|
|
2329
|
+
return {
|
|
2330
|
+
valid: true,
|
|
2331
|
+
client: cloneOAuthClientRecord(client),
|
|
2332
|
+
};
|
|
2333
|
+
}),
|
|
2334
|
+
});
|
|
2335
|
+
export const makeOAuthTokenIntrospection = (dependencies) => OAuthTokenIntrospection.make({
|
|
2336
|
+
introspect: Effect.fn("auth.oauth.token_introspection.introspect")(function* (input) {
|
|
2337
|
+
const { token } = input;
|
|
2338
|
+
const tokenValue = token === undefined ? undefined : String(Redacted.value(token)).trim();
|
|
2339
|
+
const clientId = oauthTokenGrantClientId(input.clientId);
|
|
2340
|
+
const tokenTypeHint = oauthTokenGrantString(input.tokenTypeHint);
|
|
2341
|
+
if (token === undefined || tokenValue === undefined || clientId === undefined) {
|
|
2342
|
+
return oauthTokenIntrospectionInvalid("invalid_request");
|
|
2343
|
+
}
|
|
2344
|
+
const clientOption = yield* dependencies.clients.find({ clientId }).pipe(Effect.mapError((cause) => oauthTokenIntrospectionError("introspect", "Failed to load OAuth client", cause)));
|
|
2345
|
+
if (Option.isNone(clientOption) || clientOption.value.status !== "active") {
|
|
2346
|
+
return oauthTokenIntrospectionInvalid("invalid_client");
|
|
2347
|
+
}
|
|
2348
|
+
const client = clientOption.value;
|
|
2349
|
+
if (client.type !== "confidential" || input.clientAuthenticated !== true) {
|
|
2350
|
+
return oauthTokenIntrospectionInvalid("invalid_client");
|
|
2351
|
+
}
|
|
2352
|
+
const now = input.now ?? (yield* currentUnixMillis);
|
|
2353
|
+
const introspection = yield* dependencies.introspector
|
|
2354
|
+
.introspect({
|
|
2355
|
+
client,
|
|
2356
|
+
token,
|
|
2357
|
+
...(tokenTypeHint === undefined ? {} : { tokenTypeHint }),
|
|
2358
|
+
now,
|
|
2359
|
+
})
|
|
2360
|
+
.pipe(Effect.mapError((cause) => oauthTokenIntrospectionError("introspect", "Failed to introspect OAuth token", cause)));
|
|
2361
|
+
return {
|
|
2362
|
+
valid: true,
|
|
2363
|
+
client: cloneOAuthClientRecord(client),
|
|
2364
|
+
introspection: cloneOAuthTokenIntrospectionResponse(introspection),
|
|
2365
|
+
};
|
|
2366
|
+
}),
|
|
2367
|
+
});
|
|
2368
|
+
const oauthProviderAuthorizationInvalid = (reason, input) => ({
|
|
2369
|
+
valid: false,
|
|
2370
|
+
reason,
|
|
2371
|
+
...(input?.redirectUri === undefined ? {} : { redirectUri: input.redirectUri }),
|
|
2372
|
+
...(input?.state === undefined ? {} : { state: input.state }),
|
|
2373
|
+
...(input?.client === undefined
|
|
2374
|
+
? {}
|
|
2375
|
+
: { client: cloneOAuthClientRecord(input.client) }),
|
|
2376
|
+
...(input?.errorDescription === undefined
|
|
2377
|
+
? {}
|
|
2378
|
+
: { errorDescription: input.errorDescription }),
|
|
2379
|
+
});
|
|
2380
|
+
const oauthProviderAuthorizationString = (value) => {
|
|
2381
|
+
const trimmed = value?.trim();
|
|
2382
|
+
return trimmed === undefined || trimmed === "" ? undefined : trimmed;
|
|
2383
|
+
};
|
|
2384
|
+
const oauthProviderAuthorizationClientId = (value) => {
|
|
2385
|
+
const clientId = oauthProviderAuthorizationString(value === undefined ? undefined : String(value));
|
|
2386
|
+
return clientId === undefined ? undefined : OAuthClientId(clientId);
|
|
2387
|
+
};
|
|
2388
|
+
const oauthProviderAuthorizationScopes = (scopes) => {
|
|
2389
|
+
const values = typeof scopes === "string" ? scopes.split(/\s+/) : scopes ?? [];
|
|
2390
|
+
const unique = new Set();
|
|
2391
|
+
for (const value of values) {
|
|
2392
|
+
const scope = value.trim();
|
|
2393
|
+
if (scope !== "") {
|
|
2394
|
+
unique.add(scope);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
return [...unique];
|
|
2398
|
+
};
|
|
2399
|
+
const oauthProviderAuthorizationScopeAllowed = (scopes, allowedScopes) => allowedScopes === undefined || scopes.every((scope) => allowedScopes.includes(scope));
|
|
2400
|
+
const oauthProviderAuthorizationScopeSubset = (scopes, requestedScopes) => scopes.every((scope) => requestedScopes.includes(scope));
|
|
2401
|
+
const oauthProviderAuthorizationClientAllowsCode = (client) => client.allowedResponseTypes.includes("code") &&
|
|
2402
|
+
client.allowedGrantTypes.includes("authorization_code");
|
|
2403
|
+
const oauthProviderAuthorizationGrantedScopesValid = (scopes, requestedScopes, allowedScopes) => oauthProviderAuthorizationScopeSubset(scopes, requestedScopes) &&
|
|
2404
|
+
oauthProviderAuthorizationScopeAllowed(scopes, allowedScopes);
|
|
2405
|
+
export const makeOAuthProviderAuthorization = (dependencies) => OAuthProviderAuthorization.make({
|
|
2406
|
+
authorize: Effect.fn("auth.oauth.provider_authorization.authorize")(function* (input) {
|
|
2407
|
+
const state = oauthProviderAuthorizationString(input.state);
|
|
2408
|
+
const clientId = oauthProviderAuthorizationClientId(input.clientId);
|
|
2409
|
+
const redirectUri = oauthProviderAuthorizationString(input.redirectUri);
|
|
2410
|
+
if (clientId === undefined || redirectUri === undefined) {
|
|
2411
|
+
return oauthProviderAuthorizationInvalid("invalid_request");
|
|
2412
|
+
}
|
|
2413
|
+
const clientOption = yield* dependencies.clients.find({ clientId }).pipe(Effect.mapError((cause) => oauthProviderAuthorizationError("authorize", "Failed to load OAuth client", cause)));
|
|
2414
|
+
if (Option.isNone(clientOption) || clientOption.value.status !== "active") {
|
|
2415
|
+
return oauthProviderAuthorizationInvalid("invalid_request");
|
|
2416
|
+
}
|
|
2417
|
+
const client = clientOption.value;
|
|
2418
|
+
if (!client.redirectUris.includes(redirectUri)) {
|
|
2419
|
+
return oauthProviderAuthorizationInvalid("invalid_request");
|
|
2420
|
+
}
|
|
2421
|
+
const redirectError = (reason, errorDescription) => oauthProviderAuthorizationInvalid(reason, {
|
|
2422
|
+
client,
|
|
2423
|
+
redirectUri,
|
|
2424
|
+
state,
|
|
2425
|
+
...(errorDescription === undefined ? {} : { errorDescription }),
|
|
2426
|
+
});
|
|
2427
|
+
if (input.responseType !== "code") {
|
|
2428
|
+
return redirectError("unsupported_response_type");
|
|
2429
|
+
}
|
|
2430
|
+
if (!oauthProviderAuthorizationClientAllowsCode(client)) {
|
|
2431
|
+
return redirectError("unauthorized_client");
|
|
2432
|
+
}
|
|
2433
|
+
const codeChallenge = oauthProviderAuthorizationString(input.codeChallenge === undefined ? undefined : String(input.codeChallenge));
|
|
2434
|
+
if (codeChallenge === undefined ||
|
|
2435
|
+
oauthProviderAuthorizationString(input.codeChallengeMethod) !== "S256") {
|
|
2436
|
+
return redirectError("invalid_request");
|
|
2437
|
+
}
|
|
2438
|
+
const requestedScopes = oauthProviderAuthorizationScopes(input.scopes);
|
|
2439
|
+
if (!oauthProviderAuthorizationScopeAllowed(requestedScopes, client.allowedScopes)) {
|
|
2440
|
+
return redirectError("invalid_scope");
|
|
2441
|
+
}
|
|
2442
|
+
const decision = yield* dependencies.policy
|
|
2443
|
+
.decide({
|
|
2444
|
+
client: cloneOAuthClientRecord(client),
|
|
2445
|
+
requestedScopes,
|
|
2446
|
+
...(state === undefined ? {} : { state }),
|
|
2447
|
+
request: input,
|
|
2448
|
+
})
|
|
2449
|
+
.pipe(Effect.mapError((cause) => oauthProviderAuthorizationError("authorize", "Failed to decide OAuth authorization request", cause)));
|
|
2450
|
+
if (decision.type === "deny") {
|
|
2451
|
+
return redirectError(decision.reason, decision.errorDescription);
|
|
2452
|
+
}
|
|
2453
|
+
const scopes = oauthProviderAuthorizationScopes(decision.scopes ?? requestedScopes);
|
|
2454
|
+
if (!oauthProviderAuthorizationGrantedScopesValid(scopes, requestedScopes, client.allowedScopes)) {
|
|
2455
|
+
return redirectError("invalid_scope");
|
|
2456
|
+
}
|
|
2457
|
+
const code = yield* dependencies.authorizationCodes
|
|
2458
|
+
.issue({
|
|
2459
|
+
clientId,
|
|
2460
|
+
subject: decision.subject,
|
|
2461
|
+
redirectUri,
|
|
2462
|
+
scopes,
|
|
2463
|
+
codeChallenge: OAuthCodeChallenge(codeChallenge),
|
|
2464
|
+
codeChallengeMethod: "S256",
|
|
2465
|
+
...(input.now === undefined ? {} : { issuedAt: input.now }),
|
|
2466
|
+
...(decision.metadata === undefined
|
|
2467
|
+
? {}
|
|
2468
|
+
: { metadata: decision.metadata }),
|
|
2469
|
+
})
|
|
2470
|
+
.pipe(Effect.mapError((cause) => oauthProviderAuthorizationError("authorize", "Failed to issue OAuth authorization code", cause)));
|
|
2471
|
+
return {
|
|
2472
|
+
valid: true,
|
|
2473
|
+
client: cloneOAuthClientRecord(client),
|
|
2474
|
+
redirectUri,
|
|
2475
|
+
...(state === undefined ? {} : { state }),
|
|
2476
|
+
code,
|
|
2477
|
+
};
|
|
2478
|
+
}),
|
|
2479
|
+
});
|
|
2480
|
+
export const issueOAuthAuthorizationCode = (input) => OAuthAuthorizationCodeGrant.use((grant) => grant.issue(input));
|
|
2481
|
+
export const consumeOAuthAuthorizationCode = (input) => OAuthAuthorizationCodeGrant.use((grant) => grant.consume(input));
|
|
2482
|
+
export const exchangeOAuthAuthorizationCodeToken = (input) => OAuthAuthorizationCodeTokenGrant.use((grant) => grant.exchange(input));
|
|
2483
|
+
export const exchangeOAuthRefreshToken = (input) => OAuthRefreshTokenGrant.use((grant) => grant.exchange(input));
|
|
2484
|
+
export const exchangeOAuthClientCredentialsToken = (input) => OAuthClientCredentialsGrant.use((grant) => grant.exchange(input));
|
|
2485
|
+
export const revokeOAuthToken = (input) => OAuthTokenRevocation.use((revocation) => revocation.revoke(input));
|
|
2486
|
+
export const introspectOAuthToken = (input) => OAuthTokenIntrospection.use((introspection) => introspection.introspect(input));
|
|
2487
|
+
export const authorizeOAuthProviderAuthorization = (input) => OAuthProviderAuthorization.use((authorization) => authorization.authorize(input));
|
|
1407
2488
|
export const makeOAuthPkce = (input) => Effect.gen(function* () {
|
|
1408
2489
|
const crypto = yield* Crypto;
|
|
1409
2490
|
return yield* makeOAuthPkceWithCrypto({ crypto }, input);
|
|
@@ -1528,6 +2609,72 @@ export const OAuthStateLive = Layer.effect(OAuthState)(Effect.gen(function* () {
|
|
|
1528
2609
|
const challenge = yield* Challenge;
|
|
1529
2610
|
return makeOAuthState({ crypto, challenge });
|
|
1530
2611
|
}));
|
|
2612
|
+
export const OAuthAuthorizationCodeGrantLive = Layer.effect(OAuthAuthorizationCodeGrant)(Effect.gen(function* () {
|
|
2613
|
+
const crypto = yield* Crypto;
|
|
2614
|
+
const store = yield* OAuthAuthorizationCodeStore;
|
|
2615
|
+
return makeOAuthAuthorizationCodeGrant({ crypto, store });
|
|
2616
|
+
}));
|
|
2617
|
+
export const OAuthAuthorizationCodeTokenGrantLive = Layer.effect(OAuthAuthorizationCodeTokenGrant)(Effect.gen(function* () {
|
|
2618
|
+
const clients = yield* OAuthClientRegistry;
|
|
2619
|
+
const authorizationCodes = yield* OAuthAuthorizationCodeGrant;
|
|
2620
|
+
const issuer = yield* OAuthAuthorizationCodeTokenIssuer;
|
|
2621
|
+
return makeOAuthAuthorizationCodeTokenGrant({
|
|
2622
|
+
clients,
|
|
2623
|
+
authorizationCodes,
|
|
2624
|
+
issuer,
|
|
2625
|
+
});
|
|
2626
|
+
}));
|
|
2627
|
+
export const OAuthRefreshTokenGrantLive = Layer.effect(OAuthRefreshTokenGrant)(Effect.gen(function* () {
|
|
2628
|
+
const clients = yield* OAuthClientRegistry;
|
|
2629
|
+
const issuer = yield* OAuthRefreshTokenIssuer;
|
|
2630
|
+
return makeOAuthRefreshTokenGrant({ clients, issuer });
|
|
2631
|
+
}));
|
|
2632
|
+
export const OAuthClientCredentialsGrantLive = Layer.effect(OAuthClientCredentialsGrant)(Effect.gen(function* () {
|
|
2633
|
+
const clients = yield* OAuthClientRegistry;
|
|
2634
|
+
const issuer = yield* OAuthClientCredentialsTokenIssuer;
|
|
2635
|
+
return makeOAuthClientCredentialsGrant({ clients, issuer });
|
|
2636
|
+
}));
|
|
2637
|
+
export const OAuthProviderModeOpaqueTokenIssuerLive = (options = {}) => Layer.mergeAll(Layer.effect(OAuthAuthorizationCodeTokenIssuer)(Effect.gen(function* () {
|
|
2638
|
+
const crypto = yield* Crypto;
|
|
2639
|
+
const tokenStore = yield* OAuthProviderModeTokenStore;
|
|
2640
|
+
return makeOAuthProviderModeOpaqueTokenIssuer({
|
|
2641
|
+
...options,
|
|
2642
|
+
crypto,
|
|
2643
|
+
tokenStore,
|
|
2644
|
+
}).authorizationCode;
|
|
2645
|
+
})), Layer.effect(OAuthRefreshTokenIssuer)(Effect.gen(function* () {
|
|
2646
|
+
const crypto = yield* Crypto;
|
|
2647
|
+
const tokenStore = yield* OAuthProviderModeTokenStore;
|
|
2648
|
+
return makeOAuthProviderModeOpaqueTokenIssuer({
|
|
2649
|
+
...options,
|
|
2650
|
+
crypto,
|
|
2651
|
+
tokenStore,
|
|
2652
|
+
}).refreshToken;
|
|
2653
|
+
})), Layer.effect(OAuthClientCredentialsTokenIssuer)(Effect.gen(function* () {
|
|
2654
|
+
const crypto = yield* Crypto;
|
|
2655
|
+
const tokenStore = yield* OAuthProviderModeTokenStore;
|
|
2656
|
+
return makeOAuthProviderModeOpaqueTokenIssuer({
|
|
2657
|
+
...options,
|
|
2658
|
+
crypto,
|
|
2659
|
+
tokenStore,
|
|
2660
|
+
}).clientCredentials;
|
|
2661
|
+
})));
|
|
2662
|
+
export const OAuthTokenRevocationLive = Layer.effect(OAuthTokenRevocation)(Effect.gen(function* () {
|
|
2663
|
+
const clients = yield* OAuthClientRegistry;
|
|
2664
|
+
const revoker = yield* OAuthTokenRevoker;
|
|
2665
|
+
return makeOAuthTokenRevocation({ clients, revoker });
|
|
2666
|
+
}));
|
|
2667
|
+
export const OAuthTokenIntrospectionLive = Layer.effect(OAuthTokenIntrospection)(Effect.gen(function* () {
|
|
2668
|
+
const clients = yield* OAuthClientRegistry;
|
|
2669
|
+
const introspector = yield* OAuthTokenIntrospector;
|
|
2670
|
+
return makeOAuthTokenIntrospection({ clients, introspector });
|
|
2671
|
+
}));
|
|
2672
|
+
export const OAuthProviderAuthorizationLive = Layer.effect(OAuthProviderAuthorization)(Effect.gen(function* () {
|
|
2673
|
+
const clients = yield* OAuthClientRegistry;
|
|
2674
|
+
const authorizationCodes = yield* OAuthAuthorizationCodeGrant;
|
|
2675
|
+
const policy = yield* OAuthProviderAuthorizationPolicy;
|
|
2676
|
+
return makeOAuthProviderAuthorization({ clients, authorizationCodes, policy });
|
|
2677
|
+
}));
|
|
1531
2678
|
export const makeOAuthProviders = (providers) => {
|
|
1532
2679
|
const providersById = new Map(providers.map((provider) => [provider.id, cloneProviderConfig(provider)]));
|
|
1533
2680
|
return OAuthProviders.make({
|
|
@@ -1540,6 +2687,261 @@ export const makeOAuthProviders = (providers) => {
|
|
|
1540
2687
|
});
|
|
1541
2688
|
};
|
|
1542
2689
|
export const OAuthProvidersLive = (providers) => Layer.succeed(OAuthProviders)(makeOAuthProviders(providers));
|
|
2690
|
+
export const OAuthClientSecretsLive = Layer.effect(OAuthClientSecrets)(Effect.gen(function* () {
|
|
2691
|
+
const crypto = yield* Crypto;
|
|
2692
|
+
return makeOAuthClientSecrets({ crypto });
|
|
2693
|
+
}));
|
|
2694
|
+
export const OAuthClientSecretVerificationLive = Layer.effect(OAuthClientSecretVerification)(Effect.gen(function* () {
|
|
2695
|
+
const clientSecrets = yield* OAuthClientSecrets;
|
|
2696
|
+
const secretStore = yield* OAuthClientSecretStore;
|
|
2697
|
+
return makeOAuthClientSecretVerification({ clientSecrets, secretStore });
|
|
2698
|
+
}));
|
|
2699
|
+
export const makeOAuthClientRegistryFromStore = (store) => OAuthClientRegistry.make({
|
|
2700
|
+
find: (input) => store.find(input).pipe(Effect.mapError((cause) => oauthClientRegistryError("find", "Failed to load OAuth client", cause))),
|
|
2701
|
+
validateRedirectUri: (input) => store.validateRedirectUri(input).pipe(Effect.mapError((cause) => oauthClientRegistryError("validate-redirect-uri", "Failed to validate OAuth client redirect URI", cause))),
|
|
2702
|
+
});
|
|
2703
|
+
export const OAuthClientRegistryFromStoreLive = Layer.effect(OAuthClientRegistry)(Effect.gen(function* () {
|
|
2704
|
+
const store = yield* OAuthClientStore;
|
|
2705
|
+
return makeOAuthClientRegistryFromStore(store);
|
|
2706
|
+
}));
|
|
2707
|
+
export const makeOAuthClientRegistryMemory = (clients = []) => {
|
|
2708
|
+
const clientsById = new Map(clients.map((client) => [client.id, cloneOAuthClientRecord(client)]));
|
|
2709
|
+
return OAuthClientRegistry.make({
|
|
2710
|
+
find: (input) => Effect.sync(() => {
|
|
2711
|
+
const client = clientsById.get(input.clientId);
|
|
2712
|
+
return client === undefined
|
|
2713
|
+
? Option.none()
|
|
2714
|
+
: Option.some(cloneOAuthClientRecord(client));
|
|
2715
|
+
}),
|
|
2716
|
+
validateRedirectUri: (input) => Effect.sync(() => {
|
|
2717
|
+
const client = clientsById.get(input.clientId);
|
|
2718
|
+
return validateOAuthClientRedirectUriRecord(client, input);
|
|
2719
|
+
}),
|
|
2720
|
+
});
|
|
2721
|
+
};
|
|
2722
|
+
export const OAuthClientRegistryMemoryLive = (clients = []) => Layer.succeed(OAuthClientRegistry)(makeOAuthClientRegistryMemory(clients));
|
|
2723
|
+
export const makeOAuthClientStoreMemory = (clients = []) => {
|
|
2724
|
+
const clientsById = new Map(clients.map((client) => [client.id, cloneOAuthClientRecord(client)]));
|
|
2725
|
+
return OAuthClientStore.make({
|
|
2726
|
+
upsert: (record) => Effect.sync(() => {
|
|
2727
|
+
clientsById.set(record.id, cloneOAuthClientRecord(record));
|
|
2728
|
+
}),
|
|
2729
|
+
find: (input) => Effect.sync(() => {
|
|
2730
|
+
const client = clientsById.get(input.clientId);
|
|
2731
|
+
return client === undefined
|
|
2732
|
+
? Option.none()
|
|
2733
|
+
: Option.some(cloneOAuthClientRecord(client));
|
|
2734
|
+
}),
|
|
2735
|
+
list: (input = {}) => Effect.sync(() => [...clientsById.values()]
|
|
2736
|
+
.filter((client) => input.includeDisabled === true || client.status === "active")
|
|
2737
|
+
.map(cloneOAuthClientRecord)),
|
|
2738
|
+
validateRedirectUri: (input) => Effect.sync(() => validateOAuthClientRedirectUriRecord(clientsById.get(input.clientId), input)),
|
|
2739
|
+
});
|
|
2740
|
+
};
|
|
2741
|
+
export const OAuthClientStoreMemoryLive = (clients = []) => Layer.succeed(OAuthClientStore)(makeOAuthClientStoreMemory(clients));
|
|
2742
|
+
const oauthConsentMemoryKey = (input) => `${input.userId}\u0000${input.clientId}`;
|
|
2743
|
+
export const makeOAuthConsentStoreMemory = (records = []) => {
|
|
2744
|
+
const recordsByKey = new Map(records.map((record) => [
|
|
2745
|
+
oauthConsentMemoryKey(record),
|
|
2746
|
+
cloneOAuthConsentRecord(record),
|
|
2747
|
+
]));
|
|
2748
|
+
return OAuthConsentStore.make({
|
|
2749
|
+
upsert: (record) => Effect.sync(() => {
|
|
2750
|
+
recordsByKey.set(oauthConsentMemoryKey(record), cloneOAuthConsentRecord(record));
|
|
2751
|
+
}),
|
|
2752
|
+
findActive: (input) => Effect.sync(() => {
|
|
2753
|
+
const record = recordsByKey.get(oauthConsentMemoryKey(input));
|
|
2754
|
+
return record === undefined ||
|
|
2755
|
+
!isOAuthConsentActive(record, input.now, input.scopes)
|
|
2756
|
+
? Option.none()
|
|
2757
|
+
: Option.some(cloneOAuthConsentRecord(record));
|
|
2758
|
+
}),
|
|
2759
|
+
listForUser: (input) => Effect.sync(() => [...recordsByKey.values()]
|
|
2760
|
+
.filter((record) => record.userId === input.userId)
|
|
2761
|
+
.filter((record) => input.includeInactive === true ||
|
|
2762
|
+
isOAuthConsentActive(record, input.now))
|
|
2763
|
+
.map(cloneOAuthConsentRecord)),
|
|
2764
|
+
revoke: (input) => Effect.sync(() => {
|
|
2765
|
+
const key = oauthConsentMemoryKey(input);
|
|
2766
|
+
const record = recordsByKey.get(key);
|
|
2767
|
+
if (record === undefined) {
|
|
2768
|
+
return Option.none();
|
|
2769
|
+
}
|
|
2770
|
+
const updated = cloneOAuthConsentRecord({
|
|
2771
|
+
...record,
|
|
2772
|
+
revokedAt: record.revokedAt ?? input.revokedAt,
|
|
2773
|
+
metadata: input.reason === undefined
|
|
2774
|
+
? record.metadata
|
|
2775
|
+
: { ...record.metadata, revokeReason: input.reason },
|
|
2776
|
+
});
|
|
2777
|
+
recordsByKey.set(key, updated);
|
|
2778
|
+
return Option.some(cloneOAuthConsentRecord(updated));
|
|
2779
|
+
}),
|
|
2780
|
+
deleteInactive: (input) => Effect.sync(() => {
|
|
2781
|
+
let deleted = 0;
|
|
2782
|
+
for (const [key, record] of recordsByKey) {
|
|
2783
|
+
const expired = input.beforeExpiresAt !== undefined &&
|
|
2784
|
+
record.expiresAt !== undefined &&
|
|
2785
|
+
Number(record.expiresAt) < Number(input.beforeExpiresAt);
|
|
2786
|
+
const revoked = input.beforeRevokedAt !== undefined &&
|
|
2787
|
+
record.revokedAt !== undefined &&
|
|
2788
|
+
Number(record.revokedAt) < Number(input.beforeRevokedAt);
|
|
2789
|
+
if (expired || revoked) {
|
|
2790
|
+
recordsByKey.delete(key);
|
|
2791
|
+
deleted += 1;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
return deleted;
|
|
2795
|
+
}),
|
|
2796
|
+
});
|
|
2797
|
+
};
|
|
2798
|
+
export const OAuthConsentStoreMemoryLive = (records = []) => Layer.succeed(OAuthConsentStore)(makeOAuthConsentStoreMemory(records));
|
|
2799
|
+
const oauthClientSecretMemoryKey = (input) => `${input.clientId}\u0000${input.prefix}`;
|
|
2800
|
+
export const makeOAuthClientSecretStoreMemory = () => {
|
|
2801
|
+
const recordsByKey = new Map();
|
|
2802
|
+
return OAuthClientSecretStore.make({
|
|
2803
|
+
upsert: (record) => Effect.sync(() => {
|
|
2804
|
+
recordsByKey.set(oauthClientSecretMemoryKey(record), cloneOAuthClientSecretRecord(record));
|
|
2805
|
+
}),
|
|
2806
|
+
findByClientIdAndPrefix: (input) => Effect.sync(() => {
|
|
2807
|
+
const record = recordsByKey.get(oauthClientSecretMemoryKey(input));
|
|
2808
|
+
return record === undefined
|
|
2809
|
+
? Option.none()
|
|
2810
|
+
: Option.some(cloneOAuthClientSecretRecord(record));
|
|
2811
|
+
}),
|
|
2812
|
+
markUsed: (input) => Effect.sync(() => {
|
|
2813
|
+
const key = oauthClientSecretMemoryKey(input);
|
|
2814
|
+
const record = recordsByKey.get(key);
|
|
2815
|
+
if (record === undefined || record.revokedAt !== undefined) {
|
|
2816
|
+
return Option.none();
|
|
2817
|
+
}
|
|
2818
|
+
const updated = cloneOAuthClientSecretRecord({
|
|
2819
|
+
...record,
|
|
2820
|
+
lastUsedAt: input.lastUsedAt,
|
|
2821
|
+
metadata: input.metadata ?? record.metadata,
|
|
2822
|
+
});
|
|
2823
|
+
recordsByKey.set(key, updated);
|
|
2824
|
+
return Option.some(cloneOAuthClientSecretRecord(updated));
|
|
2825
|
+
}),
|
|
2826
|
+
revoke: (input) => Effect.sync(() => {
|
|
2827
|
+
const key = oauthClientSecretMemoryKey(input);
|
|
2828
|
+
const record = recordsByKey.get(key);
|
|
2829
|
+
if (record === undefined) {
|
|
2830
|
+
return Option.none();
|
|
2831
|
+
}
|
|
2832
|
+
const updated = cloneOAuthClientSecretRecord({
|
|
2833
|
+
...record,
|
|
2834
|
+
revokedAt: input.revokedAt,
|
|
2835
|
+
metadata: input.reason === undefined
|
|
2836
|
+
? record.metadata
|
|
2837
|
+
: { ...record.metadata, revokeReason: input.reason },
|
|
2838
|
+
});
|
|
2839
|
+
recordsByKey.set(key, updated);
|
|
2840
|
+
return Option.some(cloneOAuthClientSecretRecord(updated));
|
|
2841
|
+
}),
|
|
2842
|
+
});
|
|
2843
|
+
};
|
|
2844
|
+
export const OAuthClientSecretStoreMemoryLive = Layer.sync(OAuthClientSecretStore)(makeOAuthClientSecretStoreMemory);
|
|
2845
|
+
export const makeOAuthAuthorizationCodeStoreMemory = () => {
|
|
2846
|
+
const recordsByHash = new Map();
|
|
2847
|
+
return OAuthAuthorizationCodeStore.make({
|
|
2848
|
+
upsert: (record) => Effect.sync(() => {
|
|
2849
|
+
recordsByHash.set(record.codeHash, cloneAuthorizationCodeRecord(record));
|
|
2850
|
+
}),
|
|
2851
|
+
findByHash: (input) => Effect.sync(() => {
|
|
2852
|
+
const record = recordsByHash.get(input.codeHash);
|
|
2853
|
+
return record === undefined
|
|
2854
|
+
? Option.none()
|
|
2855
|
+
: Option.some(cloneAuthorizationCodeRecord(record));
|
|
2856
|
+
}),
|
|
2857
|
+
markConsumed: (input) => Effect.sync(() => {
|
|
2858
|
+
const record = recordsByHash.get(input.codeHash);
|
|
2859
|
+
if (record === undefined) {
|
|
2860
|
+
return { status: "missing" };
|
|
2861
|
+
}
|
|
2862
|
+
if (record.consumedAt !== undefined) {
|
|
2863
|
+
return {
|
|
2864
|
+
status: "already_consumed",
|
|
2865
|
+
record: cloneAuthorizationCodeRecord(record),
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
const consumed = cloneAuthorizationCodeRecord({
|
|
2869
|
+
...record,
|
|
2870
|
+
consumedAt: input.consumedAt,
|
|
2871
|
+
});
|
|
2872
|
+
recordsByHash.set(input.codeHash, consumed);
|
|
2873
|
+
return {
|
|
2874
|
+
status: "consumed",
|
|
2875
|
+
record: cloneAuthorizationCodeRecord(consumed),
|
|
2876
|
+
};
|
|
2877
|
+
}),
|
|
2878
|
+
});
|
|
2879
|
+
};
|
|
2880
|
+
export const OAuthAuthorizationCodeStoreMemoryLive = Layer.sync(OAuthAuthorizationCodeStore)(makeOAuthAuthorizationCodeStoreMemory);
|
|
2881
|
+
export const makeOAuthProviderModeTokenStoreMemory = () => {
|
|
2882
|
+
const recordsByHash = new Map();
|
|
2883
|
+
return OAuthProviderModeTokenStore.make({
|
|
2884
|
+
upsert: (record) => Effect.sync(() => {
|
|
2885
|
+
recordsByHash.set(record.tokenHash, cloneOAuthProviderModeTokenRecord(record));
|
|
2886
|
+
}),
|
|
2887
|
+
findByHash: (input) => Effect.sync(() => {
|
|
2888
|
+
const record = recordsByHash.get(input.tokenHash);
|
|
2889
|
+
return record === undefined
|
|
2890
|
+
? Option.none()
|
|
2891
|
+
: Option.some(cloneOAuthProviderModeTokenRecord(record));
|
|
2892
|
+
}),
|
|
2893
|
+
revokeByHash: (input) => Effect.sync(() => {
|
|
2894
|
+
const record = recordsByHash.get(input.tokenHash);
|
|
2895
|
+
if (record === undefined ||
|
|
2896
|
+
(input.tokenTypeHint !== undefined &&
|
|
2897
|
+
record.tokenType !== input.tokenTypeHint)) {
|
|
2898
|
+
return Option.none();
|
|
2899
|
+
}
|
|
2900
|
+
const revoked = cloneOAuthProviderModeTokenRecord({
|
|
2901
|
+
...record,
|
|
2902
|
+
revokedAt: record.revokedAt ?? input.revokedAt,
|
|
2903
|
+
revocationReason: record.revocationReason ?? input.reason,
|
|
2904
|
+
});
|
|
2905
|
+
recordsByHash.set(input.tokenHash, revoked);
|
|
2906
|
+
return Option.some(cloneOAuthProviderModeTokenRecord(revoked));
|
|
2907
|
+
}),
|
|
2908
|
+
rotateRefreshToken: (input) => Effect.sync(() => {
|
|
2909
|
+
const record = recordsByHash.get(input.tokenHash);
|
|
2910
|
+
if (record === undefined ||
|
|
2911
|
+
record.tokenType !== "refresh_token" ||
|
|
2912
|
+
record.revokedAt !== undefined ||
|
|
2913
|
+
record.rotatedAt !== undefined) {
|
|
2914
|
+
return Option.none();
|
|
2915
|
+
}
|
|
2916
|
+
const rotated = cloneOAuthProviderModeTokenRecord({
|
|
2917
|
+
...record,
|
|
2918
|
+
rotatedAt: input.rotatedAt,
|
|
2919
|
+
replacedByTokenHash: input.replacedByTokenHash,
|
|
2920
|
+
});
|
|
2921
|
+
recordsByHash.set(input.tokenHash, rotated);
|
|
2922
|
+
return Option.some(cloneOAuthProviderModeTokenRecord(rotated));
|
|
2923
|
+
}),
|
|
2924
|
+
deleteRetired: (input) => Effect.sync(() => {
|
|
2925
|
+
let deleted = 0;
|
|
2926
|
+
for (const [tokenHash, record] of recordsByHash) {
|
|
2927
|
+
const expired = input.beforeExpiresAt !== undefined &&
|
|
2928
|
+
Number(record.expiresAt) < Number(input.beforeExpiresAt);
|
|
2929
|
+
const revoked = input.beforeRevokedAt !== undefined &&
|
|
2930
|
+
record.revokedAt !== undefined &&
|
|
2931
|
+
Number(record.revokedAt) < Number(input.beforeRevokedAt);
|
|
2932
|
+
const rotated = input.beforeRotatedAt !== undefined &&
|
|
2933
|
+
record.rotatedAt !== undefined &&
|
|
2934
|
+
Number(record.rotatedAt) < Number(input.beforeRotatedAt);
|
|
2935
|
+
if (expired || revoked || rotated) {
|
|
2936
|
+
recordsByHash.delete(tokenHash);
|
|
2937
|
+
deleted += 1;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
return deleted;
|
|
2941
|
+
}),
|
|
2942
|
+
});
|
|
2943
|
+
};
|
|
2944
|
+
export const OAuthProviderModeTokenStoreMemoryLive = Layer.sync(OAuthProviderModeTokenStore)(makeOAuthProviderModeTokenStoreMemory);
|
|
1543
2945
|
export const OidcIdTokenVerifierLive = Layer.effect(OidcIdTokenVerifier)(Effect.gen(function* () {
|
|
1544
2946
|
const jwtVerifier = yield* JwtVerifier;
|
|
1545
2947
|
return makeOidcIdTokenVerifier({ jwtVerifier });
|