Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit db820b7

Browse files
committedJun 16, 2025··
refactor: use middleware to specify CORS policy for constrained anonymous API
1 parent 52c5cf2 commit db820b7

File tree

6 files changed

+390
-73
lines changed

6 files changed

+390
-73
lines changed
 
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { GlobalValues } from '@logto/shared';
2+
import { createMockUtils } from '@logto/shared/esm';
3+
import type { RequestMethod } from 'node-mocks-http';
4+
5+
import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js';
6+
7+
const { jest } = import.meta;
8+
9+
const { mockEsmWithActual } = createMockUtils(jest);
10+
11+
await mockEsmWithActual('#src/env-set/index.js', () => ({
12+
EnvSet: {
13+
get values() {
14+
return new GlobalValues();
15+
},
16+
},
17+
}));
18+
19+
const {
20+
default: koaAnonymousCors,
21+
localDevelopmentOrigins,
22+
productionDomainSuffixes,
23+
} = await import('./koa-anonymous-cors.js');
24+
25+
// eslint-disable-next-line @typescript-eslint/no-empty-function
26+
const noop = async () => {};
27+
28+
const mockContext = (method: RequestMethod, url: string, origin?: string) => {
29+
const ctx = createMockContext({
30+
method,
31+
headers: { origin: origin ?? new URL(url).origin },
32+
});
33+
34+
// Mock the path property
35+
ctx.request.path = '/anonymous-api';
36+
37+
const setSpy = jest.spyOn(ctx, 'set');
38+
39+
return [ctx, setSpy] as const;
40+
};
41+
42+
// Helper function to check if CORS headers are set properly
43+
// Following the pattern from koa-cors.test.ts - only check headers that are actually set by @koa/cors
44+
const expectCorsHeaders = (setSpy: jest.SpyInstance, origin: string, shouldHaveHeaders = true) => {
45+
if (shouldHaveHeaders && origin) {
46+
expect(setSpy).toHaveBeenCalledWith('Access-Control-Allow-Origin', origin);
47+
// @koa/cors sets these headers automatically for actual CORS requests
48+
// but we should check if they are called at all during the middleware execution
49+
expect(setSpy).toHaveBeenCalled();
50+
} else {
51+
expect(setSpy).not.toHaveBeenCalledWith(
52+
'Access-Control-Allow-Origin',
53+
expect.stringMatching('.*')
54+
);
55+
}
56+
};
57+
58+
describe('koaAnonymousCors() middleware', () => {
59+
const envBackup = Object.freeze({ ...process.env });
60+
61+
afterEach(() => {
62+
process.env = { ...envBackup };
63+
});
64+
65+
describe('in development environment', () => {
66+
beforeEach(() => {
67+
process.env.NODE_ENV = 'development';
68+
});
69+
70+
it('should allow localhost origins in development', async () => {
71+
const allowedMethods = 'GET, OPTIONS';
72+
const run = koaAnonymousCors(allowedMethods);
73+
74+
const [ctx1, setSpy1] = mockContext('GET', 'http://localhost:3000');
75+
await run(ctx1, noop);
76+
expectCorsHeaders(setSpy1, 'http://localhost:3000');
77+
78+
const [ctx2, setSpy2] = mockContext('GET', 'http://127.0.0.1:8080');
79+
await run(ctx2, noop);
80+
expectCorsHeaders(setSpy2, 'http://127.0.0.1:8080');
81+
});
82+
83+
it('should allow .local domains in development', async () => {
84+
const allowedMethods = 'POST, OPTIONS';
85+
const run = koaAnonymousCors(allowedMethods);
86+
87+
const [ctx, setSpy] = mockContext('POST', 'http://test.local:3000');
88+
await run(ctx, noop);
89+
expectCorsHeaders(setSpy, 'http://test.local:3000');
90+
});
91+
92+
it('should allow host.docker.internal in development', async () => {
93+
const allowedMethods = 'GET, POST, OPTIONS';
94+
const run = koaAnonymousCors(allowedMethods);
95+
96+
const [ctx, setSpy] = mockContext('GET', 'http://host.docker.internal:3000');
97+
await run(ctx, noop);
98+
expectCorsHeaders(setSpy, 'http://host.docker.internal:3000');
99+
});
100+
101+
it('should reject non-allowed origins in development', async () => {
102+
const allowedMethods = 'GET, OPTIONS';
103+
const run = koaAnonymousCors(allowedMethods);
104+
105+
const [ctx] = mockContext('GET', 'https://malicious.com');
106+
107+
await expect(run(ctx, noop)).rejects.toThrow();
108+
});
109+
});
110+
111+
describe('in production environment', () => {
112+
beforeEach(() => {
113+
process.env.NODE_ENV = 'production';
114+
});
115+
116+
it('should allow *.logto.io domains in production', async () => {
117+
const allowedMethods = 'GET, OPTIONS';
118+
const run = koaAnonymousCors(allowedMethods);
119+
120+
const [ctx1, setSpy1] = mockContext('GET', 'https://app.logto.io');
121+
await run(ctx1, noop);
122+
expectCorsHeaders(setSpy1, 'https://app.logto.io');
123+
124+
const [ctx2, setSpy2] = mockContext('GET', 'https://test.logto.io');
125+
await run(ctx2, noop);
126+
expectCorsHeaders(setSpy2, 'https://test.logto.io');
127+
});
128+
129+
it('should allow *.logto.dev domains in production', async () => {
130+
const allowedMethods = 'POST, OPTIONS';
131+
const run = koaAnonymousCors(allowedMethods);
132+
133+
const [ctx, setSpy] = mockContext('POST', 'https://staging.logto.dev');
134+
await run(ctx, noop);
135+
expectCorsHeaders(setSpy, 'https://staging.logto.dev');
136+
});
137+
138+
it('should reject localhost in production', async () => {
139+
const allowedMethods = 'GET, OPTIONS';
140+
const run = koaAnonymousCors(allowedMethods);
141+
142+
const [ctx] = mockContext('GET', 'http://localhost:3000');
143+
144+
await expect(run(ctx, noop)).rejects.toThrow();
145+
});
146+
147+
it('should reject non-allowed domains in production', async () => {
148+
const allowedMethods = 'GET, OPTIONS';
149+
const run = koaAnonymousCors(allowedMethods);
150+
151+
const [ctx] = mockContext('GET', 'https://evil.com');
152+
153+
await expect(run(ctx, noop)).rejects.toThrow();
154+
});
155+
});
156+
157+
describe('in integration test environment', () => {
158+
beforeEach(() => {
159+
process.env.NODE_ENV = 'production';
160+
process.env.INTEGRATION_TEST = 'true';
161+
});
162+
163+
it('should allow localhost in integration test even in production mode', async () => {
164+
const allowedMethods = 'GET, OPTIONS';
165+
const run = koaAnonymousCors(allowedMethods);
166+
167+
const [ctx, setSpy] = mockContext('GET', 'http://localhost:3000');
168+
await run(ctx, noop);
169+
expectCorsHeaders(setSpy, 'http://localhost:3000');
170+
});
171+
});
172+
173+
describe('OPTIONS request handling', () => {
174+
it('should handle OPTIONS requests automatically with @koa/cors', async () => {
175+
const allowedMethods = 'GET, POST, OPTIONS';
176+
const run = koaAnonymousCors(allowedMethods);
177+
178+
const [ctx] = mockContext('OPTIONS', 'http://localhost:3000');
179+
180+
// @koa/cors handles OPTIONS requests automatically
181+
// We just verify that the middleware runs without throwing errors
182+
await expect(run(ctx, noop)).resolves.not.toThrow();
183+
});
184+
});
185+
186+
describe('exported constants', () => {
187+
it('should export localDevelopmentOrigins', () => {
188+
expect(localDevelopmentOrigins).toEqual([
189+
'localhost',
190+
'127.0.0.1',
191+
'0.0.0.0',
192+
'[::1]',
193+
'.local',
194+
'host.docker.internal',
195+
]);
196+
});
197+
198+
it('should export productionDomainSuffixes', () => {
199+
expect(productionDomainSuffixes).toEqual([
200+
'.logto.io',
201+
'.logto.dev',
202+
'.logto-docs.pages.dev',
203+
]);
204+
});
205+
});
206+
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import cors from '@koa/cors';
2+
import { ConsoleLog } from '@logto/shared';
3+
import chalk from 'chalk';
4+
import type { MiddlewareType } from 'koa';
5+
6+
import { EnvSet } from '#src/env-set/index.js';
7+
import RequestError from '#src/errors/RequestError/index.js';
8+
9+
/**
10+
* @fileoverview Anonymous CORS middleware for Logto APIs
11+
*
12+
* This middleware provides strict CORS (Cross-Origin Resource Sharing) control for anonymous APIs
13+
* that need to be accessible from specific Logto-related domains only. It implements a whitelist-based
14+
* approach to ensure only trusted origins can access sensitive anonymous endpoints.
15+
*
16+
* **Use Cases:**
17+
* - Other anonymous authentication endpoints
18+
* - Public APIs that should only be accessible from Logto-hosted websites
19+
* - Any anonymous endpoint requiring strict origin validation
20+
*
21+
* **Security Features:**
22+
* - Strict domain whitelist enforcement
23+
* - Development vs production environment handling
24+
* - Integration test environment support
25+
* - Automatic OPTIONS preflight request handling
26+
*
27+
* @see {@link koaCors} for general-purpose CORS handling with URL Sets
28+
*/
29+
30+
const consoleLog = new ConsoleLog(chalk.magenta('anonymous-cors'));
31+
32+
// List of allowed local development origins
33+
const localDevelopmentOrigins = [
34+
'localhost', // Localhost with any port
35+
'127.0.0.1', // IPv4 loopback
36+
'0.0.0.0', // All interfaces
37+
'[::1]', // IPv6 loopback
38+
'.local', // MDNS domains (especially for macOS)
39+
'host.docker.internal', // Docker host from container
40+
];
41+
42+
// List of allowed production domain suffixes
43+
const productionDomainSuffixes = [
44+
'.logto.io', // Production domain
45+
'.logto.dev', // Development domain
46+
...(EnvSet.values.isDevFeaturesEnabled ? ['.logto-docs.pages.dev'] : []), // Logto Docs CI preview domain, for testing purposes
47+
];
48+
49+
/**
50+
* Anonymous CORS middleware factory
51+
*
52+
* Creates a Koa middleware that enforces strict CORS policies for anonymous APIs.
53+
* This middleware is designed for endpoints that need to be accessible without authentication
54+
* but should be restricted to specific trusted domains only.
55+
*
56+
* **Features:**
57+
* - Automatic OPTIONS preflight request handling with 204 status
58+
* - Strict origin validation against predefined whitelists
59+
* - Environment-aware behavior (development vs production)
60+
* - Detailed error responses for unauthorized origins
61+
*
62+
* **Usage Example:**
63+
* ```typescript
64+
* router.get('/anonymous-api', koaAnonymousCors('GET, OPTIONS'), async (ctx) => {
65+
* // Your anonymous API logic here
66+
* });
67+
* ```
68+
*
69+
* @param allowedMethods - Comma-separated string of allowed HTTP methods (e.g., 'GET, POST, OPTIONS')
70+
* @returns Koa middleware function for handling anonymous CORS
71+
*
72+
* @throws {RequestError} 403 Forbidden when request origin is not in the whitelist
73+
*/
74+
export default function koaAnonymousCors<StateT, ContextT, ResponseBodyT>(
75+
allowedMethods: string
76+
): MiddlewareType<StateT, ContextT, ResponseBodyT> {
77+
return cors({
78+
origin: (ctx) => {
79+
const origin = ctx.get('origin');
80+
const { isProduction, isIntegrationTest } = EnvSet.values;
81+
82+
// Only show debug logs in non-production environments
83+
if (!isProduction) {
84+
consoleLog.info(`origin: ${origin}`);
85+
consoleLog.info(`isIntegrationTest: ${isIntegrationTest}`);
86+
}
87+
88+
// Allow local development origins
89+
if (
90+
(!isProduction || isIntegrationTest) &&
91+
localDevelopmentOrigins.some((item) => origin.includes(item))
92+
) {
93+
return origin;
94+
}
95+
96+
// In production, only allow *.logto.io or *.logto.dev domains to access
97+
if (isProduction && productionDomainSuffixes.some((suffix) => origin.endsWith(suffix))) {
98+
return origin;
99+
}
100+
101+
// Throw error for unauthorized origins
102+
throw new RequestError({ code: 'auth.forbidden', status: 403 });
103+
},
104+
allowMethods: allowedMethods.split(',').map((method) => method.trim()),
105+
allowHeaders: ['Content-Type'],
106+
});
107+
}
108+
109+
// Export utilities for testing
110+
export { localDevelopmentOrigins, productionDomainSuffixes };

‎packages/core/src/middleware/koa-cors.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,75 @@ import type { MiddlewareType } from 'koa';
44

55
import { EnvSet } from '#src/env-set/index.js';
66

7+
/**
8+
* @fileoverview General-purpose CORS middleware for Logto APIs
9+
*
10+
* This middleware provides flexible CORS (Cross-Origin Resource Sharing) control for authenticated
11+
* and general-purpose APIs. It uses configurable URL Sets to determine allowed origins and supports
12+
* both strict and lenient policies based on environment and path configurations.
13+
*
14+
* **Use Cases:**
15+
* - Management APIs with authentication
16+
* - User APIs with authentication
17+
* - General endpoints that need flexible origin control
18+
* - APIs that serve multiple applications with different domains
19+
* - Development APIs that need permissive CORS policies
20+
*
21+
* **Key Features:**
22+
* - URL Set-based origin validation
23+
* - Path-based allowlist for specific endpoints
24+
* - Environment-aware policies (development vs production)
25+
* - Automatic localhost filtering in production for security
26+
* - Support for multiple endpoint configurations
27+
*
28+
* **Comparison with Anonymous CORS:**
29+
* - This middleware: Flexible, URL Set-based, suitable for authenticated APIs
30+
* - Anonymous CORS: Strict whitelist, domain suffix-based, for anonymous sensitive APIs
31+
*
32+
* @see {@link koaAnonymousCors} for strict anonymous API CORS handling
33+
*/
34+
35+
/**
36+
* General-purpose CORS middleware factory
37+
*
38+
* Creates a flexible Koa CORS middleware that can handle various origin validation scenarios.
39+
* This middleware is built on top of @koa/cors and adds Logto-specific logic for URL Set
40+
* validation and environment-based security policies.
41+
*
42+
* **Features:**
43+
* - **URL Set Validation**: Validates origins against configured URL sets (admin, user, etc.)
44+
* - **Path-based Allowlist**: Certain paths can allow any origin when specified in allowedPrefixes
45+
* - **Environment Awareness**: More permissive in development, strict in production
46+
* - **Localhost Protection**: Automatically filters localhost in production for security
47+
* - **Multiple Endpoint Support**: Handles complex multi-domain scenarios
48+
*
49+
* **Usage Examples:**
50+
* ```typescript
51+
* // Basic usage with URL sets
52+
* app.use(koaCors([adminUrlSet, userUrlSet]));
53+
*
54+
* // With allowed prefixes for public APIs
55+
* app.use(koaCors([adminUrlSet], ['/api/public', '/health']));
56+
* ```
57+
*
58+
* **Security Behavior:**
59+
* - **Development**: Allows any origin for flexibility
60+
* - **Production**: Strict validation against URL sets, blocks localhost
61+
* - **Allowed Prefixes**: Override strict validation for specific paths
62+
*
63+
* @param urlSets - Array of UrlSet objects containing allowed origins
64+
* @param allowedPrefixes - Optional array of path prefixes that allow any origin
65+
* @returns Koa CORS middleware with Logto-specific origin validation
66+
*
67+
* @example
68+
* ```typescript
69+
* // For management APIs
70+
* managementRouter.use(koaCors([adminUrlSet, cloudUrlSet]));
71+
*
72+
* // For APIs with public endpoints
73+
* apiRouter.use(koaCors([userUrlSet], ['/account', '/verification']));
74+
* ```
75+
*/
776
export default function koaCors<StateT, ContextT, ResponseBodyT>(
877
urlSets: UrlSet[],
978
allowedPrefixes: string[] = []

‎packages/core/src/routes/google-one-tap/index.openapi.json

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,8 @@
5757
"content": {
5858
"application/json": {
5959
"schema": {
60-
"type": "object",
6160
"properties": {
6261
"idToken": {
63-
"type": "string",
6462
"description": "The Google ID Token from Google One Tap"
6563
}
6664
}
@@ -74,23 +72,17 @@
7472
"content": {
7573
"application/json": {
7674
"schema": {
77-
"type": "object",
7875
"properties": {
7976
"oneTimeToken": {
80-
"type": "string",
8177
"description": "The generated one-time token for authentication"
8278
},
8379
"isNewUser": {
84-
"type": "boolean",
8580
"description": "Whether this is a new user (registration) or existing user (login)"
8681
},
8782
"email": {
88-
"type": "string",
89-
"format": "email",
9083
"description": "The verified email address from the Google ID Token"
9184
}
92-
},
93-
"required": ["oneTimeToken", "isNewUser", "email"]
85+
}
9486
}
9587
}
9688
}

‎packages/core/src/routes/google-one-tap/index.ts

Lines changed: 3 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
import chalk from 'chalk';
66
import { addSeconds } from 'date-fns';
77
import { createRemoteJWKSet, jwtVerify } from 'jose';
8-
import type { Context } from 'koa';
98
import { z } from 'zod';
109

1110
import { EnvSet } from '#src/env-set/index.js';
1211
import RequestError from '#src/errors/RequestError/index.js';
12+
import koaAnonymousCors from '#src/middleware/koa-anonymous-cors.js';
1313
import type TenantContext from '#src/tenants/TenantContext.js';
1414
import type { LogtoConnector } from '#src/utils/connectors/types.js';
1515

@@ -21,87 +21,32 @@
2121
// Default expiration time: 10 minutes.
2222
const defaultExpiresTime = 10 * 60;
2323

24-
// List of allowed local development origins
25-
const localDevelopmentOrigins = [
26-
'localhost', // Localhost with any port
27-
'127.0.0.1', // IPv4 loopback
28-
'0.0.0.0', // All interfaces
29-
'[::1]', // IPv6 loopback
30-
'.local', // MDNS domains (especially for macOS)
31-
'host.docker.internal', // Docker host from container
32-
];
33-
34-
// List of allowed production domain suffixes
35-
const productionDomainSuffixes = ['.logto.io', '.logto.dev'];
36-
37-
/**
38-
* Check CORS origin and set appropriate headers
39-
*/
40-
const setCorsHeaders = (ctx: Context, allowedMethods: string) => {
41-
const origin = ctx.get('origin');
42-
43-
const { isProduction, isIntegrationTest } = EnvSet.values;
44-
45-
// Only show debug logs in non-production environments
46-
if (!isProduction) {
47-
consoleLog.info(`origin: ${origin}`);
48-
consoleLog.info(`isIntegrationTest: ${isIntegrationTest}`);
49-
}
50-
51-
// Allow local development origins
52-
if (
53-
(!isProduction || isIntegrationTest) &&
54-
localDevelopmentOrigins.some((item) => origin.includes(item))
55-
) {
56-
ctx.set('Access-Control-Allow-Origin', origin);
57-
}
58-
// In production, only allow *.logto.io or *.logto.dev domains to access
59-
else if (isProduction && productionDomainSuffixes.some((suffix) => origin.endsWith(suffix))) {
60-
ctx.set('Access-Control-Allow-Origin', origin);
61-
} else {
62-
throw new RequestError({ code: 'auth.forbidden', status: 403 });
63-
}
64-
65-
ctx.set('Access-Control-Allow-Methods', allowedMethods);
66-
ctx.set('Access-Control-Allow-Headers', 'Content-Type');
67-
};
68-
69-
/**
70-
* Handle OPTIONS preflight requests
71-
*/
72-
const handleOptionsRequest = async (ctx: Context, next: () => Promise<void>) => {
73-
if (ctx.method === 'OPTIONS') {
74-
ctx.status = 204;
75-
return next();
76-
}
77-
};
78-
7924
/**
8025
* Get and validate Google One Tap connector configuration
8126
*/
8227
const getGoogleOneTapConnector = async (getLogtoConnectors: () => Promise<LogtoConnector[]>) => {
8328
const connectors = await getLogtoConnectors();
8429
const googleOneTapConnector = connectors.find(
8530
(connector) =>
8631
connector.type === ConnectorType.Social && connector.metadata.id === GoogleConnector.factoryId
8732
);
8833

8934
if (!googleOneTapConnector) {
9035
throw new RequestError({ code: 'connector.not_found', status: 404 });
9136
}
9237

9338
const configResult = GoogleConnector.configGuard.safeParse(googleOneTapConnector.dbEntry.config);
9439

9540
if (!configResult.success) {
9641
throw new RequestError({
9742
code: 'connector.invalid_config',
9843
status: 400,
9944
details: configResult.error.flatten(),
10045
});
10146
}
10247

10348
return { connector: googleOneTapConnector, config: configResult.data };
10449
};
10550

10651
export default function googleOneTapRoutes<T extends AnonymousRouter>(
10752
router: T,
@@ -121,6 +66,7 @@
12166

12267
router.get(
12368
'/google-one-tap/config',
69+
koaAnonymousCors('GET'),
12470
koaGuard({
12571
status: [200, 204, 400, 403, 404],
12672
response: GoogleConnector.configGuard
@@ -131,22 +77,19 @@
13177
.optional(),
13278
}),
13379
async (ctx, next) => {
134-
setCorsHeaders(ctx, 'GET, OPTIONS');
135-
136-
await handleOptionsRequest(ctx, next);
137-
13880
const {
13981
config: { clientId, oneTap },
14082
} = await getGoogleOneTapConnector(getLogtoConnectors);
14183

14284
ctx.status = 200;
14385
ctx.body = { clientId, oneTap };
14486
return next();
14587
}
14688
);
14789

14890
router.post(
14991
'/google-one-tap/verify',
92+
koaAnonymousCors('POST'),
15093
koaGuard({
15194
body: z.object({
15295
idToken: z.string(),
@@ -159,74 +102,70 @@
159102
status: [200, 204, 400, 403, 404],
160103
}),
161104
async (ctx, next) => {
162-
setCorsHeaders(ctx, 'POST, OPTIONS');
163-
164-
await handleOptionsRequest(ctx, next);
165-
166105
const { idToken } = ctx.guard.body;
167106

168107
const {
169108
config: { clientId },
170109
} = await getGoogleOneTapConnector(getLogtoConnectors);
171110

172111
// Verify Google ID Token
173112
const { payload } = await tryThat(
174113
async () =>
175114
jwtVerify(idToken, createRemoteJWKSet(new URL(GoogleConnector.jwksUri)), {
176115
issuer: GoogleConnector.issuer,
177116
audience: clientId,
178117
clockTolerance: 10,
179118
}),
180119
(error) => {
181120
throw new RequestError({
182121
code: 'session.google_one_tap.invalid_id_token',
183122
status: 400,
184123
details: error,
185124
});
186125
}
187126
);
188127

189128
const { sub: googleUserId, email, email_verified } = payload;
190129

191130
if (!email || !email_verified) {
192131
throw new RequestError({
193132
code: 'session.google_one_tap.unverified_email',
194133
status: 400,
195134
});
196135
}
197136

198137
// Check if user exists with this Google identity
199138
const existingUser = await findUserByIdentity(GoogleConnector.target, String(googleUserId));
200139
const isNewUser = !existingUser;
201140

202141
// Create one-time token with appropriate context
203142
const expiresAt = addSeconds(new Date(), defaultExpiresTime);
204143
const oneTimeToken = await insertOneTimeToken({
205144
id: generateStandardId(),
206145
email: String(email),
207146
token: generateStandardSecret(),
208147
expiresAt: expiresAt.getTime(),
209148
context: {
210149
// Add jitOrganizationIds if this is for registration
211150
...(isNewUser && { jitOrganizationIds: [] }),
212151
},
213152
});
214153

215154
// Clean up any expired tokens for this email
216155
await trySafe(
217156
async () => updateExpiredOneTimeTokensStatusByEmail(String(email)),
218157
(error) => {
219158
consoleLog.error('Failed to clean up expired tokens:', error);
220159
}
221160
);
222161

223162
ctx.status = 200;
224163
ctx.body = {
225164
oneTimeToken: oneTimeToken.token,
226165
isNewUser,
227166
email: String(email),
228167
};
229168

230169
return next();
231170
}
232171
);

‎packages/toolkit/connector-kit/src/types/social.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export const GoogleConnector = Object.freeze({
131131
target: 'google',
132132
/** The factory ID of the official Google connector. */
133133
factoryId: 'google-universal',
134+
// TODO: update google connector as well, keep it unchanged for now since it's out of scope.
134135
/**
135136
* The URI of the Google JWKS. Used to verify the Google-issued JWT.
136137
* @see {@link https://developers.google.com/identity/gsi/web/guides/verify-google-id-token | Verify the Google ID token on your server side}

0 commit comments

Comments
 (0)
Please sign in to comment.