actor-gate 0.1.0

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.
Files changed (155) hide show
  1. package/package.json +25 -0
  2. package/src/config/base-config.d.ts +17 -0
  3. package/src/config/base-config.js +33 -0
  4. package/src/config/index.d.ts +5 -0
  5. package/src/config/index.js +5 -0
  6. package/src/config/nextjs-public-config.d.ts +46 -0
  7. package/src/config/nextjs-public-config.js +89 -0
  8. package/src/config/nextjs-server-config.d.ts +32 -0
  9. package/src/config/nextjs-server-config.js +10 -0
  10. package/src/config/react-client.d.ts +23 -0
  11. package/src/config/react-client.js +69 -0
  12. package/src/config/react-config.d.ts +18 -0
  13. package/src/config/react-config.js +38 -0
  14. package/src/core/adapters/access-token-revocation-adapter.d.ts +8 -0
  15. package/src/core/adapters/access-token-revocation-adapter.js +1 -0
  16. package/src/core/adapters/access-token-transport-adapter.d.ts +15 -0
  17. package/src/core/adapters/access-token-transport-adapter.js +1 -0
  18. package/src/core/adapters/authorization-code-adapter.d.ts +21 -0
  19. package/src/core/adapters/authorization-code-adapter.js +1 -0
  20. package/src/core/adapters/authorization-hooks.d.ts +13 -0
  21. package/src/core/adapters/authorization-hooks.js +1 -0
  22. package/src/core/adapters/index.d.ts +14 -0
  23. package/src/core/adapters/index.js +1 -0
  24. package/src/core/adapters/login-method-adapter.d.ts +7 -0
  25. package/src/core/adapters/login-method-adapter.js +1 -0
  26. package/src/core/adapters/oauth-client-adapter.d.ts +13 -0
  27. package/src/core/adapters/oauth-client-adapter.js +1 -0
  28. package/src/core/adapters/oauth-client-management-adapter.d.ts +23 -0
  29. package/src/core/adapters/oauth-client-management-adapter.js +1 -0
  30. package/src/core/adapters/oauth-grant-type.d.ts +1 -0
  31. package/src/core/adapters/oauth-grant-type.js +1 -0
  32. package/src/core/adapters/oauth-policy.d.ts +9 -0
  33. package/src/core/adapters/oauth-policy.js +1 -0
  34. package/src/core/adapters/observability-hooks.d.ts +31 -0
  35. package/src/core/adapters/observability-hooks.js +1 -0
  36. package/src/core/adapters/pending-auth-request-adapter.d.ts +18 -0
  37. package/src/core/adapters/pending-auth-request-adapter.js +1 -0
  38. package/src/core/adapters/refresh-token-adapter.d.ts +24 -0
  39. package/src/core/adapters/refresh-token-adapter.js +1 -0
  40. package/src/core/adapters/session-adapter.d.ts +14 -0
  41. package/src/core/adapters/session-adapter.js +1 -0
  42. package/src/core/adapters/token-adapter.d.ts +15 -0
  43. package/src/core/adapters/token-adapter.js +1 -0
  44. package/src/core/http/bearer-challenge.d.ts +6 -0
  45. package/src/core/http/bearer-challenge.js +16 -0
  46. package/src/core/ids/id-codec.d.ts +6 -0
  47. package/src/core/ids/id-codec.js +30 -0
  48. package/src/core/index.d.ts +9 -0
  49. package/src/core/index.js +7 -0
  50. package/src/core/oauth/pkce.d.ts +9 -0
  51. package/src/core/oauth/pkce.js +30 -0
  52. package/src/core/services/access-token-service.d.ts +42 -0
  53. package/src/core/services/access-token-service.js +304 -0
  54. package/src/core/services/auth-error.d.ts +14 -0
  55. package/src/core/services/auth-error.js +47 -0
  56. package/src/core/services/contracts.d.ts +23 -0
  57. package/src/core/services/contracts.js +1 -0
  58. package/src/core/services/direct-auth-service.d.ts +50 -0
  59. package/src/core/services/direct-auth-service.js +267 -0
  60. package/src/core/services/index.d.ts +7 -0
  61. package/src/core/services/index.js +5 -0
  62. package/src/core/services/mcp-auth-service.d.ts +39 -0
  63. package/src/core/services/mcp-auth-service.js +170 -0
  64. package/src/core/services/oauth-service.d.ts +91 -0
  65. package/src/core/services/oauth-service.js +571 -0
  66. package/src/core/services/observability.d.ts +22 -0
  67. package/src/core/services/observability.js +71 -0
  68. package/src/core/services/revocation-policy.d.ts +21 -0
  69. package/src/core/services/revocation-policy.js +51 -0
  70. package/src/core/sessions/client-session.d.ts +7 -0
  71. package/src/core/sessions/client-session.js +18 -0
  72. package/src/core/tokens/access-claims.d.ts +21 -0
  73. package/src/core/tokens/access-claims.js +128 -0
  74. package/src/core/tokens/id-claims.d.ts +20 -0
  75. package/src/core/tokens/id-claims.js +25 -0
  76. package/src/core/types/auth-contract.d.ts +33 -0
  77. package/src/core/types/auth-contract.js +1 -0
  78. package/src/express/index.d.ts +1 -0
  79. package/src/express/index.js +1 -0
  80. package/src/express/protected-route.d.ts +44 -0
  81. package/src/express/protected-route.js +119 -0
  82. package/src/index.d.ts +8 -0
  83. package/src/index.js +8 -0
  84. package/src/mcp/index.d.ts +1 -0
  85. package/src/mcp/index.js +1 -0
  86. package/src/mcp/json-rpc-auth.d.ts +5 -0
  87. package/src/mcp/json-rpc-auth.js +41 -0
  88. package/src/next/app/catch-all.d.ts +32 -0
  89. package/src/next/app/catch-all.js +82 -0
  90. package/src/next/app/cookies.d.ts +22 -0
  91. package/src/next/app/cookies.js +36 -0
  92. package/src/next/app/direct-auth-handlers.d.ts +55 -0
  93. package/src/next/app/direct-auth-handlers.js +419 -0
  94. package/src/next/app/index.d.ts +8 -0
  95. package/src/next/app/index.js +8 -0
  96. package/src/next/app/mcp-oauth-handlers.d.ts +74 -0
  97. package/src/next/app/mcp-oauth-handlers.js +365 -0
  98. package/src/next/app/protected-route.d.ts +27 -0
  99. package/src/next/app/protected-route.js +59 -0
  100. package/src/next/app/request.d.ts +12 -0
  101. package/src/next/app/request.js +30 -0
  102. package/src/next/app/response.d.ts +16 -0
  103. package/src/next/app/response.js +48 -0
  104. package/src/next/app/wrapper.d.ts +28 -0
  105. package/src/next/app/wrapper.js +78 -0
  106. package/src/next/index.d.ts +6 -0
  107. package/src/next/index.js +5 -0
  108. package/src/next/pages/catch-all.d.ts +19 -0
  109. package/src/next/pages/catch-all.js +60 -0
  110. package/src/next/pages/cookies.d.ts +41 -0
  111. package/src/next/pages/cookies.js +87 -0
  112. package/src/next/pages/direct-auth-handlers.d.ts +58 -0
  113. package/src/next/pages/direct-auth-handlers.js +425 -0
  114. package/src/next/pages/index.d.ts +8 -0
  115. package/src/next/pages/index.js +8 -0
  116. package/src/next/pages/mcp-oauth-handlers.d.ts +77 -0
  117. package/src/next/pages/mcp-oauth-handlers.js +341 -0
  118. package/src/next/pages/protected-route.d.ts +28 -0
  119. package/src/next/pages/protected-route.js +59 -0
  120. package/src/next/pages/request.d.ts +14 -0
  121. package/src/next/pages/request.js +66 -0
  122. package/src/next/pages/response.d.ts +28 -0
  123. package/src/next/pages/response.js +29 -0
  124. package/src/next/pages/wrapper.d.ts +29 -0
  125. package/src/next/pages/wrapper.js +74 -0
  126. package/src/next/rewrites.d.ts +12 -0
  127. package/src/next/rewrites.js +74 -0
  128. package/src/next/shared/auth-http.d.ts +24 -0
  129. package/src/next/shared/auth-http.js +42 -0
  130. package/src/next/shared/auth-routes.d.ts +17 -0
  131. package/src/next/shared/auth-routes.js +153 -0
  132. package/src/next/shared/direct-auth-utils.d.ts +71 -0
  133. package/src/next/shared/direct-auth-utils.js +275 -0
  134. package/src/next/shared/oauth-utils.d.ts +45 -0
  135. package/src/next/shared/oauth-utils.js +308 -0
  136. package/src/next/shared/well-known-utils.d.ts +46 -0
  137. package/src/next/shared/well-known-utils.js +108 -0
  138. package/src/testing/in-memory/in-memory-access-token-revocation-adapter.d.ts +2 -0
  139. package/src/testing/in-memory/in-memory-access-token-revocation-adapter.js +14 -0
  140. package/src/testing/in-memory/in-memory-authorization-code-adapter.d.ts +2 -0
  141. package/src/testing/in-memory/in-memory-authorization-code-adapter.js +36 -0
  142. package/src/testing/in-memory/in-memory-oauth-client-adapter.d.ts +14 -0
  143. package/src/testing/in-memory/in-memory-oauth-client-adapter.js +26 -0
  144. package/src/testing/in-memory/in-memory-pending-auth-request-adapter.d.ts +2 -0
  145. package/src/testing/in-memory/in-memory-pending-auth-request-adapter.js +43 -0
  146. package/src/testing/in-memory/in-memory-refresh-token-adapter.d.ts +2 -0
  147. package/src/testing/in-memory/in-memory-refresh-token-adapter.js +67 -0
  148. package/src/testing/in-memory/in-memory-session-adapter.d.ts +6 -0
  149. package/src/testing/in-memory/in-memory-session-adapter.js +43 -0
  150. package/src/testing/in-memory/index.d.ts +7 -0
  151. package/src/testing/in-memory/index.js +7 -0
  152. package/src/testing/in-memory/test-fixtures.d.ts +5 -0
  153. package/src/testing/in-memory/test-fixtures.js +18 -0
  154. package/src/testing/index.d.ts +2 -0
  155. package/src/testing/index.js +4 -0
