@harperfast/oauth 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +219 -0
- package/assets/test.html +321 -0
- package/config.yaml +23 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +241 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/CSRFTokenManager.d.ts +32 -0
- package/dist/lib/CSRFTokenManager.js +90 -0
- package/dist/lib/CSRFTokenManager.js.map +1 -0
- package/dist/lib/OAuthProvider.d.ts +59 -0
- package/dist/lib/OAuthProvider.js +370 -0
- package/dist/lib/OAuthProvider.js.map +1 -0
- package/dist/lib/config.d.ts +31 -0
- package/dist/lib/config.js +138 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/handlers.d.ts +56 -0
- package/dist/lib/handlers.js +386 -0
- package/dist/lib/handlers.js.map +1 -0
- package/dist/lib/hookManager.d.ts +52 -0
- package/dist/lib/hookManager.js +114 -0
- package/dist/lib/hookManager.js.map +1 -0
- package/dist/lib/providers/auth0.d.ts +8 -0
- package/dist/lib/providers/auth0.js +34 -0
- package/dist/lib/providers/auth0.js.map +1 -0
- package/dist/lib/providers/azure.d.ts +7 -0
- package/dist/lib/providers/azure.js +33 -0
- package/dist/lib/providers/azure.js.map +1 -0
- package/dist/lib/providers/generic.d.ts +7 -0
- package/dist/lib/providers/generic.js +20 -0
- package/dist/lib/providers/generic.js.map +1 -0
- package/dist/lib/providers/github.d.ts +7 -0
- package/dist/lib/providers/github.js +73 -0
- package/dist/lib/providers/github.js.map +1 -0
- package/dist/lib/providers/google.d.ts +7 -0
- package/dist/lib/providers/google.js +27 -0
- package/dist/lib/providers/google.js.map +1 -0
- package/dist/lib/providers/index.d.ts +17 -0
- package/dist/lib/providers/index.js +49 -0
- package/dist/lib/providers/index.js.map +1 -0
- package/dist/lib/providers/okta.d.ts +8 -0
- package/dist/lib/providers/okta.js +45 -0
- package/dist/lib/providers/okta.js.map +1 -0
- package/dist/lib/providers/validation.d.ts +67 -0
- package/dist/lib/providers/validation.js +156 -0
- package/dist/lib/providers/validation.js.map +1 -0
- package/dist/lib/resource.d.ts +102 -0
- package/dist/lib/resource.js +368 -0
- package/dist/lib/resource.js.map +1 -0
- package/dist/lib/sessionValidator.d.ts +38 -0
- package/dist/lib/sessionValidator.js +162 -0
- package/dist/lib/sessionValidator.js.map +1 -0
- package/dist/lib/tenantManager.d.ts +102 -0
- package/dist/lib/tenantManager.js +177 -0
- package/dist/lib/tenantManager.js.map +1 -0
- package/dist/lib/withOAuthValidation.d.ts +64 -0
- package/dist/lib/withOAuthValidation.js +188 -0
- package/dist/lib/withOAuthValidation.js.map +1 -0
- package/dist/types.d.ts +326 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +89 -0
- package/schema/oauth.graphql +21 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Session Validation Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Wraps Harper resources to add automatic OAuth session validation and token refresh
|
|
5
|
+
* before handling any request. This enables transparent token management for protected endpoints.
|
|
6
|
+
*/
|
|
7
|
+
import { validateAndRefreshSession } from "./sessionValidator.js";
|
|
8
|
+
/**
|
|
9
|
+
* Wraps a Harper resource to add automatic OAuth session validation
|
|
10
|
+
*
|
|
11
|
+
* This wrapper intercepts all resource method calls (get, post, put, patch, delete)
|
|
12
|
+
* and validates/refreshes OAuth tokens before passing the request to the original resource.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // In your application component:
|
|
17
|
+
* import { withOAuthValidation } from '@harperfast/oauth';
|
|
18
|
+
*
|
|
19
|
+
* export function handleApplication(scope) {
|
|
20
|
+
* // Get OAuth providers from the OAuth plugin
|
|
21
|
+
* const oauthPlugin = scope.parent.resources.get('oauth');
|
|
22
|
+
*
|
|
23
|
+
* // Wrap your protected resource
|
|
24
|
+
* const myResource = {
|
|
25
|
+
* async get(target, request) {
|
|
26
|
+
* // This code only runs if OAuth session is valid
|
|
27
|
+
* return { user: request.session.oauthUser };
|
|
28
|
+
* }
|
|
29
|
+
* };
|
|
30
|
+
*
|
|
31
|
+
* scope.resources.set('protected', withOAuthValidation(myResource, {
|
|
32
|
+
* providers: oauthPlugin.providers,
|
|
33
|
+
* requireAuth: true,
|
|
34
|
+
* logger: scope.logger
|
|
35
|
+
* }));
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function withOAuthValidation(resource, options) {
|
|
40
|
+
const { providers, logger, requireAuth = false, onValidationError } = options;
|
|
41
|
+
// Create a proxy that wraps all resource methods
|
|
42
|
+
return new Proxy(resource, {
|
|
43
|
+
get(target, prop) {
|
|
44
|
+
const originalMethod = target[prop];
|
|
45
|
+
// Only wrap HTTP methods
|
|
46
|
+
if (!['get', 'post', 'put', 'patch', 'delete'].includes(prop)) {
|
|
47
|
+
return originalMethod;
|
|
48
|
+
}
|
|
49
|
+
// Return wrapped method with OAuth validation
|
|
50
|
+
return async function (...args) {
|
|
51
|
+
// Extract request from arguments (usually last or second argument)
|
|
52
|
+
const request = args.find((arg) => arg?.session !== undefined);
|
|
53
|
+
if (!request) {
|
|
54
|
+
// No request object found - just pass through
|
|
55
|
+
return originalMethod.apply(this, args);
|
|
56
|
+
}
|
|
57
|
+
// Check if session has OAuth data
|
|
58
|
+
const hasOAuth = request.session?.oauth !== undefined;
|
|
59
|
+
if (!hasOAuth) {
|
|
60
|
+
if (requireAuth) {
|
|
61
|
+
// OAuth authentication required but not present
|
|
62
|
+
const error = 'OAuth authentication required';
|
|
63
|
+
if (onValidationError) {
|
|
64
|
+
return onValidationError(request, error);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
status: 401,
|
|
68
|
+
body: {
|
|
69
|
+
error: 'Unauthorized',
|
|
70
|
+
message: error,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// OAuth not required, pass through
|
|
75
|
+
return originalMethod.apply(this, args);
|
|
76
|
+
}
|
|
77
|
+
// Get provider for this OAuth session
|
|
78
|
+
const providerName = request.session?.oauth?.provider;
|
|
79
|
+
if (!providerName) {
|
|
80
|
+
// No provider name in session - invalid OAuth data
|
|
81
|
+
if (request.session) {
|
|
82
|
+
delete request.session.oauth;
|
|
83
|
+
delete request.session.oauthUser;
|
|
84
|
+
}
|
|
85
|
+
if (requireAuth) {
|
|
86
|
+
const error = 'Invalid OAuth session data';
|
|
87
|
+
if (onValidationError) {
|
|
88
|
+
return onValidationError(request, error);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
status: 401,
|
|
92
|
+
body: {
|
|
93
|
+
error: 'Unauthorized',
|
|
94
|
+
message: error,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return originalMethod.apply(this, args);
|
|
99
|
+
}
|
|
100
|
+
const providerData = providers[providerName];
|
|
101
|
+
if (!providerData) {
|
|
102
|
+
logger?.warn?.(`OAuth provider '${providerName}' not found for session validation`);
|
|
103
|
+
// Provider not found - clear OAuth data and continue
|
|
104
|
+
if (request.session) {
|
|
105
|
+
delete request.session.oauth;
|
|
106
|
+
delete request.session.oauthUser;
|
|
107
|
+
}
|
|
108
|
+
if (requireAuth) {
|
|
109
|
+
const error = `OAuth provider '${providerName}' not configured`;
|
|
110
|
+
if (onValidationError) {
|
|
111
|
+
return onValidationError(request, error);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
status: 401,
|
|
115
|
+
body: {
|
|
116
|
+
error: 'Unauthorized',
|
|
117
|
+
message: error,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return originalMethod.apply(this, args);
|
|
122
|
+
}
|
|
123
|
+
// Validate and refresh session
|
|
124
|
+
const validation = await validateAndRefreshSession(request, providerData.provider, logger);
|
|
125
|
+
if (!validation.valid) {
|
|
126
|
+
// Session validation failed
|
|
127
|
+
logger?.info?.(`OAuth session validation failed: ${validation.error}`);
|
|
128
|
+
if (requireAuth) {
|
|
129
|
+
const error = validation.error || 'OAuth session expired';
|
|
130
|
+
if (onValidationError) {
|
|
131
|
+
return onValidationError(request, error);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
status: 401,
|
|
135
|
+
body: {
|
|
136
|
+
error: 'Unauthorized',
|
|
137
|
+
message: 'OAuth session expired. Please log in again.',
|
|
138
|
+
details: validation.error,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Not requiring auth, but validation failed - continue without OAuth
|
|
143
|
+
return originalMethod.apply(this, args);
|
|
144
|
+
}
|
|
145
|
+
// Session is valid (and possibly refreshed)
|
|
146
|
+
if (validation.refreshed) {
|
|
147
|
+
logger?.debug?.(`OAuth token refreshed for ${providerName} session`);
|
|
148
|
+
}
|
|
149
|
+
// Call original method with validated/refreshed session
|
|
150
|
+
return originalMethod.apply(this, args);
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Helper to get OAuth providers from the OAuth plugin
|
|
157
|
+
* Call this from your application to access the provider registry
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* import { getOAuthProviders } from '@harperfast/oauth';
|
|
162
|
+
*
|
|
163
|
+
* export function handleApplication(scope) {
|
|
164
|
+
* const providers = getOAuthProviders(scope);
|
|
165
|
+
* // Use providers with withOAuthValidation
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export function getOAuthProviders(scope) {
|
|
170
|
+
try {
|
|
171
|
+
// Try to get OAuth plugin from parent scope
|
|
172
|
+
const oauthResource = scope.parent?.resources?.get?.('oauth');
|
|
173
|
+
if (oauthResource?.providers) {
|
|
174
|
+
return oauthResource.providers;
|
|
175
|
+
}
|
|
176
|
+
// Try to get from same scope (if plugin is loaded at same level)
|
|
177
|
+
const localOAuth = scope.resources?.get?.('oauth');
|
|
178
|
+
if (localOAuth?.providers) {
|
|
179
|
+
return localOAuth.providers;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// OAuth module not loaded or accessible
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=withOAuthValidation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withOAuthValidation.js","sourceRoot":"","sources":["../../src/lib/withOAuthValidation.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Plugin Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
import type { IncomingMessage } from 'node:http';
|
|
5
|
+
import type { Scope, User, RequestTarget } from 'harperdb';
|
|
6
|
+
/**
|
|
7
|
+
* OAuth Plugin Configuration
|
|
8
|
+
* Runtime configuration for the OAuth plugin
|
|
9
|
+
*/
|
|
10
|
+
export interface OAuthPluginConfig {
|
|
11
|
+
/** Enable debug mode to expose additional endpoints and information (can be boolean or string from env var) */
|
|
12
|
+
debug?: boolean | string;
|
|
13
|
+
/** OAuth provider configurations */
|
|
14
|
+
providers?: Record<string, any>;
|
|
15
|
+
/** Default redirect URI for all providers */
|
|
16
|
+
redirectUri?: string;
|
|
17
|
+
/** Default post-login redirect path */
|
|
18
|
+
postLoginRedirect?: string;
|
|
19
|
+
/** Default OAuth scopes */
|
|
20
|
+
scope?: string;
|
|
21
|
+
/** Default username claim path */
|
|
22
|
+
usernameClaim?: string;
|
|
23
|
+
/** Default role assignment */
|
|
24
|
+
defaultRole?: string;
|
|
25
|
+
/** Lifecycle hooks */
|
|
26
|
+
hooks?: OAuthHooks;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* OAuth Lifecycle Hooks
|
|
30
|
+
* Callbacks invoked at key points in the OAuth flow
|
|
31
|
+
*/
|
|
32
|
+
export interface OAuthHooks {
|
|
33
|
+
/**
|
|
34
|
+
* Resolve OAuth provider configuration dynamically
|
|
35
|
+
*
|
|
36
|
+
* Called when a provider is not found in the static registry.
|
|
37
|
+
* Allows applications to implement multi-tenant OAuth by
|
|
38
|
+
* returning provider configurations based on naming conventions.
|
|
39
|
+
*
|
|
40
|
+
* Example: Provider name "okta-org_abc123" can be parsed to load
|
|
41
|
+
* organization-specific Okta configuration from database.
|
|
42
|
+
*
|
|
43
|
+
* @param providerName - Provider name from URL path (e.g., "okta-org_abc123")
|
|
44
|
+
* @param logger - Optional logger instance
|
|
45
|
+
* @returns Provider configuration or null if not found
|
|
46
|
+
* @throws Error if resolution fails (returns 500 to client)
|
|
47
|
+
*
|
|
48
|
+
* Security Requirements:
|
|
49
|
+
* - MUST validate tenant ID format before database lookup
|
|
50
|
+
* - MUST validate domain safety (SSRF protection)
|
|
51
|
+
* - MUST validate provider-specific configuration
|
|
52
|
+
* - MUST NOT return configurations for disabled/inactive tenants
|
|
53
|
+
* - SHOULD log all resolution attempts for audit trail
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* onResolveProvider: async (providerName, logger) => {
|
|
58
|
+
* // Parse provider name: "okta-org_abc123" → ["okta", "org_abc123"]
|
|
59
|
+
* const match = providerName.match(/^(okta|azure|auth0)-(.+)$/);
|
|
60
|
+
* if (!match) return null;
|
|
61
|
+
*
|
|
62
|
+
* const [, provider, tenantId] = match;
|
|
63
|
+
*
|
|
64
|
+
* // Validate tenant ID format
|
|
65
|
+
* validateTenantId(tenantId);
|
|
66
|
+
*
|
|
67
|
+
* // Query database for tenant config
|
|
68
|
+
* const org = await Organization.get(tenantId, context);
|
|
69
|
+
* if (!org?.oauthConfig?.enabled) return null;
|
|
70
|
+
*
|
|
71
|
+
* // Build and return provider config
|
|
72
|
+
* return buildProviderConfig(org.oauthConfig, provider);
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
onResolveProvider?: (providerName: string, logger?: Logger) => Promise<OAuthProviderConfig | null>;
|
|
77
|
+
/**
|
|
78
|
+
* Called after successful OAuth login, before session is finalized
|
|
79
|
+
* Use this to provision users, assign roles, etc.
|
|
80
|
+
* @param oauthUser - The OAuth user information
|
|
81
|
+
* @param tokenResponse - The token response from the provider
|
|
82
|
+
* @param session - The current session object
|
|
83
|
+
* @param request - The HTTP request object
|
|
84
|
+
* @param provider - The provider name (e.g., 'github', 'google')
|
|
85
|
+
* @returns Optional data to merge into the session
|
|
86
|
+
*/
|
|
87
|
+
onLogin?: (oauthUser: OAuthUser, tokenResponse: TokenResponse, session: any, request: any, provider: string) => Promise<Record<string, any> | void>;
|
|
88
|
+
/**
|
|
89
|
+
* Called before logout, before session is cleared
|
|
90
|
+
* Use this to clean up user-specific data
|
|
91
|
+
* @param session - The current session object
|
|
92
|
+
* @param request - The HTTP request object
|
|
93
|
+
*/
|
|
94
|
+
onLogout?: (session: any, request: any) => Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Called after token refresh completes
|
|
97
|
+
* @param session - The updated session with new tokens
|
|
98
|
+
* @param refreshed - Whether tokens were actually refreshed
|
|
99
|
+
* @param request - The HTTP request object (may be undefined for background refresh)
|
|
100
|
+
*/
|
|
101
|
+
onTokenRefresh?: (session: any, refreshed: boolean, request?: any) => Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* OAuth Provider Configuration
|
|
105
|
+
* Configuration options for an OAuth 2.0/OIDC provider
|
|
106
|
+
*/
|
|
107
|
+
export interface OAuthProviderConfig {
|
|
108
|
+
/** Provider identifier (e.g., 'github', 'google', 'azure') */
|
|
109
|
+
provider: string;
|
|
110
|
+
clientId: string;
|
|
111
|
+
clientSecret: string;
|
|
112
|
+
authorizationUrl: string;
|
|
113
|
+
tokenUrl: string;
|
|
114
|
+
userInfoUrl: string;
|
|
115
|
+
redirectUri?: string;
|
|
116
|
+
/** OAuth scopes to request (space-separated) */
|
|
117
|
+
scope?: string;
|
|
118
|
+
/** JWKS URI for ID token validation (OIDC only) */
|
|
119
|
+
jwksUri?: string | null;
|
|
120
|
+
/** Expected token issuer for validation (OIDC only) */
|
|
121
|
+
issuer?: string | null;
|
|
122
|
+
/** Claim to use as username (dot notation supported for nested) */
|
|
123
|
+
usernameClaim?: string;
|
|
124
|
+
/** Claim containing user's email address */
|
|
125
|
+
emailClaim?: string;
|
|
126
|
+
/** Claim containing user's display name */
|
|
127
|
+
nameClaim?: string;
|
|
128
|
+
/** Claim containing user's role/group membership */
|
|
129
|
+
roleClaim?: string;
|
|
130
|
+
/** Default role if not found in claims */
|
|
131
|
+
defaultRole?: string;
|
|
132
|
+
/** URL to redirect after successful login */
|
|
133
|
+
postLoginRedirect?: string;
|
|
134
|
+
/** Prefer ID token claims over userinfo endpoint (OIDC) */
|
|
135
|
+
preferIdToken?: boolean;
|
|
136
|
+
/** Whether to fetch email from provider's dedicated email endpoint */
|
|
137
|
+
fetchEmail?: boolean;
|
|
138
|
+
/** Additional query parameters for authorization URL */
|
|
139
|
+
additionalParams?: Record<string, string>;
|
|
140
|
+
/** Custom function to fetch/transform user info */
|
|
141
|
+
getUserInfo?: (accessToken: string, helpers: GetUserInfoHelpers) => Promise<any>;
|
|
142
|
+
/** Custom function to validate token (for non-expiring tokens like GitHub) */
|
|
143
|
+
validateToken?: (accessToken: string, logger?: Logger) => Promise<boolean>;
|
|
144
|
+
/** Interval for periodic token validation (ms) - only for tokens without expiration */
|
|
145
|
+
tokenValidationInterval?: number;
|
|
146
|
+
/** Provider-specific configuration function (e.g., for tenant/domain setup) */
|
|
147
|
+
configure?: (param: string) => Partial<OAuthProviderConfig>;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Helpers passed to custom getUserInfo function
|
|
151
|
+
*/
|
|
152
|
+
export interface GetUserInfoHelpers {
|
|
153
|
+
/** Default getUserInfo implementation to call */
|
|
154
|
+
getUserInfo: (accessToken: string) => Promise<any>;
|
|
155
|
+
logger?: Logger;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* OAuth User Object
|
|
159
|
+
* Represents a user authenticated via OAuth
|
|
160
|
+
*/
|
|
161
|
+
export interface OAuthUser {
|
|
162
|
+
/** Username extracted from configured claim */
|
|
163
|
+
username: string;
|
|
164
|
+
role: string;
|
|
165
|
+
/** OAuth provider name */
|
|
166
|
+
provider: string;
|
|
167
|
+
/** User ID from the OAuth provider */
|
|
168
|
+
providerUserId?: string;
|
|
169
|
+
email?: string;
|
|
170
|
+
name?: string;
|
|
171
|
+
/** Additional provider-specific data */
|
|
172
|
+
metadata?: Record<string, any>;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* OAuth Token Response
|
|
176
|
+
* Standard OAuth 2.0 token endpoint response
|
|
177
|
+
*/
|
|
178
|
+
export interface TokenResponse {
|
|
179
|
+
access_token: string;
|
|
180
|
+
/** Usually 'Bearer' */
|
|
181
|
+
token_type?: string;
|
|
182
|
+
/** Token lifetime in seconds */
|
|
183
|
+
expires_in?: number;
|
|
184
|
+
refresh_token?: string;
|
|
185
|
+
/** ID token for OIDC providers */
|
|
186
|
+
id_token?: string;
|
|
187
|
+
/** Space-separated granted scopes */
|
|
188
|
+
scope?: string;
|
|
189
|
+
/** Error code if token request failed (some providers return 200 with error) */
|
|
190
|
+
error?: string;
|
|
191
|
+
/** Human-readable error description */
|
|
192
|
+
error_description?: string;
|
|
193
|
+
/** URL to documentation about the error */
|
|
194
|
+
error_uri?: string;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* CSRF Token Data
|
|
198
|
+
* Metadata stored with CSRF tokens during OAuth flow
|
|
199
|
+
*/
|
|
200
|
+
export interface CSRFTokenData {
|
|
201
|
+
/** Unix timestamp when token was created */
|
|
202
|
+
timestamp: number;
|
|
203
|
+
/** URL to redirect to after successful authentication */
|
|
204
|
+
originalUrl?: string;
|
|
205
|
+
/** Session ID to link OAuth flow with existing session */
|
|
206
|
+
sessionId?: string;
|
|
207
|
+
[key: string]: any;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* OAuth Provider Interface
|
|
211
|
+
* Methods that OAuthProvider class implements
|
|
212
|
+
*/
|
|
213
|
+
export interface IOAuthProvider {
|
|
214
|
+
config: OAuthProviderConfig;
|
|
215
|
+
logger?: Logger;
|
|
216
|
+
/** Generate CSRF token for OAuth state parameter */
|
|
217
|
+
generateCSRFToken(metadata: Record<string, any>): Promise<string>;
|
|
218
|
+
/** Verify and consume CSRF token (one-time use) */
|
|
219
|
+
verifyCSRFToken(token: string): Promise<CSRFTokenData | null>;
|
|
220
|
+
/** Build authorization URL with all required parameters */
|
|
221
|
+
getAuthorizationUrl(state: string, redirectUri: string): string;
|
|
222
|
+
/** Exchange authorization code for access/refresh tokens */
|
|
223
|
+
exchangeCodeForToken(code: string, redirectUri: string): Promise<TokenResponse>;
|
|
224
|
+
/** Fetch user information from provider */
|
|
225
|
+
getUserInfo(accessToken: string, idTokenClaims?: any): Promise<any>;
|
|
226
|
+
/** Map provider user info to Harper user format */
|
|
227
|
+
mapUserToHarper(userInfo: any): OAuthUser;
|
|
228
|
+
/** Verify and decode ID token (OIDC only) */
|
|
229
|
+
verifyIdToken?(idToken: string): Promise<any>;
|
|
230
|
+
/** Exchange refresh token for new access token */
|
|
231
|
+
refreshAccessToken?(refreshToken: string): Promise<TokenResponse>;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Provider Registry Entry
|
|
235
|
+
* Stores initialized provider instance with its config
|
|
236
|
+
*/
|
|
237
|
+
export interface ProviderRegistryEntry {
|
|
238
|
+
/** Initialized OAuth provider instance */
|
|
239
|
+
provider: IOAuthProvider;
|
|
240
|
+
config: OAuthProviderConfig;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Provider Registry
|
|
244
|
+
* Collection of all configured OAuth providers keyed by name
|
|
245
|
+
*/
|
|
246
|
+
export interface ProviderRegistry {
|
|
247
|
+
[providerName: string]: ProviderRegistryEntry;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Harper Table Instance
|
|
251
|
+
* Methods available on a Harper table
|
|
252
|
+
*/
|
|
253
|
+
export interface Table {
|
|
254
|
+
get(id: string): Promise<any>;
|
|
255
|
+
put(record: any): Promise<any>;
|
|
256
|
+
delete(id: string): Promise<void>;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Logger Interface
|
|
260
|
+
* Harper's logger interface for component logging
|
|
261
|
+
*/
|
|
262
|
+
export interface Logger {
|
|
263
|
+
info?: (message: string, ...args: any[]) => void;
|
|
264
|
+
error?: (message: string, ...args: any[]) => void;
|
|
265
|
+
warn?: (message: string, ...args: any[]) => void;
|
|
266
|
+
debug?: (message: string, ...args: any[]) => void;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Harper HTTP Request
|
|
270
|
+
* Extended Node.js IncomingMessage with Harper additions
|
|
271
|
+
*/
|
|
272
|
+
export interface Request extends IncomingMessage {
|
|
273
|
+
/** Authenticated Harper user or username string */
|
|
274
|
+
user?: User | string;
|
|
275
|
+
session?: Session;
|
|
276
|
+
headers: IncomingMessage['headers'];
|
|
277
|
+
/** Client IP address */
|
|
278
|
+
ip?: string;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* OAuth Session Metadata
|
|
282
|
+
* Token and expiration data stored in session for automatic refresh
|
|
283
|
+
*/
|
|
284
|
+
export interface OAuthSessionMetadata {
|
|
285
|
+
/**
|
|
286
|
+
* Provider configuration ID/key from config (e.g., 'my-custom-github', 'production-okta').
|
|
287
|
+
* @deprecated Use `providerConfigId` instead. This field is maintained for backwards compatibility only.
|
|
288
|
+
*/
|
|
289
|
+
provider: string;
|
|
290
|
+
/** Provider configuration ID/key from config (e.g., 'my-custom-github', 'production-okta'). */
|
|
291
|
+
providerConfigId: string;
|
|
292
|
+
/** OAuth provider type (e.g., 'github', 'google', 'okta') */
|
|
293
|
+
providerType: string;
|
|
294
|
+
/** Current access token */
|
|
295
|
+
accessToken: string;
|
|
296
|
+
/** Refresh token for obtaining new access tokens */
|
|
297
|
+
refreshToken?: string;
|
|
298
|
+
/** Unix timestamp (ms) when the access token expires */
|
|
299
|
+
expiresAt?: number;
|
|
300
|
+
/** Unix timestamp (ms) when to proactively refresh (80% of lifetime) */
|
|
301
|
+
refreshThreshold?: number;
|
|
302
|
+
/** Space-separated list of granted scopes */
|
|
303
|
+
scope?: string;
|
|
304
|
+
/** Token type (usually 'Bearer') */
|
|
305
|
+
tokenType?: string;
|
|
306
|
+
/** Unix timestamp (ms) of last successful token refresh */
|
|
307
|
+
lastRefreshed?: number;
|
|
308
|
+
/** Unix timestamp (ms) of last token validation (for non-expiring tokens) */
|
|
309
|
+
lastValidated?: number;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Harper Session
|
|
313
|
+
* Session data stored for authenticated users
|
|
314
|
+
*/
|
|
315
|
+
export interface Session {
|
|
316
|
+
id?: string;
|
|
317
|
+
/** Harper username (string) */
|
|
318
|
+
user?: string;
|
|
319
|
+
/** Full OAuth user object */
|
|
320
|
+
oauthUser?: OAuthUser;
|
|
321
|
+
/** OAuth session metadata for automatic token refresh */
|
|
322
|
+
oauth?: OAuthSessionMetadata;
|
|
323
|
+
/** Async session update method (when available) */
|
|
324
|
+
update?: (data: Partial<Session>) => Promise<void>;
|
|
325
|
+
}
|
|
326
|
+
export type { Scope, User, RequestTarget };
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@harperfast/oauth",
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"description": "OAuth 2.0 authentication plugin for Harper",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "HarperDB, Inc.",
|
|
8
|
+
"email": "opensource@harperdb.io",
|
|
9
|
+
"url": "https://harper.fast/"
|
|
10
|
+
},
|
|
11
|
+
"contributors": [],
|
|
12
|
+
"homepage": "https://harper.fast/",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/HarperFast/oauth.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/HarperFast/oauth/issues",
|
|
19
|
+
"email": "opensource@harperdb.io"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"harperdb",
|
|
23
|
+
"harper",
|
|
24
|
+
"plugin",
|
|
25
|
+
"oauth",
|
|
26
|
+
"oauth2",
|
|
27
|
+
"authentication",
|
|
28
|
+
"oidc",
|
|
29
|
+
"openid"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"main": "dist/index.js",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": "./dist/index.js",
|
|
35
|
+
"./config": "./config.yaml"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist/",
|
|
39
|
+
"assets/",
|
|
40
|
+
"schema/",
|
|
41
|
+
"config.yaml",
|
|
42
|
+
"LICENSE",
|
|
43
|
+
"README.md"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc || true",
|
|
47
|
+
"dev": "tsc --watch",
|
|
48
|
+
"test": "npm run build && node --test \"test/**/*.test.js\"",
|
|
49
|
+
"test:watch": "npm run build && node --test --watch \"test/**/*.test.js\"",
|
|
50
|
+
"test:coverage": "npm run build && node --enable-source-maps --test --experimental-test-coverage --test-coverage-exclude='test/**' --test-coverage-exclude='**/harperdb/**' \"test/**/*.test.js\"",
|
|
51
|
+
"test:node-twenty": "npm run build && node --test test/",
|
|
52
|
+
"lint": "eslint . --ignore-pattern 'dist/**'",
|
|
53
|
+
"format": "prettier .",
|
|
54
|
+
"format:check": "npm run format -- --check",
|
|
55
|
+
"format:write": "npm run format -- --write",
|
|
56
|
+
"prepublishOnly": "npm run build"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"jsonwebtoken": "^9.0.2",
|
|
60
|
+
"jwks-rsa": "^3.1.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@harperdb/code-guidelines": "^0.0.5",
|
|
64
|
+
"@types/jsonwebtoken": "^9.0.5",
|
|
65
|
+
"@types/node": "^20.11.0",
|
|
66
|
+
"eslint": "^9.35.0",
|
|
67
|
+
"harperdb": "^4.7.14",
|
|
68
|
+
"prettier": "^3.6.2",
|
|
69
|
+
"typescript": "^5.3.3"
|
|
70
|
+
},
|
|
71
|
+
"peerDependencies": {
|
|
72
|
+
"harperdb": ">=4.6.0"
|
|
73
|
+
},
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=20",
|
|
76
|
+
"bun": ">=1.0"
|
|
77
|
+
},
|
|
78
|
+
"devEngines": {
|
|
79
|
+
"runtime": {
|
|
80
|
+
"name": "node",
|
|
81
|
+
"version": ">=20",
|
|
82
|
+
"onFail": "error"
|
|
83
|
+
},
|
|
84
|
+
"packageManager": {
|
|
85
|
+
"name": "npm",
|
|
86
|
+
"onFail": "error"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## OAuth Plugin Schema
|
|
2
|
+
## This defines the tables needed for OAuth functionality
|
|
3
|
+
|
|
4
|
+
## CSRF Token storage table in oauth database
|
|
5
|
+
## Stores temporary CSRF tokens for OAuth flows (10 minute expiration)
|
|
6
|
+
type csrf_tokens @table(database: "oauth", expiration: 600) {
|
|
7
|
+
token_id: ID @primaryKey
|
|
8
|
+
data: String # JSON stringified CSRFTokenData
|
|
9
|
+
created_at: Float
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
## OAuth User Session table (optional, for future use)
|
|
13
|
+
## Could store OAuth-specific user data
|
|
14
|
+
# type oauth_sessions @table(database: "oauth") {
|
|
15
|
+
# username: ID @primaryKey
|
|
16
|
+
# provider: String @indexed
|
|
17
|
+
# provider_id: String
|
|
18
|
+
# access_token: String
|
|
19
|
+
# refresh_token: String
|
|
20
|
+
# expires_at: Float
|
|
21
|
+
# }
|