raffel 0.1.2 → 0.2.2
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 +314 -346
- package/dist/adapters/index.d.ts +3 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +3 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/s3db/adapter.d.ts.map +1 -1
- package/dist/adapters/s3db/adapter.js +0 -3
- package/dist/adapters/s3db/adapter.js.map +1 -1
- package/dist/adapters/udp.d.ts +83 -0
- package/dist/adapters/udp.d.ts.map +1 -0
- package/dist/adapters/udp.int.test.d.ts +5 -0
- package/dist/adapters/udp.int.test.d.ts.map +1 -0
- package/dist/adapters/udp.int.test.js +397 -0
- package/dist/adapters/udp.int.test.js.map +1 -0
- package/dist/adapters/udp.js +391 -0
- package/dist/adapters/udp.js.map +1 -0
- package/dist/cache/drivers/file.d.ts.map +1 -1
- package/dist/cache/drivers/file.js +13 -1
- package/dist/cache/drivers/file.js.map +1 -1
- package/dist/cache/drivers/memory.d.ts.map +1 -1
- package/dist/cache/drivers/memory.js +1 -0
- package/dist/cache/drivers/memory.js.map +1 -1
- package/dist/cache/types.d.ts +1 -0
- package/dist/cache/types.d.ts.map +1 -1
- package/dist/docs/generators/http-generator.d.ts.map +1 -1
- package/dist/docs/generators/http-generator.js +0 -1
- package/dist/docs/generators/http-generator.js.map +1 -1
- package/dist/graphql/graphql.int.test.d.ts +10 -0
- package/dist/graphql/graphql.int.test.d.ts.map +1 -0
- package/dist/graphql/graphql.int.test.js +698 -0
- package/dist/graphql/graphql.int.test.js.map +1 -0
- package/dist/graphql/schema-generator.d.ts.map +1 -1
- package/dist/graphql/schema-generator.js +20 -7
- package/dist/graphql/schema-generator.js.map +1 -1
- package/dist/http/auth.d.ts.map +1 -1
- package/dist/http/auth.js +15 -1
- package/dist/http/auth.js.map +1 -1
- package/dist/http/http.int.test.d.ts +7 -0
- package/dist/http/http.int.test.d.ts.map +1 -0
- package/dist/http/http.int.test.js +604 -0
- package/dist/http/http.int.test.js.map +1 -0
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/index.js +2 -0
- package/dist/http/index.js.map +1 -1
- package/dist/http/oauth2.d.ts.map +1 -1
- package/dist/http/oauth2.js +39 -0
- package/dist/http/oauth2.js.map +1 -1
- package/dist/http/oidc.d.ts.map +1 -1
- package/dist/http/oidc.js +9 -1
- package/dist/http/oidc.js.map +1 -1
- package/dist/http/session-redis.d.ts +187 -0
- package/dist/http/session-redis.d.ts.map +1 -0
- package/dist/http/session-redis.int.test.d.ts +8 -0
- package/dist/http/session-redis.int.test.d.ts.map +1 -0
- package/dist/http/session-redis.int.test.js +492 -0
- package/dist/http/session-redis.int.test.js.map +1 -0
- package/dist/http/session-redis.js +320 -0
- package/dist/http/session-redis.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/cli.js +2 -1
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/docs/adapters.d.ts.map +1 -1
- package/dist/mcp/docs/adapters.js +175 -145
- package/dist/mcp/docs/adapters.js.map +1 -1
- package/dist/mcp/docs/interceptors.d.ts +1 -1
- package/dist/mcp/docs/interceptors.d.ts.map +1 -1
- package/dist/mcp/docs/interceptors.js +231 -305
- package/dist/mcp/docs/interceptors.js.map +1 -1
- package/dist/mcp/docs/patterns.d.ts.map +1 -1
- package/dist/mcp/docs/patterns.js +20 -18
- package/dist/mcp/docs/patterns.js.map +1 -1
- package/dist/mcp/docs/quickstart.d.ts +1 -1
- package/dist/mcp/docs/quickstart.d.ts.map +1 -1
- package/dist/mcp/docs/quickstart.js +48 -46
- package/dist/mcp/docs/quickstart.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +6 -7
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/version.d.ts +7 -0
- package/dist/mcp/version.d.ts.map +1 -0
- package/dist/mcp/version.js +20 -0
- package/dist/mcp/version.js.map +1 -0
- package/dist/middleware/auth/oauth2.d.ts +294 -0
- package/dist/middleware/auth/oauth2.d.ts.map +1 -0
- package/dist/middleware/auth/oauth2.int.test.d.ts +2 -0
- package/dist/middleware/auth/oauth2.int.test.d.ts.map +1 -0
- package/dist/middleware/auth/oauth2.int.test.js +714 -0
- package/dist/middleware/auth/oauth2.int.test.js.map +1 -0
- package/dist/middleware/auth/oauth2.js +671 -0
- package/dist/middleware/auth/oauth2.js.map +1 -0
- package/dist/middleware/auth.d.ts +2 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +16 -0
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/index.d.ts +5 -2
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +4 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/interceptors/circuit-breaker.d.ts.map +1 -1
- package/dist/middleware/interceptors/circuit-breaker.js +0 -1
- package/dist/middleware/interceptors/circuit-breaker.js.map +1 -1
- package/dist/middleware/interceptors/envelope.d.ts +176 -0
- package/dist/middleware/interceptors/envelope.d.ts.map +1 -0
- package/dist/middleware/interceptors/envelope.int.test.d.ts +5 -0
- package/dist/middleware/interceptors/envelope.int.test.d.ts.map +1 -0
- package/dist/middleware/interceptors/envelope.int.test.js +409 -0
- package/dist/middleware/interceptors/envelope.int.test.js.map +1 -0
- package/dist/middleware/interceptors/envelope.js +294 -0
- package/dist/middleware/interceptors/envelope.js.map +1 -0
- package/dist/middleware/interceptors/index.d.ts +2 -0
- package/dist/middleware/interceptors/index.d.ts.map +1 -1
- package/dist/middleware/interceptors/index.js +2 -0
- package/dist/middleware/interceptors/index.js.map +1 -1
- package/dist/middleware/types.d.ts +25 -0
- package/dist/middleware/types.d.ts.map +1 -1
- package/dist/rate-limit/drivers/drivers.int.test.d.ts +7 -0
- package/dist/rate-limit/drivers/drivers.int.test.d.ts.map +1 -0
- package/dist/rate-limit/drivers/drivers.int.test.js +466 -0
- package/dist/rate-limit/drivers/drivers.int.test.js.map +1 -0
- package/dist/server/builder.d.ts.map +1 -1
- package/dist/server/builder.int.test.js +41 -0
- package/dist/server/builder.int.test.js.map +1 -1
- package/dist/server/builder.js +72 -15
- package/dist/server/builder.js.map +1 -1
- package/dist/server/channel-utils.d.ts +4 -1
- package/dist/server/channel-utils.d.ts.map +1 -1
- package/dist/server/channel-utils.js +12 -2
- package/dist/server/channel-utils.js.map +1 -1
- package/dist/server/errors.d.ts.map +1 -1
- package/dist/server/errors.js +0 -22
- package/dist/server/errors.js.map +1 -1
- package/dist/server/fs-routes/watcher.js +1 -1
- package/dist/server/fs-routes/watcher.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/types.d.ts +37 -33
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tracing/interceptor.d.ts.map +1 -1
- package/dist/tracing/interceptor.js +4 -5
- package/dist/tracing/interceptor.js.map +1 -1
- package/dist/types/envelope.d.ts +1 -1
- package/dist/types/envelope.d.ts.map +1 -1
- package/dist/types/envelope.js.map +1 -1
- package/dist/types/handlers.d.ts +8 -0
- package/dist/types/handlers.d.ts.map +1 -1
- package/dist/ui/core/index.d.ts +7 -0
- package/dist/ui/core/index.d.ts.map +1 -0
- package/dist/ui/docs/generators/content-types.d.ts +10 -0
- package/dist/ui/docs/generators/content-types.d.ts.map +1 -0
- package/dist/ui/docs/generators/errors-types.d.ts +409 -0
- package/dist/ui/docs/generators/errors-types.d.ts.map +1 -0
- package/dist/ui/docs/generators/errors.d.ts +88 -0
- package/dist/ui/docs/generators/errors.d.ts.map +1 -0
- package/dist/ui/docs/generators/grpc-generator.d.ts +53 -0
- package/dist/ui/docs/generators/grpc-generator.d.ts.map +1 -0
- package/dist/ui/docs/generators/http-generator.d.ts +49 -0
- package/dist/ui/docs/generators/http-generator.d.ts.map +1 -0
- package/dist/ui/docs/generators/index.d.ts +17 -0
- package/dist/ui/docs/generators/index.d.ts.map +1 -0
- package/dist/ui/docs/generators/jsonrpc-generator.d.ts +53 -0
- package/dist/ui/docs/generators/jsonrpc-generator.d.ts.map +1 -0
- package/dist/ui/docs/generators/schema-converter.d.ts +117 -0
- package/dist/ui/docs/generators/schema-converter.d.ts.map +1 -0
- package/dist/ui/docs/generators/streams-generator.d.ts +85 -0
- package/dist/ui/docs/generators/streams-generator.d.ts.map +1 -0
- package/dist/ui/docs/generators/tcp-generator.d.ts +133 -0
- package/dist/ui/docs/generators/tcp-generator.d.ts.map +1 -0
- package/dist/ui/docs/generators/udp-generator.d.ts +119 -0
- package/dist/ui/docs/generators/udp-generator.d.ts.map +1 -0
- package/dist/ui/docs/generators/usd-generator.d.ts +182 -0
- package/dist/ui/docs/generators/usd-generator.d.ts.map +1 -0
- package/dist/ui/docs/generators/websocket-generator.d.ts +49 -0
- package/dist/ui/docs/generators/websocket-generator.d.ts.map +1 -0
- package/dist/ui/docs/index.d.ts +31 -0
- package/dist/ui/docs/index.d.ts.map +1 -0
- package/dist/ui/docs/usd-middleware.d.ts +157 -0
- package/dist/ui/docs/usd-middleware.d.ts.map +1 -0
- package/dist/ui/errors/factories.d.ts +142 -0
- package/dist/ui/errors/factories.d.ts.map +1 -0
- package/dist/ui/errors/index.d.ts +9 -0
- package/dist/ui/errors/index.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/index.d.ts +66 -0
- package/dist/ui/server/fs-routes/index.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/loader.d.ts +28 -0
- package/dist/ui/server/fs-routes/loader.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/middleware-processor.d.ts +19 -0
- package/dist/ui/server/fs-routes/middleware-processor.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/resources/index.d.ts +8 -0
- package/dist/ui/server/fs-routes/resources/index.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/resources/loader.d.ts +16 -0
- package/dist/ui/server/fs-routes/resources/loader.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/resources/types.d.ts +256 -0
- package/dist/ui/server/fs-routes/resources/types.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/rest/index.d.ts +8 -0
- package/dist/ui/server/fs-routes/rest/index.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/rest/loader.d.ts +11 -0
- package/dist/ui/server/fs-routes/rest/loader.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/rest/types.d.ts +288 -0
- package/dist/ui/server/fs-routes/rest/types.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/tcp/index.d.ts +8 -0
- package/dist/ui/server/fs-routes/tcp/index.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/tcp/loader.d.ts +15 -0
- package/dist/ui/server/fs-routes/tcp/loader.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/tcp/types.d.ts +215 -0
- package/dist/ui/server/fs-routes/tcp/types.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/types.d.ts +437 -0
- package/dist/ui/server/fs-routes/types.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/udp/index.d.ts +8 -0
- package/dist/ui/server/fs-routes/udp/index.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/udp/loader.d.ts +15 -0
- package/dist/ui/server/fs-routes/udp/loader.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/udp/types.d.ts +164 -0
- package/dist/ui/server/fs-routes/udp/types.d.ts.map +1 -0
- package/dist/ui/server/fs-routes/watcher.d.ts +34 -0
- package/dist/ui/server/fs-routes/watcher.d.ts.map +1 -0
- package/dist/ui/types/envelope.d.ts +1 -1
- package/dist/ui/types/envelope.d.ts.map +1 -1
- package/dist/ui/types/handlers.d.ts +8 -0
- package/dist/ui/types/handlers.d.ts.map +1 -1
- package/dist/ui/usd/builder/document.d.ts.map +1 -1
- package/dist/ui/usd/export/openapi.d.ts.map +1 -1
- package/dist/ui/usd/parser/normalize.d.ts.map +1 -1
- package/dist/ui/usd/spec/types.d.ts +14 -20
- package/dist/ui/usd/spec/types.d.ts.map +1 -1
- package/dist/ui/usd/utils/refs.d.ts.map +1 -1
- package/dist/ui/usd/validator/index.d.ts.map +1 -1
- package/dist/ui/usd/validator/schema.d.ts.map +1 -1
- package/dist/ui/usd/validator/semantic.d.ts.map +1 -1
- package/dist/ui/utils/logger.d.ts +15 -0
- package/dist/ui/utils/logger.d.ts.map +1 -0
- package/dist/usd/builder/document.d.ts.map +1 -1
- package/dist/usd/builder/document.js.map +1 -1
- package/dist/usd/export/openapi.d.ts.map +1 -1
- package/dist/usd/export/openapi.js +2 -4
- package/dist/usd/export/openapi.js.map +1 -1
- package/dist/usd/parser/normalize.d.ts.map +1 -1
- package/dist/usd/parser/normalize.js +0 -1
- package/dist/usd/parser/normalize.js.map +1 -1
- package/dist/usd/usd.int.test.d.ts +10 -0
- package/dist/usd/usd.int.test.d.ts.map +1 -0
- package/dist/usd/usd.int.test.js +719 -0
- package/dist/usd/usd.int.test.js.map +1 -0
- package/dist/usd/utils/refs.d.ts.map +1 -1
- package/dist/usd/validator/index.d.ts.map +1 -1
- package/dist/usd/validator/index.js.map +1 -1
- package/dist/usd/validator/schema.d.ts.map +1 -1
- package/dist/usd/validator/schema.js.map +1 -1
- package/dist/usd/validator/semantic.d.ts.map +1 -1
- package/dist/usd/validator/semantic.js.map +1 -1
- package/package.json +1 -1
- package/dist/middleware/rate-limit.d.ts +0 -105
- package/dist/middleware/rate-limit.d.ts.map +0 -1
- package/dist/middleware/rate-limit.int.test.d.ts +0 -5
- package/dist/middleware/rate-limit.int.test.d.ts.map +0 -1
- package/dist/middleware/rate-limit.int.test.js +0 -350
- package/dist/middleware/rate-limit.int.test.js.map +0 -1
- package/dist/middleware/rate-limit.js +0 -206
- package/dist/middleware/rate-limit.js.map +0 -1
- package/dist/openapi/index.d.ts +0 -9
- package/dist/openapi/index.d.ts.map +0 -1
- package/dist/openapi/index.js +0 -9
- package/dist/openapi/index.js.map +0 -1
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth2/OIDC Strategy Integration Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
5
|
+
import { createOAuth2Strategy, createOIDCStrategy, createGoogleOAuth2Strategy, createGitHubOAuth2Strategy, createMicrosoftOAuth2Strategy, createAppleOAuth2Strategy, createFacebookOAuth2Strategy, generateState, generateNonce, clearDiscoveryCache, OAuth2Providers, } from './oauth2.js';
|
|
6
|
+
// Mock fetch globally
|
|
7
|
+
const mockFetch = vi.fn();
|
|
8
|
+
global.fetch = mockFetch;
|
|
9
|
+
// Helper to create test envelope
|
|
10
|
+
function createTestEnvelope(metadata = {}) {
|
|
11
|
+
return {
|
|
12
|
+
id: 'test-1',
|
|
13
|
+
procedure: 'test.procedure',
|
|
14
|
+
type: 'request',
|
|
15
|
+
payload: {},
|
|
16
|
+
metadata,
|
|
17
|
+
context: createTestContext(),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// Helper to create test context
|
|
21
|
+
function createTestContext() {
|
|
22
|
+
return {
|
|
23
|
+
requestId: 'req-123',
|
|
24
|
+
tracing: {
|
|
25
|
+
traceId: 'trace-123',
|
|
26
|
+
spanId: 'span-123',
|
|
27
|
+
},
|
|
28
|
+
signal: new AbortController().signal,
|
|
29
|
+
extensions: new Map(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
describe('OAuth2 Strategy', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
clearDiscoveryCache();
|
|
36
|
+
});
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.restoreAllMocks();
|
|
39
|
+
});
|
|
40
|
+
describe('createOAuth2Strategy', () => {
|
|
41
|
+
it('should create strategy with Google provider preset', () => {
|
|
42
|
+
const strategy = createOAuth2Strategy({
|
|
43
|
+
provider: 'google',
|
|
44
|
+
clientId: 'client-id',
|
|
45
|
+
clientSecret: 'client-secret',
|
|
46
|
+
redirectUri: 'https://example.com/callback',
|
|
47
|
+
});
|
|
48
|
+
expect(strategy.name).toBe('oauth2:google');
|
|
49
|
+
expect(strategy.authenticate).toBeDefined();
|
|
50
|
+
expect(strategy.getAuthorizationUrl).toBeDefined();
|
|
51
|
+
expect(strategy.exchangeCode).toBeDefined();
|
|
52
|
+
expect(strategy.refreshToken).toBeDefined();
|
|
53
|
+
expect(strategy.getUserInfo).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
it('should create strategy with custom provider', () => {
|
|
56
|
+
const strategy = createOAuth2Strategy({
|
|
57
|
+
provider: 'custom',
|
|
58
|
+
clientId: 'client-id',
|
|
59
|
+
clientSecret: 'client-secret',
|
|
60
|
+
redirectUri: 'https://example.com/callback',
|
|
61
|
+
authorizationUrl: 'https://custom.auth/authorize',
|
|
62
|
+
tokenUrl: 'https://custom.auth/token',
|
|
63
|
+
});
|
|
64
|
+
expect(strategy.name).toBe('oauth2:custom');
|
|
65
|
+
});
|
|
66
|
+
it('should return null when no Authorization header present', async () => {
|
|
67
|
+
const strategy = createOAuth2Strategy({
|
|
68
|
+
provider: 'google',
|
|
69
|
+
clientId: 'client-id',
|
|
70
|
+
clientSecret: 'client-secret',
|
|
71
|
+
redirectUri: 'https://example.com/callback',
|
|
72
|
+
});
|
|
73
|
+
const envelope = createTestEnvelope();
|
|
74
|
+
const ctx = createTestContext();
|
|
75
|
+
const result = await strategy.authenticate(envelope, ctx);
|
|
76
|
+
expect(result).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
it('should return null for non-Bearer tokens', async () => {
|
|
79
|
+
const strategy = createOAuth2Strategy({
|
|
80
|
+
provider: 'google',
|
|
81
|
+
clientId: 'client-id',
|
|
82
|
+
clientSecret: 'client-secret',
|
|
83
|
+
redirectUri: 'https://example.com/callback',
|
|
84
|
+
});
|
|
85
|
+
const envelope = createTestEnvelope({
|
|
86
|
+
authorization: 'Basic dXNlcjpwYXNz',
|
|
87
|
+
});
|
|
88
|
+
const ctx = createTestContext();
|
|
89
|
+
const result = await strategy.authenticate(envelope, ctx);
|
|
90
|
+
expect(result).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
it('should validate token via introspection endpoint', async () => {
|
|
93
|
+
mockFetch.mockResolvedValueOnce({
|
|
94
|
+
ok: true,
|
|
95
|
+
json: async () => ({
|
|
96
|
+
active: true,
|
|
97
|
+
sub: 'user-123',
|
|
98
|
+
username: 'testuser',
|
|
99
|
+
scope: 'openid email profile',
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
const strategy = createOAuth2Strategy({
|
|
103
|
+
provider: 'custom',
|
|
104
|
+
clientId: 'client-id',
|
|
105
|
+
clientSecret: 'client-secret',
|
|
106
|
+
redirectUri: 'https://example.com/callback',
|
|
107
|
+
authorizationUrl: 'https://auth.example.com/authorize',
|
|
108
|
+
tokenUrl: 'https://auth.example.com/token',
|
|
109
|
+
introspectionUrl: 'https://auth.example.com/introspect',
|
|
110
|
+
tokenValidation: 'introspection', // Must specify introspection
|
|
111
|
+
});
|
|
112
|
+
const envelope = createTestEnvelope({
|
|
113
|
+
authorization: 'Bearer valid-token',
|
|
114
|
+
});
|
|
115
|
+
const ctx = createTestContext();
|
|
116
|
+
const result = await strategy.authenticate(envelope, ctx);
|
|
117
|
+
expect(result).toEqual({
|
|
118
|
+
authenticated: true,
|
|
119
|
+
principal: 'user-123',
|
|
120
|
+
claims: {
|
|
121
|
+
active: true,
|
|
122
|
+
sub: 'user-123',
|
|
123
|
+
username: 'testuser',
|
|
124
|
+
scope: 'openid email profile',
|
|
125
|
+
accessToken: 'valid-token',
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
it('should return unauthenticated for inactive token', async () => {
|
|
130
|
+
mockFetch.mockResolvedValueOnce({
|
|
131
|
+
ok: true,
|
|
132
|
+
json: async () => ({
|
|
133
|
+
active: false,
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
const strategy = createOAuth2Strategy({
|
|
137
|
+
provider: 'custom',
|
|
138
|
+
clientId: 'client-id',
|
|
139
|
+
clientSecret: 'client-secret',
|
|
140
|
+
redirectUri: 'https://example.com/callback',
|
|
141
|
+
authorizationUrl: 'https://auth.example.com/authorize',
|
|
142
|
+
tokenUrl: 'https://auth.example.com/token',
|
|
143
|
+
introspectionUrl: 'https://auth.example.com/introspect',
|
|
144
|
+
tokenValidation: 'introspection', // Must specify introspection
|
|
145
|
+
});
|
|
146
|
+
const envelope = createTestEnvelope({
|
|
147
|
+
authorization: 'Bearer invalid-token',
|
|
148
|
+
});
|
|
149
|
+
const ctx = createTestContext();
|
|
150
|
+
const result = await strategy.authenticate(envelope, ctx);
|
|
151
|
+
expect(result).toEqual({ authenticated: false });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('getAuthorizationUrl', () => {
|
|
155
|
+
it('should generate authorization URL with all parameters', () => {
|
|
156
|
+
const strategy = createOAuth2Strategy({
|
|
157
|
+
provider: 'google',
|
|
158
|
+
clientId: 'client-id',
|
|
159
|
+
clientSecret: 'client-secret',
|
|
160
|
+
redirectUri: 'https://example.com/callback',
|
|
161
|
+
scopes: ['openid', 'email', 'profile'],
|
|
162
|
+
});
|
|
163
|
+
const url = strategy.getAuthorizationUrl({
|
|
164
|
+
state: 'random-state',
|
|
165
|
+
nonce: 'random-nonce',
|
|
166
|
+
additionalParams: {
|
|
167
|
+
prompt: 'consent',
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
expect(url).toContain('https://accounts.google.com/o/oauth2/v2/auth');
|
|
171
|
+
expect(url).toContain('client_id=client-id');
|
|
172
|
+
expect(url).toContain('redirect_uri=https%3A%2F%2Fexample.com%2Fcallback');
|
|
173
|
+
expect(url).toContain('response_type=code');
|
|
174
|
+
expect(url).toContain('scope=openid+email+profile');
|
|
175
|
+
expect(url).toContain('state=random-state');
|
|
176
|
+
expect(url).toContain('nonce=random-nonce');
|
|
177
|
+
expect(url).toContain('prompt=consent');
|
|
178
|
+
});
|
|
179
|
+
it('should include PKCE parameters via additionalParams', () => {
|
|
180
|
+
const strategy = createOAuth2Strategy({
|
|
181
|
+
provider: 'google',
|
|
182
|
+
clientId: 'client-id',
|
|
183
|
+
clientSecret: 'client-secret',
|
|
184
|
+
redirectUri: 'https://example.com/callback',
|
|
185
|
+
});
|
|
186
|
+
const url = strategy.getAuthorizationUrl({
|
|
187
|
+
additionalParams: {
|
|
188
|
+
code_challenge: 'challenge-value',
|
|
189
|
+
code_challenge_method: 'S256',
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
expect(url).toContain('code_challenge=challenge-value');
|
|
193
|
+
expect(url).toContain('code_challenge_method=S256');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe('exchangeCode', () => {
|
|
197
|
+
it('should exchange authorization code for tokens', async () => {
|
|
198
|
+
mockFetch.mockResolvedValueOnce({
|
|
199
|
+
ok: true,
|
|
200
|
+
json: async () => ({
|
|
201
|
+
access_token: 'access-token-123',
|
|
202
|
+
token_type: 'Bearer',
|
|
203
|
+
expires_in: 3600,
|
|
204
|
+
refresh_token: 'refresh-token-123',
|
|
205
|
+
scope: 'openid email profile',
|
|
206
|
+
id_token: 'id-token-123',
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
const strategy = createOAuth2Strategy({
|
|
210
|
+
provider: 'google',
|
|
211
|
+
clientId: 'client-id',
|
|
212
|
+
clientSecret: 'client-secret',
|
|
213
|
+
redirectUri: 'https://example.com/callback',
|
|
214
|
+
});
|
|
215
|
+
const tokens = await strategy.exchangeCode('auth-code-123');
|
|
216
|
+
expect(tokens).toEqual({
|
|
217
|
+
accessToken: 'access-token-123',
|
|
218
|
+
tokenType: 'Bearer',
|
|
219
|
+
expiresIn: 3600,
|
|
220
|
+
refreshToken: 'refresh-token-123',
|
|
221
|
+
scope: 'openid email profile',
|
|
222
|
+
idToken: 'id-token-123',
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
it('should include required parameters in token request', async () => {
|
|
226
|
+
mockFetch.mockResolvedValueOnce({
|
|
227
|
+
ok: true,
|
|
228
|
+
json: async () => ({
|
|
229
|
+
access_token: 'access-token',
|
|
230
|
+
token_type: 'Bearer',
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
const strategy = createOAuth2Strategy({
|
|
234
|
+
provider: 'google',
|
|
235
|
+
clientId: 'client-id',
|
|
236
|
+
clientSecret: 'client-secret',
|
|
237
|
+
redirectUri: 'https://example.com/callback',
|
|
238
|
+
});
|
|
239
|
+
await strategy.exchangeCode('auth-code');
|
|
240
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
241
|
+
method: 'POST',
|
|
242
|
+
body: expect.stringContaining('grant_type=authorization_code'),
|
|
243
|
+
}));
|
|
244
|
+
});
|
|
245
|
+
it('should throw on token exchange failure', async () => {
|
|
246
|
+
mockFetch.mockResolvedValueOnce({
|
|
247
|
+
ok: false,
|
|
248
|
+
status: 400,
|
|
249
|
+
text: async () => '{"error": "invalid_grant"}',
|
|
250
|
+
});
|
|
251
|
+
const strategy = createOAuth2Strategy({
|
|
252
|
+
provider: 'google',
|
|
253
|
+
clientId: 'client-id',
|
|
254
|
+
clientSecret: 'client-secret',
|
|
255
|
+
redirectUri: 'https://example.com/callback',
|
|
256
|
+
});
|
|
257
|
+
await expect(strategy.exchangeCode('invalid-code')).rejects.toThrow('Token exchange failed');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
describe('refreshToken', () => {
|
|
261
|
+
it('should refresh access token', async () => {
|
|
262
|
+
mockFetch.mockResolvedValueOnce({
|
|
263
|
+
ok: true,
|
|
264
|
+
json: async () => ({
|
|
265
|
+
access_token: 'new-access-token',
|
|
266
|
+
token_type: 'Bearer',
|
|
267
|
+
expires_in: 3600,
|
|
268
|
+
}),
|
|
269
|
+
});
|
|
270
|
+
const strategy = createOAuth2Strategy({
|
|
271
|
+
provider: 'google',
|
|
272
|
+
clientId: 'client-id',
|
|
273
|
+
clientSecret: 'client-secret',
|
|
274
|
+
redirectUri: 'https://example.com/callback',
|
|
275
|
+
});
|
|
276
|
+
const tokens = await strategy.refreshToken('refresh-token-123');
|
|
277
|
+
expect(tokens.accessToken).toBe('new-access-token');
|
|
278
|
+
expect(tokens.refreshToken).toBe('refresh-token-123'); // Original preserved
|
|
279
|
+
});
|
|
280
|
+
it('should use new refresh token if provided', async () => {
|
|
281
|
+
mockFetch.mockResolvedValueOnce({
|
|
282
|
+
ok: true,
|
|
283
|
+
json: async () => ({
|
|
284
|
+
access_token: 'new-access-token',
|
|
285
|
+
token_type: 'Bearer',
|
|
286
|
+
refresh_token: 'new-refresh-token',
|
|
287
|
+
}),
|
|
288
|
+
});
|
|
289
|
+
const strategy = createOAuth2Strategy({
|
|
290
|
+
provider: 'google',
|
|
291
|
+
clientId: 'client-id',
|
|
292
|
+
clientSecret: 'client-secret',
|
|
293
|
+
redirectUri: 'https://example.com/callback',
|
|
294
|
+
});
|
|
295
|
+
const tokens = await strategy.refreshToken('old-refresh-token');
|
|
296
|
+
expect(tokens.refreshToken).toBe('new-refresh-token');
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
describe('getUserInfo', () => {
|
|
300
|
+
it('should fetch user info', async () => {
|
|
301
|
+
mockFetch.mockResolvedValueOnce({
|
|
302
|
+
ok: true,
|
|
303
|
+
json: async () => ({
|
|
304
|
+
sub: 'user-123',
|
|
305
|
+
email: 'user@example.com',
|
|
306
|
+
name: 'Test User',
|
|
307
|
+
picture: 'https://example.com/avatar.jpg',
|
|
308
|
+
}),
|
|
309
|
+
});
|
|
310
|
+
const strategy = createOAuth2Strategy({
|
|
311
|
+
provider: 'google',
|
|
312
|
+
clientId: 'client-id',
|
|
313
|
+
clientSecret: 'client-secret',
|
|
314
|
+
redirectUri: 'https://example.com/callback',
|
|
315
|
+
});
|
|
316
|
+
const userInfo = await strategy.getUserInfo('access-token');
|
|
317
|
+
expect(userInfo).toEqual({
|
|
318
|
+
sub: 'user-123',
|
|
319
|
+
email: 'user@example.com',
|
|
320
|
+
name: 'Test User',
|
|
321
|
+
picture: 'https://example.com/avatar.jpg',
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
it('should normalize GitHub response', async () => {
|
|
325
|
+
mockFetch.mockResolvedValueOnce({
|
|
326
|
+
ok: true,
|
|
327
|
+
json: async () => ({
|
|
328
|
+
id: 12345,
|
|
329
|
+
login: 'testuser',
|
|
330
|
+
name: 'Test User',
|
|
331
|
+
email: 'test@github.com',
|
|
332
|
+
avatar_url: 'https://github.com/avatar.jpg',
|
|
333
|
+
}),
|
|
334
|
+
});
|
|
335
|
+
const strategy = createOAuth2Strategy({
|
|
336
|
+
provider: 'github',
|
|
337
|
+
clientId: 'client-id',
|
|
338
|
+
clientSecret: 'client-secret',
|
|
339
|
+
redirectUri: 'https://example.com/callback',
|
|
340
|
+
});
|
|
341
|
+
const userInfo = await strategy.getUserInfo('access-token');
|
|
342
|
+
expect(userInfo.sub).toBe('12345'); // Normalized from id
|
|
343
|
+
expect(userInfo.name).toBe('Test User');
|
|
344
|
+
expect(userInfo.email).toBe('test@github.com');
|
|
345
|
+
expect(userInfo.picture).toBe('https://github.com/avatar.jpg');
|
|
346
|
+
});
|
|
347
|
+
it('should throw when userInfoUrl not configured', async () => {
|
|
348
|
+
const strategy = createOAuth2Strategy({
|
|
349
|
+
provider: 'custom',
|
|
350
|
+
clientId: 'client-id',
|
|
351
|
+
clientSecret: 'client-secret',
|
|
352
|
+
redirectUri: 'https://example.com/callback',
|
|
353
|
+
authorizationUrl: 'https://auth.example.com/authorize',
|
|
354
|
+
tokenUrl: 'https://auth.example.com/token',
|
|
355
|
+
// No userInfoUrl
|
|
356
|
+
});
|
|
357
|
+
await expect(strategy.getUserInfo('access-token')).rejects.toThrow('UserInfo endpoint not configured');
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
describe('revokeToken', () => {
|
|
361
|
+
it('should revoke token when revocationUrl configured', async () => {
|
|
362
|
+
mockFetch.mockResolvedValueOnce({ ok: true });
|
|
363
|
+
const strategy = createOAuth2Strategy({
|
|
364
|
+
provider: 'custom',
|
|
365
|
+
clientId: 'client-id',
|
|
366
|
+
clientSecret: 'client-secret',
|
|
367
|
+
redirectUri: 'https://example.com/callback',
|
|
368
|
+
authorizationUrl: 'https://auth.example.com/authorize',
|
|
369
|
+
tokenUrl: 'https://auth.example.com/token',
|
|
370
|
+
revocationUrl: 'https://auth.example.com/revoke',
|
|
371
|
+
});
|
|
372
|
+
await expect(strategy.revokeToken('access-token')).resolves.toBeUndefined();
|
|
373
|
+
expect(mockFetch).toHaveBeenCalledWith('https://auth.example.com/revoke', expect.objectContaining({
|
|
374
|
+
method: 'POST',
|
|
375
|
+
}));
|
|
376
|
+
});
|
|
377
|
+
it('should not have revokeToken when revocationUrl not configured', () => {
|
|
378
|
+
const strategy = createOAuth2Strategy({
|
|
379
|
+
provider: 'custom',
|
|
380
|
+
clientId: 'client-id',
|
|
381
|
+
clientSecret: 'client-secret',
|
|
382
|
+
redirectUri: 'https://example.com/callback',
|
|
383
|
+
authorizationUrl: 'https://auth.example.com/authorize',
|
|
384
|
+
tokenUrl: 'https://auth.example.com/token',
|
|
385
|
+
});
|
|
386
|
+
expect(strategy.revokeToken).toBeUndefined();
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
describe('OIDC Strategy', () => {
|
|
391
|
+
beforeEach(() => {
|
|
392
|
+
vi.clearAllMocks();
|
|
393
|
+
clearDiscoveryCache();
|
|
394
|
+
});
|
|
395
|
+
afterEach(() => {
|
|
396
|
+
vi.restoreAllMocks();
|
|
397
|
+
});
|
|
398
|
+
describe('createOIDCStrategy', () => {
|
|
399
|
+
it('should create strategy with lazy initialization', () => {
|
|
400
|
+
// No fetch needed during creation - discovery is lazy
|
|
401
|
+
const strategy = createOIDCStrategy({
|
|
402
|
+
issuer: 'https://auth.example.com',
|
|
403
|
+
clientId: 'client-id',
|
|
404
|
+
clientSecret: 'client-secret',
|
|
405
|
+
redirectUri: 'https://example.com/callback',
|
|
406
|
+
});
|
|
407
|
+
expect(strategy.name).toBe('oidc');
|
|
408
|
+
expect(strategy.authenticate).toBeDefined();
|
|
409
|
+
expect(strategy.getAuthorizationUrl).toBeDefined();
|
|
410
|
+
expect(strategy.exchangeCode).toBeDefined();
|
|
411
|
+
expect(strategy.validateIdToken).toBeDefined();
|
|
412
|
+
});
|
|
413
|
+
it('should expose discovery document after initialization', async () => {
|
|
414
|
+
// Discovery is fetched lazily when methods are called
|
|
415
|
+
// We can verify the strategy has the right interface
|
|
416
|
+
const strategy = createOIDCStrategy({
|
|
417
|
+
issuer: 'https://auth.example.com',
|
|
418
|
+
clientId: 'client-id',
|
|
419
|
+
clientSecret: 'client-secret',
|
|
420
|
+
redirectUri: 'https://example.com/callback',
|
|
421
|
+
});
|
|
422
|
+
// Before initialization, discovery is null
|
|
423
|
+
expect(strategy.discovery).toBeNull();
|
|
424
|
+
});
|
|
425
|
+
it('should cache discovery document', async () => {
|
|
426
|
+
mockFetch.mockResolvedValue({
|
|
427
|
+
ok: true,
|
|
428
|
+
json: async () => ({
|
|
429
|
+
issuer: 'https://auth.example.com',
|
|
430
|
+
authorization_endpoint: 'https://auth.example.com/authorize',
|
|
431
|
+
token_endpoint: 'https://auth.example.com/token',
|
|
432
|
+
userinfo_endpoint: 'https://auth.example.com/userinfo',
|
|
433
|
+
jwks_uri: 'https://auth.example.com/.well-known/jwks.json',
|
|
434
|
+
}),
|
|
435
|
+
});
|
|
436
|
+
const strategy1 = createOIDCStrategy({
|
|
437
|
+
issuer: 'https://auth.example.com',
|
|
438
|
+
clientId: 'client-id',
|
|
439
|
+
clientSecret: 'client-secret',
|
|
440
|
+
redirectUri: 'https://example.com/callback',
|
|
441
|
+
});
|
|
442
|
+
const strategy2 = createOIDCStrategy({
|
|
443
|
+
issuer: 'https://auth.example.com',
|
|
444
|
+
clientId: 'client-id-2',
|
|
445
|
+
clientSecret: 'client-secret-2',
|
|
446
|
+
redirectUri: 'https://example.com/callback2',
|
|
447
|
+
});
|
|
448
|
+
const envelope = createTestEnvelope();
|
|
449
|
+
const ctx = createTestContext();
|
|
450
|
+
// Trigger discovery for both
|
|
451
|
+
await strategy1.authenticate(envelope, ctx);
|
|
452
|
+
await strategy2.authenticate(envelope, ctx);
|
|
453
|
+
// Discovery should only be fetched once (cached)
|
|
454
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
455
|
+
});
|
|
456
|
+
it('should clear discovery cache', async () => {
|
|
457
|
+
mockFetch.mockResolvedValue({
|
|
458
|
+
ok: true,
|
|
459
|
+
json: async () => ({
|
|
460
|
+
issuer: 'https://auth.example.com',
|
|
461
|
+
authorization_endpoint: 'https://auth.example.com/authorize',
|
|
462
|
+
token_endpoint: 'https://auth.example.com/token',
|
|
463
|
+
userinfo_endpoint: 'https://auth.example.com/userinfo',
|
|
464
|
+
jwks_uri: 'https://auth.example.com/.well-known/jwks.json',
|
|
465
|
+
}),
|
|
466
|
+
});
|
|
467
|
+
const strategy1 = createOIDCStrategy({
|
|
468
|
+
issuer: 'https://auth.example.com',
|
|
469
|
+
clientId: 'client-id',
|
|
470
|
+
clientSecret: 'client-secret',
|
|
471
|
+
redirectUri: 'https://example.com/callback',
|
|
472
|
+
});
|
|
473
|
+
const envelope = createTestEnvelope();
|
|
474
|
+
const ctx = createTestContext();
|
|
475
|
+
// First discovery
|
|
476
|
+
await strategy1.authenticate(envelope, ctx);
|
|
477
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
478
|
+
// Clear cache
|
|
479
|
+
clearDiscoveryCache();
|
|
480
|
+
// Create new strategy and trigger discovery
|
|
481
|
+
const strategy2 = createOIDCStrategy({
|
|
482
|
+
issuer: 'https://auth.example.com',
|
|
483
|
+
clientId: 'client-id',
|
|
484
|
+
clientSecret: 'client-secret',
|
|
485
|
+
redirectUri: 'https://example.com/callback',
|
|
486
|
+
});
|
|
487
|
+
await strategy2.authenticate(envelope, ctx);
|
|
488
|
+
// Should fetch twice (cache was cleared)
|
|
489
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
490
|
+
});
|
|
491
|
+
it('should throw on discovery failure', async () => {
|
|
492
|
+
mockFetch.mockResolvedValueOnce({
|
|
493
|
+
ok: false,
|
|
494
|
+
status: 404,
|
|
495
|
+
});
|
|
496
|
+
const strategy = createOIDCStrategy({
|
|
497
|
+
issuer: 'https://invalid.example.com',
|
|
498
|
+
clientId: 'client-id',
|
|
499
|
+
clientSecret: 'client-secret',
|
|
500
|
+
redirectUri: 'https://example.com/callback',
|
|
501
|
+
});
|
|
502
|
+
const envelope = createTestEnvelope();
|
|
503
|
+
const ctx = createTestContext();
|
|
504
|
+
// Discovery failure happens on first authenticate
|
|
505
|
+
await expect(strategy.authenticate(envelope, ctx)).rejects.toThrow('OIDC discovery failed');
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
describe('validateIdToken', () => {
|
|
509
|
+
it('should decode and validate ID token claims', async () => {
|
|
510
|
+
mockFetch.mockResolvedValueOnce({
|
|
511
|
+
ok: true,
|
|
512
|
+
json: async () => ({
|
|
513
|
+
issuer: 'https://auth.example.com',
|
|
514
|
+
authorization_endpoint: 'https://auth.example.com/authorize',
|
|
515
|
+
token_endpoint: 'https://auth.example.com/token',
|
|
516
|
+
userinfo_endpoint: 'https://auth.example.com/userinfo',
|
|
517
|
+
jwks_uri: 'https://auth.example.com/.well-known/jwks.json',
|
|
518
|
+
}),
|
|
519
|
+
});
|
|
520
|
+
const strategy = createOIDCStrategy({
|
|
521
|
+
issuer: 'https://auth.example.com',
|
|
522
|
+
clientId: 'client-id',
|
|
523
|
+
clientSecret: 'client-secret',
|
|
524
|
+
redirectUri: 'https://example.com/callback',
|
|
525
|
+
});
|
|
526
|
+
// Create a mock JWT (header.payload.signature)
|
|
527
|
+
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
|
|
528
|
+
const payload = Buffer.from(JSON.stringify({
|
|
529
|
+
iss: 'https://auth.example.com',
|
|
530
|
+
sub: 'user-123',
|
|
531
|
+
aud: 'client-id',
|
|
532
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
533
|
+
iat: Math.floor(Date.now() / 1000),
|
|
534
|
+
email: 'user@example.com',
|
|
535
|
+
name: 'Test User',
|
|
536
|
+
})).toString('base64url');
|
|
537
|
+
const signature = 'fake-signature';
|
|
538
|
+
const idToken = `${header}.${payload}.${signature}`;
|
|
539
|
+
// validateIdToken is the method, not getIdTokenClaims
|
|
540
|
+
const claims = await strategy.validateIdToken(idToken);
|
|
541
|
+
expect(claims.sub).toBe('user-123');
|
|
542
|
+
expect(claims.email).toBe('user@example.com');
|
|
543
|
+
expect(claims.name).toBe('Test User');
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
describe('Provider Presets', () => {
|
|
548
|
+
it('should have all expected providers', () => {
|
|
549
|
+
expect(OAuth2Providers.google).toBeDefined();
|
|
550
|
+
expect(OAuth2Providers.github).toBeDefined();
|
|
551
|
+
expect(OAuth2Providers.microsoft).toBeDefined();
|
|
552
|
+
expect(OAuth2Providers.apple).toBeDefined();
|
|
553
|
+
expect(OAuth2Providers.facebook).toBeDefined();
|
|
554
|
+
});
|
|
555
|
+
it('should have correct Google URLs', () => {
|
|
556
|
+
expect(OAuth2Providers.google.authorizationUrl).toBe('https://accounts.google.com/o/oauth2/v2/auth');
|
|
557
|
+
expect(OAuth2Providers.google.tokenUrl).toBe('https://oauth2.googleapis.com/token');
|
|
558
|
+
expect(OAuth2Providers.google.userInfoUrl).toBe('https://openidconnect.googleapis.com/v1/userinfo');
|
|
559
|
+
});
|
|
560
|
+
it('should have correct GitHub URLs', () => {
|
|
561
|
+
expect(OAuth2Providers.github.authorizationUrl).toBe('https://github.com/login/oauth/authorize');
|
|
562
|
+
expect(OAuth2Providers.github.tokenUrl).toBe('https://github.com/login/oauth/access_token');
|
|
563
|
+
expect(OAuth2Providers.github.userInfoUrl).toBe('https://api.github.com/user');
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
describe('Provider Shortcut Functions', () => {
|
|
567
|
+
it('should create Google strategy', () => {
|
|
568
|
+
const strategy = createGoogleOAuth2Strategy({
|
|
569
|
+
clientId: 'client-id',
|
|
570
|
+
clientSecret: 'client-secret',
|
|
571
|
+
redirectUri: 'https://example.com/callback',
|
|
572
|
+
});
|
|
573
|
+
expect(strategy.name).toBe('oauth2:google');
|
|
574
|
+
});
|
|
575
|
+
it('should create GitHub strategy', () => {
|
|
576
|
+
const strategy = createGitHubOAuth2Strategy({
|
|
577
|
+
clientId: 'client-id',
|
|
578
|
+
clientSecret: 'client-secret',
|
|
579
|
+
redirectUri: 'https://example.com/callback',
|
|
580
|
+
});
|
|
581
|
+
expect(strategy.name).toBe('oauth2:github');
|
|
582
|
+
});
|
|
583
|
+
it('should create Microsoft strategy with tenant', () => {
|
|
584
|
+
const strategy = createMicrosoftOAuth2Strategy({
|
|
585
|
+
clientId: 'client-id',
|
|
586
|
+
clientSecret: 'client-secret',
|
|
587
|
+
redirectUri: 'https://example.com/callback',
|
|
588
|
+
tenant: 'my-tenant',
|
|
589
|
+
});
|
|
590
|
+
expect(strategy.name).toBe('oauth2:custom'); // Uses custom provider internally
|
|
591
|
+
});
|
|
592
|
+
it('should create Apple strategy', () => {
|
|
593
|
+
const strategy = createAppleOAuth2Strategy({
|
|
594
|
+
clientId: 'client-id',
|
|
595
|
+
clientSecret: 'client-secret',
|
|
596
|
+
redirectUri: 'https://example.com/callback',
|
|
597
|
+
});
|
|
598
|
+
expect(strategy.name).toBe('oauth2:apple');
|
|
599
|
+
});
|
|
600
|
+
it('should create Facebook strategy', () => {
|
|
601
|
+
const strategy = createFacebookOAuth2Strategy({
|
|
602
|
+
clientId: 'client-id',
|
|
603
|
+
clientSecret: 'client-secret',
|
|
604
|
+
redirectUri: 'https://example.com/callback',
|
|
605
|
+
});
|
|
606
|
+
expect(strategy.name).toBe('oauth2:facebook');
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
describe('Utility Functions', () => {
|
|
610
|
+
describe('generateState', () => {
|
|
611
|
+
it('should generate URL-safe random string', () => {
|
|
612
|
+
const state = generateState();
|
|
613
|
+
expect(state).toBeDefined();
|
|
614
|
+
expect(typeof state).toBe('string');
|
|
615
|
+
expect(state.length).toBeGreaterThan(0);
|
|
616
|
+
// Base64url should not contain +, /, or =
|
|
617
|
+
expect(state).not.toMatch(/[+/=]/);
|
|
618
|
+
});
|
|
619
|
+
it('should generate different values each time', () => {
|
|
620
|
+
const state1 = generateState();
|
|
621
|
+
const state2 = generateState();
|
|
622
|
+
expect(state1).not.toBe(state2);
|
|
623
|
+
});
|
|
624
|
+
it('should respect length parameter', () => {
|
|
625
|
+
const state16 = generateState(16);
|
|
626
|
+
const state64 = generateState(64);
|
|
627
|
+
// Base64url encoding produces ~4/3 chars per byte
|
|
628
|
+
expect(state16.length).toBeLessThan(state64.length);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
describe('generateNonce', () => {
|
|
632
|
+
it('should generate URL-safe random string', () => {
|
|
633
|
+
const nonce = generateNonce();
|
|
634
|
+
expect(nonce).toBeDefined();
|
|
635
|
+
expect(typeof nonce).toBe('string');
|
|
636
|
+
expect(nonce.length).toBeGreaterThan(0);
|
|
637
|
+
// Base64url should not contain +, /, or =
|
|
638
|
+
expect(nonce).not.toMatch(/[+/=]/);
|
|
639
|
+
});
|
|
640
|
+
it('should generate different values each time', () => {
|
|
641
|
+
const nonce1 = generateNonce();
|
|
642
|
+
const nonce2 = generateNonce();
|
|
643
|
+
expect(nonce1).not.toBe(nonce2);
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
describe('Client Credentials in Body', () => {
|
|
648
|
+
beforeEach(() => {
|
|
649
|
+
vi.clearAllMocks();
|
|
650
|
+
});
|
|
651
|
+
it('should send credentials in body when clientCredentialsInBody is true', async () => {
|
|
652
|
+
mockFetch.mockResolvedValueOnce({
|
|
653
|
+
ok: true,
|
|
654
|
+
json: async () => ({
|
|
655
|
+
access_token: 'access-token',
|
|
656
|
+
token_type: 'Bearer',
|
|
657
|
+
}),
|
|
658
|
+
});
|
|
659
|
+
const strategy = createOAuth2Strategy({
|
|
660
|
+
provider: 'custom',
|
|
661
|
+
clientId: 'client-id',
|
|
662
|
+
clientSecret: 'client-secret',
|
|
663
|
+
redirectUri: 'https://example.com/callback',
|
|
664
|
+
authorizationUrl: 'https://auth.example.com/authorize',
|
|
665
|
+
tokenUrl: 'https://auth.example.com/token',
|
|
666
|
+
clientCredentialsInBody: true,
|
|
667
|
+
});
|
|
668
|
+
await strategy.exchangeCode('auth-code');
|
|
669
|
+
const [, requestInit] = mockFetch.mock.calls[0];
|
|
670
|
+
const body = requestInit.body;
|
|
671
|
+
expect(body).toContain('client_id=client-id');
|
|
672
|
+
expect(body).toContain('client_secret=client-secret');
|
|
673
|
+
expect(requestInit.headers).not.toHaveProperty('Authorization');
|
|
674
|
+
});
|
|
675
|
+
it('should send credentials in Authorization header by default', async () => {
|
|
676
|
+
mockFetch.mockResolvedValueOnce({
|
|
677
|
+
ok: true,
|
|
678
|
+
json: async () => ({
|
|
679
|
+
access_token: 'access-token',
|
|
680
|
+
token_type: 'Bearer',
|
|
681
|
+
}),
|
|
682
|
+
});
|
|
683
|
+
const strategy = createOAuth2Strategy({
|
|
684
|
+
provider: 'custom',
|
|
685
|
+
clientId: 'client-id',
|
|
686
|
+
clientSecret: 'client-secret',
|
|
687
|
+
redirectUri: 'https://example.com/callback',
|
|
688
|
+
authorizationUrl: 'https://auth.example.com/authorize',
|
|
689
|
+
tokenUrl: 'https://auth.example.com/token',
|
|
690
|
+
});
|
|
691
|
+
await strategy.exchangeCode('auth-code');
|
|
692
|
+
const [, requestInit] = mockFetch.mock.calls[0];
|
|
693
|
+
const headers = requestInit.headers;
|
|
694
|
+
expect(headers['Authorization']).toMatch(/^Basic /);
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
describe('Strategy Configuration', () => {
|
|
698
|
+
beforeEach(() => {
|
|
699
|
+
vi.clearAllMocks();
|
|
700
|
+
});
|
|
701
|
+
it('should respect timeout configuration', () => {
|
|
702
|
+
// The strategy accepts a timeout option
|
|
703
|
+
const strategy = createOAuth2Strategy({
|
|
704
|
+
provider: 'google',
|
|
705
|
+
clientId: 'client-id',
|
|
706
|
+
clientSecret: 'client-secret',
|
|
707
|
+
redirectUri: 'https://example.com/callback',
|
|
708
|
+
timeout: 1000,
|
|
709
|
+
});
|
|
710
|
+
// Strategy is created successfully with timeout
|
|
711
|
+
expect(strategy.name).toBe('oauth2:google');
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
//# sourceMappingURL=oauth2.int.test.js.map
|