@@ -0,0 +1,50 @@
1
+ import type { AccessTokenRevocationAdapter } from '../adapters/access-token-revocation-adapter';
2
+ import type { ObservabilityConfig } from '../adapters/observability-hooks';
3
+ import type { RefreshTokenAdapter } from '../adapters/refresh-token-adapter';
4
+ import type { SessionAdapter } from '../adapters/session-adapter';
5
+ import type { AuthActor } from '../types/auth-contract';
6
+ import type { AccessTokenService, CreateAccessTokenServiceInput } from './access-token-service';
7
+ import type { RefreshTokenMode, RefreshTokenReuseRevokeScope, ValidatedAccessTokenResult } from './contracts';
8
+ export type CreateDirectAuthServiceInput<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>> = {
9
+ accessTokenService: AccessTokenService<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>;
10
+ sessionAdapter: SessionAdapter<TSessionId, TUserId, TActor, TServerSessionData>;
11
+ refreshTokenAdapter: RefreshTokenAdapter<TSessionId>;
12
+ accessTokenRevocationAdapter?: AccessTokenRevocationAdapter;
13
+ hashToken: (token: string) => string;
14
+ generateToken: () => string;
15
+ refreshTokenTtlSeconds: number;
16
+ refreshTokenMode?: RefreshTokenMode;
17
+ revokeScopeOnReuse?: RefreshTokenReuseRevokeScope;
18
+ nowUnix?: () => number;
19
+ buildClientSessionData?: CreateAccessTokenServiceInput<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>['buildClientSessionData'];
20
+ observability?: ObservabilityConfig<TActor>;
21
+ };
22
+ export type DirectAuthService<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>> = {
23
+ validateAccessToken(input: {
24
+ accessToken: string;
25
+ requestId?: string;
26
+ expectedAudience?: string;
27
+ allowedActors?: readonly TActor[] | ReadonlySet<TActor>;
28
+ }): Promise<ValidatedAccessTokenResult<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>>;
29
+ refresh(input: {
30
+ refreshToken: string;
31
+ requestId?: string;
32
+ }): Promise<{
33
+ accessToken: string;
34
+ refreshToken?: string;
35
+ accessClaims: ValidatedAccessTokenResult<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>['claims'];
36
+ clientSession: ValidatedAccessTokenResult<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>['clientSession'];
37
+ }>;
38
+ logout(input: {
39
+ requestId?: string;
40
+ sessionId?: TSessionId;
41
+ refreshToken?: string;
42
+ accessTokenJti?: string;
43
+ accessTokenExpiresAtUnix?: number;
44
+ }): Promise<{
45
+ sessionRevoked: boolean;
46
+ refreshTokenRevoked: boolean;
47
+ accessTokenRevoked: boolean;
48
+ }>;
49
+ };
50
+ export declare function createDirectAuthService<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>>(input: CreateDirectAuthServiceInput<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>): DirectAuthService<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>;
@@ -0,0 +1,267 @@
1
+ import { buildClientSession } from '../sessions/client-session';
2
+ import { AuthServiceError, isAuthServiceError } from './auth-error';
3
+ import { emitAuditEvent, emitMetric, reportServiceError, } from './observability';
4
+ function assertPositiveSafeInteger(value, fieldName) {
5
+ if (!Number.isSafeInteger(value) || value <= 0) {
6
+ throw new AuthServiceError({
7
+ code: 'invalid_request',
8
+ message: `${fieldName} must be a positive safe integer.`,
9
+ });
10
+ }
11
+ }
12
+ async function revokeOnRefreshTokenReuse(input) {
13
+ switch (input.scope) {
14
+ case 'token':
15
+ await input.refreshTokenAdapter.revoke(input.tokenHash, input.nowUnix);
16
+ return;
17
+ case 'family':
18
+ await input.refreshTokenAdapter.revokeFamily(input.familyId, input.nowUnix);
19
+ return;
20
+ case 'session':
21
+ await input.refreshTokenAdapter.revokeFamily(input.familyId, input.nowUnix);
22
+ await input.sessionAdapter.revoke(input.sessionId, input.nowUnix);
23
+ return;
24
+ }
25
+ }
26
+ export function createDirectAuthService(input) {
27
+ const nowUnix = input.nowUnix ?? (() => Math.floor(Date.now() / 1000));
28
+ const refreshTokenMode = input.refreshTokenMode ?? 'rotate_with_reuse_detection';
29
+ const revokeScopeOnReuse = input.revokeScopeOnReuse ?? 'family';
30
+ assertPositiveSafeInteger(input.refreshTokenTtlSeconds, 'refreshTokenTtlSeconds');
31
+ return {
32
+ validateAccessToken(validationInput) {
33
+ return input.accessTokenService.validateAccessToken(validationInput);
34
+ },
35
+ async refresh({ refreshToken, requestId }) {
36
+ const now = nowUnix();
37
+ const tokenHash = input.hashToken(refreshToken);
38
+ try {
39
+ let refreshRecord;
40
+ if (refreshTokenMode === 'reuse') {
41
+ const found = await input.refreshTokenAdapter.find(tokenHash);
42
+ if (!found) {
43
+ throw new AuthServiceError({
44
+ code: 'invalid_grant',
45
+ message: 'Refresh token is invalid.',
46
+ });
47
+ }
48
+ refreshRecord = found;
49
+ }
50
+ else {
51
+ const consumed = await input.refreshTokenAdapter.consume(tokenHash, now);
52
+ if (!consumed) {
53
+ throw new AuthServiceError({
54
+ code: 'invalid_grant',
55
+ message: 'Refresh token is invalid.',
56
+ });
57
+ }
58
+ if (consumed.consumedAtUnix !== now) {
59
+ if (refreshTokenMode === 'rotate_with_reuse_detection') {
60
+ await revokeOnRefreshTokenReuse({
61
+ scope: revokeScopeOnReuse,
62
+ refreshTokenAdapter: input.refreshTokenAdapter,
63
+ sessionAdapter: input.sessionAdapter,
64
+ tokenHash,
65
+ familyId: consumed.familyId,
66
+ sessionId: consumed.sessionId,
67
+ nowUnix: now,
68
+ });
69
+ await emitAuditEvent({
70
+ observability: input.observability,
71
+ requestId,
72
+ event: {
73
+ name: 'refresh_token_reuse_detected',
74
+ atUnix: now,
75
+ reason: 'refresh_token_reused',
76
+ metadata: { revokeScopeOnReuse },
77
+ },
78
+ });
79
+ }
80
+ throw new AuthServiceError({
81
+ code: 'invalid_grant',
82
+ message: 'Refresh token has already been used.',
83
+ });
84
+ }
85
+ refreshRecord = consumed;
86
+ }
87
+ if (refreshRecord.revokedAtUnix !== undefined &&
88
+ refreshRecord.revokedAtUnix !== null &&
89
+ refreshRecord.revokedAtUnix <= now) {
90
+ throw new AuthServiceError({
91
+ code: 'invalid_grant',
92
+ message: 'Refresh token has been revoked.',
93
+ });
94
+ }
95
+ if (refreshRecord.expiresAtUnix <= now) {
96
+ throw new AuthServiceError({
97
+ code: 'invalid_grant',
98
+ message: 'Refresh token has expired.',
99
+ });
100
+ }
101
+ const session = await input.sessionAdapter.findById(refreshRecord.sessionId);
102
+ if (!session) {
103
+ throw new AuthServiceError({
104
+ code: 'session_not_found',
105
+ message: 'Session was not found.',
106
+ });
107
+ }
108
+ if (session.revokedAt !== undefined &&
109
+ session.revokedAt !== null &&
110
+ session.revokedAt <= now) {
111
+ throw new AuthServiceError({
112
+ code: 'session_revoked',
113
+ message: 'Session was revoked.',
114
+ });
115
+ }
116
+ if (session.expiresAt <= now) {
117
+ throw new AuthServiceError({
118
+ code: 'session_expired',
119
+ message: 'Session has expired.',
120
+ });
121
+ }
122
+ const issued = await input.accessTokenService.issueAccessToken({
123
+ session,
124
+ ...(requestId === undefined ? {} : { requestId }),
125
+ });
126
+ const clientSession = buildClientSession({
127
+ session,
128
+ ...(input.buildClientSessionData === undefined
129
+ ? {}
130
+ : { buildClientSessionData: input.buildClientSessionData }),
131
+ });
132
+ let nextRefreshToken;
133
+ if (refreshTokenMode !== 'reuse') {
134
+ nextRefreshToken = input.generateToken();
135
+ await input.refreshTokenAdapter.create({
136
+ sessionId: session.sessionId,
137
+ tokenHash: input.hashToken(nextRefreshToken),
138
+ expiresAtUnix: now + input.refreshTokenTtlSeconds,
139
+ familyId: refreshRecord.familyId,
140
+ parentTokenHash: tokenHash,
141
+ });
142
+ await emitAuditEvent({
143
+ observability: input.observability,
144
+ requestId,
145
+ event: {
146
+ name: 'refresh_token_rotated',
147
+ atUnix: now,
148
+ actor: session.actor,
149
+ },
150
+ });
151
+ await emitMetric({
152
+ observability: input.observability,
153
+ requestId,
154
+ metric: {
155
+ name: 'auth.refresh.rotated',
156
+ value: 1,
157
+ tags: { mode: refreshTokenMode },
158
+ },
159
+ });
160
+ }
161
+ return {
162
+ accessToken: issued.accessToken,
163
+ accessClaims: issued.claims,
164
+ ...(nextRefreshToken === undefined
165
+ ? {}
166
+ : { refreshToken: nextRefreshToken }),
167
+ clientSession,
168
+ };
169
+ }
170
+ catch (error) {
171
+ if (isAuthServiceError(error)) {
172
+ throw error;
173
+ }
174
+ await reportServiceError({
175
+ observability: input.observability,
176
+ where: 'direct-auth-service.refresh',
177
+ error,
178
+ requestId,
179
+ });
180
+ throw new AuthServiceError({
181
+ code: 'system_error',
182
+ message: 'Failed to refresh direct-auth session.',
183
+ cause: error,
184
+ });
185
+ }
186
+ },
187
+ async logout({ requestId, sessionId, refreshToken, accessTokenJti, accessTokenExpiresAtUnix, }) {
188
+ const now = nowUnix();
189
+ if (sessionId === undefined &&
190
+ refreshToken === undefined &&
191
+ accessTokenJti === undefined) {
192
+ throw new AuthServiceError({
193
+ code: 'invalid_request',
194
+ message: 'At least one of sessionId, refreshToken, or accessTokenJti must be provided.',
195
+ });
196
+ }
197
+ try {
198
+ let sessionRevoked = false;
199
+ let refreshTokenRevoked = false;
200
+ let accessTokenRevoked = false;
201
+ if (sessionId !== undefined) {
202
+ await input.sessionAdapter.revoke(sessionId, now);
203
+ sessionRevoked = true;
204
+ await emitAuditEvent({
205
+ observability: input.observability,
206
+ requestId,
207
+ event: {
208
+ name: 'session_revoked',
209
+ atUnix: now,
210
+ },
211
+ });
212
+ }
213
+ if (refreshToken !== undefined) {
214
+ const refreshTokenHash = input.hashToken(refreshToken);
215
+ const token = await input.refreshTokenAdapter.find(refreshTokenHash);
216
+ await input.refreshTokenAdapter.revoke(refreshTokenHash, now);
217
+ if (token) {
218
+ await input.refreshTokenAdapter.revokeFamily(token.familyId, now);
219
+ refreshTokenRevoked = true;
220
+ }
221
+ }
222
+ if (accessTokenJti !== undefined) {
223
+ if (accessTokenExpiresAtUnix === undefined) {
224
+ throw new AuthServiceError({
225
+ code: 'invalid_request',
226
+ message: 'accessTokenExpiresAtUnix is required when accessTokenJti is provided.',
227
+ });
228
+ }
229
+ assertPositiveSafeInteger(accessTokenExpiresAtUnix, 'accessTokenExpiresAtUnix');
230
+ if (!input.accessTokenRevocationAdapter) {
231
+ throw new AuthServiceError({
232
+ code: 'invalid_request',
233
+ message: 'accessTokenRevocationAdapter is required to revoke access token jti.',
234
+ });
235
+ }
236
+ await input.accessTokenRevocationAdapter.revokeJti({
237
+ jti: accessTokenJti,
238
+ expiresAtUnix: accessTokenExpiresAtUnix,
239
+ reason: 'logout',
240
+ });
241
+ accessTokenRevoked = true;
242
+ }
243
+ return {
244
+ sessionRevoked,
245
+ refreshTokenRevoked,
246
+ accessTokenRevoked,
247
+ };
248
+ }
249
+ catch (error) {
250
+ if (isAuthServiceError(error)) {
251
+ throw error;
252
+ }
253
+ await reportServiceError({
254
+ observability: input.observability,
255
+ where: 'direct-auth-service.logout',
256
+ error,
257
+ requestId,
258
+ });
259
+ throw new AuthServiceError({
260
+ code: 'system_error',
261
+ message: 'Failed to process logout.',
262
+ cause: error,
263
+ });
264
+ }
265
+ },
266
+ };
267
+ }
@@ -0,0 +1,7 @@
1
+ export { AuthServiceError, authErrorToHttpStatus, isAuthServiceError, type AuthServiceErrorCode, } from './auth-error';
2
+ export { createAccessTokenService, type AccessTokenService, type CreateAccessTokenServiceInput, } from './access-token-service';
3
+ export type { RevocationGuarantee, SessionMode } from './revocation-policy';
4
+ export { createDirectAuthService, type CreateDirectAuthServiceInput, type DirectAuthService, } from './direct-auth-service';
5
+ export { createOAuthService, type CreateOAuthServiceInput, type OAuthService, } from './oauth-service';
6
+ export { createMcpAuthService, type CreateMcpAuthServiceInput, type McpAuthService, type McpAuthenticationResult, } from './mcp-auth-service';
7
+ export type { IssuedAccessToken, OAuthPendingRequest, RefreshTokenMode, RefreshTokenReuseRevokeScope, ValidatedAccessTokenResult, } from './contracts';
@@ -0,0 +1,5 @@
1
+ export { AuthServiceError, authErrorToHttpStatus, isAuthServiceError, } from './auth-error';
2
+ export { createAccessTokenService, } from './access-token-service';
3
+ export { createDirectAuthService, } from './direct-auth-service';
4
+ export { createOAuthService, } from './oauth-service';
5
+ export { createMcpAuthService, } from './mcp-auth-service';
@@ -0,0 +1,39 @@
1
+ import type { AccessTokenSource, AccessTokenTransportAdapter, AccessTokenTransportInput } from '../adapters/access-token-transport-adapter';
2
+ import type { AuthorizationHooks } from '../adapters/authorization-hooks';
3
+ import type { ObservabilityConfig } from '../adapters/observability-hooks';
4
+ import type { AccessClaims, AuthActor } from '../types/auth-contract';
5
+ import type { AccessTokenService } from './access-token-service';
6
+ import type { ValidatedAccessTokenResult } from './contracts';
7
+ export type CreateMcpAuthServiceInput<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>> = {
8
+ accessTokenService: AccessTokenService<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>;
9
+ accessTokenTransportAdapter: AccessTokenTransportAdapter<TActor>;
10
+ authorizationHooks?: AuthorizationHooks<TActor, TUserId>;
11
+ observability?: ObservabilityConfig<TActor>;
12
+ unauthenticatedMethods?: ReadonlySet<string>;
13
+ denyUserActorForTools?: boolean;
14
+ extractScopes?: (claims: AccessClaims<TActor, TExtClaims>) => string[];
15
+ nowUnix?: () => number;
16
+ };
17
+ export type McpAuthenticationResult<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>> = {
18
+ rpcMethods: string[];
19
+ requiresAuthentication: boolean;
20
+ authenticated: boolean;
21
+ tokenSource?: AccessTokenSource;
22
+ auth?: ValidatedAccessTokenResult<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>;
23
+ };
24
+ export type McpAuthService<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>> = {
25
+ authenticateRequest(input: {
26
+ body: unknown;
27
+ transport: AccessTokenTransportInput;
28
+ requestId?: string;
29
+ expectedAudience?: string;
30
+ allowedActors?: readonly TActor[] | ReadonlySet<TActor>;
31
+ }): Promise<McpAuthenticationResult<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>>;
32
+ attachContext<TContext extends Record<string, unknown>>(input: {
33
+ context: TContext;
34
+ authentication: McpAuthenticationResult<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>;
35
+ }): TContext & {
36
+ auth?: ValidatedAccessTokenResult<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>;
37
+ };
38
+ };
39
+ export declare function createMcpAuthService<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>>(input: CreateMcpAuthServiceInput<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>): McpAuthService<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>;
@@ -0,0 +1,170 @@
1
+ import { DEFAULT_UNAUTHENTICATED_RPC_METHODS, getRpcMethodsFromBody, requiresAuth, } from '../../mcp/json-rpc-auth';
2
+ import { AuthServiceError, isAuthServiceError } from './auth-error';
3
+ import { emitAuditEvent, reportServiceError } from './observability';
4
+ function extractToolNameFromPayload(payload) {
5
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
6
+ return null;
7
+ }
8
+ const method = payload.method;
9
+ if (method !== 'tools/call') {
10
+ return null;
11
+ }
12
+ const params = payload.params;
13
+ if (!params || typeof params !== 'object' || Array.isArray(params)) {
14
+ return null;
15
+ }
16
+ const toolName = params.name;
17
+ return typeof toolName === 'string' && toolName.length > 0 ? toolName : null;
18
+ }
19
+ function extractToolNamesFromBody(body) {
20
+ if (Array.isArray(body)) {
21
+ return body
22
+ .map(message => extractToolNameFromPayload(message))
23
+ .filter((toolName) => toolName !== null);
24
+ }
25
+ const toolName = extractToolNameFromPayload(body);
26
+ return toolName ? [toolName] : [];
27
+ }
28
+ function defaultExtractScopes(claims) {
29
+ const scopeValue = claims.ext
30
+ ? claims.ext.scopes
31
+ : undefined;
32
+ if (!Array.isArray(scopeValue)) {
33
+ return [];
34
+ }
35
+ if (!scopeValue.every(item => typeof item === 'string')) {
36
+ return [];
37
+ }
38
+ return [...scopeValue];
39
+ }
40
+ export function createMcpAuthService(input) {
41
+ const nowUnix = input.nowUnix ?? (() => Math.floor(Date.now() / 1000));
42
+ const unauthenticatedMethods = input.unauthenticatedMethods ??
43
+ new Set(DEFAULT_UNAUTHENTICATED_RPC_METHODS);
44
+ const denyUserActorForTools = input.denyUserActorForTools ?? true;
45
+ const extractScopes = input.extractScopes ?? defaultExtractScopes;
46
+ return {
47
+ async authenticateRequest({ body, transport, requestId, expectedAudience, allowedActors, }) {
48
+ const rpcMethods = getRpcMethodsFromBody(body);
49
+ const requiresAuthentication = rpcMethods.some(method => requiresAuth(method, unauthenticatedMethods));
50
+ if (!requiresAuthentication) {
51
+ return {
52
+ rpcMethods,
53
+ requiresAuthentication: false,
54
+ authenticated: false,
55
+ };
56
+ }
57
+ const now = nowUnix();
58
+ try {
59
+ const extractedToken = input.accessTokenTransportAdapter.extractAccessToken({
60
+ transport,
61
+ });
62
+ if (!extractedToken.token) {
63
+ throw new AuthServiceError({
64
+ code: 'unauthorized',
65
+ message: 'Access token is required for authenticated MCP methods.',
66
+ });
67
+ }
68
+ const validated = await input.accessTokenService.validateAccessToken({
69
+ accessToken: extractedToken.token,
70
+ ...(requestId === undefined ? {} : { requestId }),
71
+ ...(expectedAudience === undefined ? {} : { expectedAudience }),
72
+ ...(allowedActors === undefined ? {} : { allowedActors }),
73
+ });
74
+ const toolNames = extractToolNamesFromBody(body);
75
+ if (toolNames.length > 0 &&
76
+ denyUserActorForTools &&
77
+ validated.claims.actor === 'USER') {
78
+ await emitAuditEvent({
79
+ observability: input.observability,
80
+ requestId,
81
+ event: {
82
+ name: 'mcp_tool_call_denied',
83
+ atUnix: now,
84
+ actor: validated.claims.actor,
85
+ userId: validated.claims.uid,
86
+ ...(validated.claims.cid === undefined
87
+ ? {}
88
+ : { clientId: validated.claims.cid }),
89
+ reason: 'user_actor_not_allowed',
90
+ },
91
+ });
92
+ throw new AuthServiceError({
93
+ code: 'forbidden',
94
+ message: 'Actor USER is not allowed to call MCP tools.',
95
+ });
96
+ }
97
+ if (toolNames.length > 0 && input.authorizationHooks?.canCallMcpTool) {
98
+ const scopes = extractScopes(validated.claims);
99
+ for (const toolName of toolNames) {
100
+ const decision = await input.authorizationHooks.canCallMcpTool({
101
+ actor: validated.claims.actor,
102
+ userId: validated.authContext.userId,
103
+ ...(validated.claims.cid === undefined
104
+ ? {}
105
+ : { clientId: validated.claims.cid }),
106
+ toolName,
107
+ scopes,
108
+ });
109
+ if (!decision.allowed) {
110
+ await emitAuditEvent({
111
+ observability: input.observability,
112
+ requestId,
113
+ event: {
114
+ name: 'mcp_tool_call_denied',
115
+ atUnix: now,
116
+ actor: validated.claims.actor,
117
+ userId: validated.claims.uid,
118
+ ...(validated.claims.cid === undefined
119
+ ? {}
120
+ : { clientId: validated.claims.cid }),
121
+ reason: decision.reason ?? 'authorization_hook_denied',
122
+ metadata: { toolName },
123
+ },
124
+ });
125
+ throw new AuthServiceError({
126
+ code: 'forbidden',
127
+ message: decision.reason ??
128
+ 'Tool access denied by authorization hook.',
129
+ });
130
+ }
131
+ }
132
+ }
133
+ return {
134
+ rpcMethods,
135
+ requiresAuthentication: true,
136
+ authenticated: true,
137
+ ...(extractedToken.source === undefined
138
+ ? {}
139
+ : { tokenSource: extractedToken.source }),
140
+ auth: validated,
141
+ };
142
+ }
143
+ catch (error) {
144
+ if (isAuthServiceError(error)) {
145
+ throw error;
146
+ }
147
+ await reportServiceError({
148
+ observability: input.observability,
149
+ where: 'mcp-auth-service.authenticateRequest',
150
+ error,
151
+ requestId,
152
+ });
153
+ throw new AuthServiceError({
154
+ code: 'system_error',
155
+ message: 'Failed to authenticate MCP request.',
156
+ cause: error,
157
+ });
158
+ }
159
+ },
160
+ attachContext({ context, authentication }) {
161
+ if (!authentication.authenticated || authentication.auth === undefined) {
162
+ return context;
163
+ }
164
+ return {
165
+ ...context,
166
+ auth: authentication.auth,
167
+ };
168
+ },
169
+ };
170
+ }
@@ -0,0 +1,91 @@
1
+ import type { AuthorizationCodeAdapter } from '../adapters/authorization-code-adapter';
2
+ import type { OAuthClientAdapter } from '../adapters/oauth-client-adapter';
3
+ import type { OAuthPolicy } from '../adapters/oauth-policy';
4
+ import type { ObservabilityConfig } from '../adapters/observability-hooks';
5
+ import type { PendingAuthRequestAdapter } from '../adapters/pending-auth-request-adapter';
6
+ import type { RefreshTokenAdapter } from '../adapters/refresh-token-adapter';
7
+ import type { SessionAdapter } from '../adapters/session-adapter';
8
+ import type { AccessClaims, AuthActor } from '../types/auth-contract';
9
+ import type { AccessTokenService } from './access-token-service';
10
+ import type { OAuthPendingRequest } from './contracts';
11
+ export type CreateOAuthServiceInput<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>> = {
12
+ accessTokenService: AccessTokenService<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>;
13
+ sessionAdapter: SessionAdapter<TSessionId, TUserId, TActor, TServerSessionData>;
14
+ authorizationCodeAdapter: AuthorizationCodeAdapter<TUserId>;
15
+ pendingAuthRequestAdapter: PendingAuthRequestAdapter<OAuthPendingRequest>;
16
+ oauthClientAdapter: OAuthClientAdapter<TActor, TUserId>;
17
+ oauthPolicy: OAuthPolicy<TActor>;
18
+ refreshTokenAdapter?: RefreshTokenAdapter<TSessionId>;
19
+ hashToken: (token: string) => string;
20
+ generateToken: () => string;
21
+ authorizationCodeTtlSeconds: number;
22
+ pendingAuthRequestTtlSeconds: number;
23
+ refreshTokenTtlSeconds?: number;
24
+ nowUnix?: () => number;
25
+ observability?: ObservabilityConfig<TActor>;
26
+ };
27
+ export type OAuthService<TUserId, TActor extends AuthActor = AuthActor, TExtClaims extends Record<string, unknown> = Record<string, never>> = {
28
+ startAuthorization(input: {
29
+ requestId: string;
30
+ clientId: string;
31
+ redirectUri: string;
32
+ codeChallenge: string;
33
+ codeMethod: OAuthPendingRequest['codeMethod'];
34
+ scopes: string[];
35
+ state?: string;
36
+ auditRequestId?: string;
37
+ }): Promise<{
38
+ requestId: string;
39
+ expiresAtUnix: number;
40
+ }>;
41
+ confirmAuthorization(input: {
42
+ requestId: string;
43
+ userId: TUserId;
44
+ approved: boolean;
45
+ auditRequestId?: string;
46
+ }): Promise<{
47
+ approved: false;
48
+ redirectUri: string;
49
+ state?: string;
50
+ } | {
51
+ approved: true;
52
+ authorizationCode: string;
53
+ redirectUri: string;
54
+ state?: string;
55
+ expiresAtUnix: number;
56
+ }>;
57
+ exchangeAuthorizationCode(input: {
58
+ clientId: string;
59
+ clientSecret: string | null;
60
+ authorizationCode: string;
61
+ redirectUri: string;
62
+ codeVerifier: string;
63
+ sessionTtlSeconds: number;
64
+ issueRefreshToken?: boolean;
65
+ auditRequestId?: string;
66
+ }): Promise<{
67
+ accessToken: string;
68
+ accessClaims: AccessClaims<TActor, TExtClaims>;
69
+ refreshToken?: string;
70
+ }>;
71
+ exchangeRefreshToken(input: {
72
+ clientId: string;
73
+ clientSecret: string | null;
74
+ refreshToken: string;
75
+ auditRequestId?: string;
76
+ }): Promise<{
77
+ accessToken: string;
78
+ accessClaims: AccessClaims<TActor, TExtClaims>;
79
+ refreshToken: string;
80
+ }>;
81
+ exchangeClientCredentials(input: {
82
+ clientId: string;
83
+ clientSecret: string | null;
84
+ sessionTtlSeconds: number;
85
+ auditRequestId?: string;
86
+ }): Promise<{
87
+ accessToken: string;
88
+ accessClaims: AccessClaims<TActor, TExtClaims>;
89
+ }>;
90
+ };
91
+ export declare function createOAuthService<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>>(input: CreateOAuthServiceInput<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>): OAuthService<TUserId, TActor, TExtClaims>;