@oauth42/next 0.4.7 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -2
- package/dist/{auth-BThRQMzh.d.mts → auth-CYMPe3k0.d.ts} +10 -0
- package/dist/{auth-BThRQMzh.d.ts → auth-D3tAruwx.d.mts} +10 -0
- package/dist/client/index.d.mts +19 -11
- package/dist/client/index.d.ts +19 -11
- package/dist/client/index.js +10 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +10 -1
- package/dist/client/index.mjs.map +1 -1
- package/dist/index-Cfqxl9r2.d.mts +94 -0
- package/dist/index-Cfqxl9r2.d.ts +94 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +237 -28
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +237 -28
- package/dist/index.mjs.map +1 -1
- package/dist/middleware/index.d.mts +2 -39
- package/dist/middleware/index.d.ts +2 -39
- package/dist/middleware/index.js +107 -10
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +107 -10
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/server/index.d.mts +2 -2
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +243 -28
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +240 -28
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -116,12 +116,72 @@ function withOAuth42ServerSideProps(getServerSideProps, authOptions) {
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// src/server/backend-session.ts
|
|
120
|
+
function isBackendSessionMode(mode) {
|
|
121
|
+
return mode === "backend";
|
|
122
|
+
}
|
|
123
|
+
function sessionEndpoint(issuer) {
|
|
124
|
+
return `${issuer.replace(/\/$/, "")}/api/sdk/sessions`;
|
|
125
|
+
}
|
|
126
|
+
async function createBackendSession(params) {
|
|
127
|
+
const response = await fetch(sessionEndpoint(params.issuer), {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "Content-Type": "application/json" },
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
access_token: params.accessToken,
|
|
132
|
+
refresh_token: params.refreshToken,
|
|
133
|
+
expires_at: params.expiresAt ?? Math.floor(Date.now() / 1e3) + 3600,
|
|
134
|
+
id_token: params.idToken,
|
|
135
|
+
client_id: params.clientId,
|
|
136
|
+
client_secret: params.clientSecret,
|
|
137
|
+
issuer: params.issuer,
|
|
138
|
+
user: params.user
|
|
139
|
+
})
|
|
140
|
+
});
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(`OAuth42 backend session create failed: ${response.status}`);
|
|
143
|
+
}
|
|
144
|
+
return response.json();
|
|
145
|
+
}
|
|
146
|
+
async function resolveBackendSession(params) {
|
|
147
|
+
const response = await fetch(`${sessionEndpoint(params.issuer)}/resolve`, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: { "Content-Type": "application/json" },
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
session_id: params.sessionId,
|
|
152
|
+
client_id: params.clientId,
|
|
153
|
+
client_secret: params.clientSecret,
|
|
154
|
+
issuer: params.issuer
|
|
155
|
+
})
|
|
156
|
+
});
|
|
157
|
+
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
throw new Error(`OAuth42 backend session resolve failed: ${response.status}`);
|
|
162
|
+
}
|
|
163
|
+
return response.json();
|
|
164
|
+
}
|
|
165
|
+
async function revokeBackendSession(params) {
|
|
166
|
+
await fetch(sessionEndpoint(params.issuer), {
|
|
167
|
+
method: "DELETE",
|
|
168
|
+
headers: { "Content-Type": "application/json" },
|
|
169
|
+
body: JSON.stringify({
|
|
170
|
+
session_id: params.sessionId,
|
|
171
|
+
client_id: params.clientId,
|
|
172
|
+
client_secret: params.clientSecret
|
|
173
|
+
})
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
119
177
|
// src/server/auth.ts
|
|
120
178
|
var NextAuth = NextAuthDefault.default || NextAuthDefault;
|
|
121
179
|
var activeRefresh = null;
|
|
122
180
|
function createAuth(options = {}) {
|
|
123
181
|
const clientId = options.clientId || process.env.OAUTH42_CLIENT_ID;
|
|
124
182
|
const clientSecret = options.clientSecret || process.env.OAUTH42_CLIENT_SECRET;
|
|
183
|
+
const issuer = options.issuer || process.env.NEXT_PUBLIC_OAUTH_ISSUER || process.env.OAUTH42_ISSUER || "https://localhost:8443";
|
|
184
|
+
const useBackendSessions = isBackendSessionMode(options.sessionStore);
|
|
125
185
|
if (!clientId || !clientSecret) {
|
|
126
186
|
throw new Error(
|
|
127
187
|
"OAuth42 client credentials are required. Set OAUTH42_CLIENT_ID and OAUTH42_CLIENT_SECRET environment variables or pass them in the options."
|
|
@@ -132,7 +192,7 @@ function createAuth(options = {}) {
|
|
|
132
192
|
OAuth42Provider({
|
|
133
193
|
clientId,
|
|
134
194
|
clientSecret,
|
|
135
|
-
issuer
|
|
195
|
+
issuer,
|
|
136
196
|
scopes: options.scopes,
|
|
137
197
|
pkceEnabled: options.pkceEnabled
|
|
138
198
|
})
|
|
@@ -141,14 +201,41 @@ function createAuth(options = {}) {
|
|
|
141
201
|
async jwt({ token, account, profile }) {
|
|
142
202
|
console.log("[OAuth42 SDK] JWT callback called", { hasAccount: !!account, hasProfile: !!profile });
|
|
143
203
|
if (account) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
204
|
+
if (useBackendSessions) {
|
|
205
|
+
if (!account.access_token || !account.refresh_token) {
|
|
206
|
+
throw new Error("OAuth42 backend sessions require access and refresh tokens");
|
|
207
|
+
}
|
|
208
|
+
const oauth42Profile = profile;
|
|
209
|
+
const backendSession = await createBackendSession({
|
|
210
|
+
issuer,
|
|
211
|
+
clientId,
|
|
212
|
+
clientSecret,
|
|
213
|
+
accessToken: account.access_token,
|
|
214
|
+
refreshToken: account.refresh_token,
|
|
215
|
+
expiresAt: account.expires_at,
|
|
216
|
+
idToken: account.id_token,
|
|
217
|
+
user: {
|
|
218
|
+
sub: oauth42Profile?.sub,
|
|
219
|
+
email: oauth42Profile?.email,
|
|
220
|
+
username: oauth42Profile?.username,
|
|
221
|
+
name: token.name,
|
|
222
|
+
emailVerified: oauth42Profile?.email_verified,
|
|
223
|
+
groups: oauth42Profile?.groups
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
token.oauth42SessionId = backendSession.session_id;
|
|
227
|
+
token.expiresAt = backendSession.expires_at;
|
|
228
|
+
token.issuer = issuer;
|
|
229
|
+
} else {
|
|
230
|
+
console.log("[OAuth42 SDK] Initial sign in - storing tokens in JWT");
|
|
231
|
+
token.accessToken = account.access_token;
|
|
232
|
+
token.refreshToken = account.refresh_token;
|
|
233
|
+
token.expiresAt = account.expires_at;
|
|
234
|
+
token.idToken = account.id_token;
|
|
235
|
+
token.clientId = clientId;
|
|
236
|
+
token.clientSecret = clientSecret;
|
|
237
|
+
token.issuer = issuer;
|
|
238
|
+
}
|
|
152
239
|
}
|
|
153
240
|
if (profile) {
|
|
154
241
|
const oauth42Profile = profile;
|
|
@@ -165,12 +252,40 @@ function createAuth(options = {}) {
|
|
|
165
252
|
},
|
|
166
253
|
async session({ session, token }) {
|
|
167
254
|
console.log("[OAuth42 SDK] Session callback called", { hasToken: !!token, hasSession: !!session });
|
|
168
|
-
|
|
169
|
-
|
|
255
|
+
if (useBackendSessions) {
|
|
256
|
+
const sessionId = token.oauth42SessionId;
|
|
257
|
+
if (!sessionId) {
|
|
258
|
+
session.error = "SessionRevoked";
|
|
259
|
+
return session;
|
|
260
|
+
}
|
|
261
|
+
const backendSession = await resolveBackendSession({
|
|
262
|
+
issuer,
|
|
263
|
+
clientId,
|
|
264
|
+
clientSecret,
|
|
265
|
+
sessionId
|
|
266
|
+
});
|
|
267
|
+
if (!backendSession) {
|
|
268
|
+
session.error = "SessionRevoked";
|
|
269
|
+
return session;
|
|
270
|
+
}
|
|
271
|
+
session.accessToken = backendSession.access_token;
|
|
272
|
+
session.idToken = backendSession.id_token;
|
|
273
|
+
token.expiresAt = backendSession.expires_at;
|
|
274
|
+
if (backendSession.user && session.user) {
|
|
275
|
+
session.user.email = backendSession.user.email;
|
|
276
|
+
session.user.name = backendSession.user.name;
|
|
277
|
+
session.user.username = backendSession.user.username;
|
|
278
|
+
session.user.emailVerified = backendSession.user.emailVerified;
|
|
279
|
+
session.user.groups = backendSession.user.groups;
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
session.accessToken = token.accessToken;
|
|
283
|
+
session.idToken = token.idToken;
|
|
284
|
+
}
|
|
170
285
|
if (token.error) {
|
|
171
286
|
session.error = token.error;
|
|
172
287
|
}
|
|
173
|
-
if (session.user) {
|
|
288
|
+
if (!useBackendSessions && session.user) {
|
|
174
289
|
session.user.email = token.email;
|
|
175
290
|
session.user.name = token.name;
|
|
176
291
|
session.user.username = token.username;
|
|
@@ -194,6 +309,26 @@ function createAuth(options = {}) {
|
|
|
194
309
|
strategy: "jwt",
|
|
195
310
|
...options.session
|
|
196
311
|
},
|
|
312
|
+
events: {
|
|
313
|
+
async signOut(message) {
|
|
314
|
+
if (useBackendSessions) {
|
|
315
|
+
const sessionId = message.token?.oauth42SessionId;
|
|
316
|
+
if (sessionId) {
|
|
317
|
+
await revokeBackendSession({
|
|
318
|
+
issuer,
|
|
319
|
+
clientId,
|
|
320
|
+
clientSecret,
|
|
321
|
+
sessionId
|
|
322
|
+
}).catch((error) => {
|
|
323
|
+
console.warn("[OAuth42 SDK] Failed to revoke backend session on signOut:", error);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (options.events?.signOut) {
|
|
328
|
+
await options.events.signOut(message);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
},
|
|
197
332
|
debug: options.debug || process.env.NODE_ENV === "development",
|
|
198
333
|
secret: process.env.NEXTAUTH_SECRET,
|
|
199
334
|
// Configure unique cookie names per app to prevent session conflicts on localhost
|
|
@@ -226,7 +361,11 @@ function createAuth(options = {}) {
|
|
|
226
361
|
secure: process.env.NODE_ENV === "production"
|
|
227
362
|
}
|
|
228
363
|
},
|
|
229
|
-
// PKCE code_verifier cookie - essential for PKCE flow
|
|
364
|
+
// PKCE code_verifier cookie - essential for PKCE flow.
|
|
365
|
+
// TTL covers the full OAuth round-trip including user-in-the-loop steps
|
|
366
|
+
// like passkey ceremonies, MFA enrollment, and any debugging pauses.
|
|
367
|
+
// The previous 15-minute value was too tight for realistic ceremony-
|
|
368
|
+
// heavy flows — see #524 for the incident where state expired mid-debug.
|
|
230
369
|
pkceCodeVerifier: {
|
|
231
370
|
name: `${options.cookiePrefix}.pkce.code_verifier`,
|
|
232
371
|
options: {
|
|
@@ -234,11 +373,13 @@ function createAuth(options = {}) {
|
|
|
234
373
|
sameSite: "lax",
|
|
235
374
|
path: "/",
|
|
236
375
|
secure: process.env.NODE_ENV === "production",
|
|
237
|
-
maxAge:
|
|
238
|
-
// 15
|
|
376
|
+
maxAge: 3600
|
|
377
|
+
// 1 hour (was 900 — 15 min was too short for passkey/MFA flows)
|
|
239
378
|
}
|
|
240
379
|
},
|
|
241
|
-
// State cookie for OAuth CSRF protection
|
|
380
|
+
// State cookie for OAuth CSRF protection. Same TTL rationale as
|
|
381
|
+
// pkceCodeVerifier above. State is single-use CSRF and cannot be
|
|
382
|
+
// replayed, so a longer TTL does not weaken the security story.
|
|
242
383
|
state: {
|
|
243
384
|
name: `${options.cookiePrefix}.state`,
|
|
244
385
|
options: {
|
|
@@ -246,8 +387,8 @@ function createAuth(options = {}) {
|
|
|
246
387
|
sameSite: "lax",
|
|
247
388
|
path: "/",
|
|
248
389
|
secure: process.env.NODE_ENV === "production",
|
|
249
|
-
maxAge:
|
|
250
|
-
// 15
|
|
390
|
+
maxAge: 3600
|
|
391
|
+
// 1 hour (was 900 — 15 min was too short for passkey/MFA flows)
|
|
251
392
|
}
|
|
252
393
|
},
|
|
253
394
|
// Nonce cookie for OpenID Connect
|
|
@@ -341,7 +482,22 @@ import { getToken, encode } from "next-auth/jwt";
|
|
|
341
482
|
var ALLOWED_COOKIE_SIZE = 4096;
|
|
342
483
|
var ESTIMATED_EMPTY_COOKIE_SIZE = 163;
|
|
343
484
|
var CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
|
|
485
|
+
var REFRESH_RACE_GRACE_SECONDS = 60;
|
|
486
|
+
var DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
|
|
344
487
|
var pendingRefreshes = /* @__PURE__ */ new Map();
|
|
488
|
+
function shouldPreserveSessionOnRefreshFailure(params) {
|
|
489
|
+
if (params.error !== "invalid_grant" || !params.expiresAt) {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
return params.now - params.expiresAt <= REFRESH_RACE_GRACE_SECONDS;
|
|
493
|
+
}
|
|
494
|
+
function resolveRefreshConfig(options, token) {
|
|
495
|
+
return {
|
|
496
|
+
clientId: options.clientId || token.clientId || process.env.OAUTH42_CLIENT_ID,
|
|
497
|
+
clientSecret: options.clientSecret || token.clientSecret || process.env.OAUTH42_CLIENT_SECRET,
|
|
498
|
+
issuer: options.issuer || token.issuer || process.env.OAUTH42_ISSUER || "https://localhost:8443"
|
|
499
|
+
};
|
|
500
|
+
}
|
|
345
501
|
async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
346
502
|
try {
|
|
347
503
|
const tokenUrl = `${issuer}/oauth2/token`;
|
|
@@ -376,9 +532,6 @@ async function refreshTokens(refreshToken, clientId, clientSecret, issuer) {
|
|
|
376
532
|
}
|
|
377
533
|
function withOAuth42Auth(options = {}) {
|
|
378
534
|
const secret = process.env.NEXTAUTH_SECRET;
|
|
379
|
-
const clientId = process.env.OAUTH42_CLIENT_ID;
|
|
380
|
-
const clientSecret = process.env.OAUTH42_CLIENT_SECRET;
|
|
381
|
-
const issuer = process.env.OAUTH42_ISSUER || "https://localhost:8443";
|
|
382
535
|
if (!secret) {
|
|
383
536
|
console.warn("[OAuth42 Middleware] NEXTAUTH_SECRET not set");
|
|
384
537
|
}
|
|
@@ -403,6 +556,42 @@ function withOAuth42Auth(options = {}) {
|
|
|
403
556
|
url.searchParams.set("callbackUrl", pathname);
|
|
404
557
|
return NextResponse.redirect(url);
|
|
405
558
|
}
|
|
559
|
+
const refreshConfig = resolveRefreshConfig(options, token);
|
|
560
|
+
if (isBackendSessionMode(options.sessionStore)) {
|
|
561
|
+
const sessionId = token.oauth42SessionId;
|
|
562
|
+
if (!sessionId || !refreshConfig.clientId || !refreshConfig.clientSecret) {
|
|
563
|
+
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
564
|
+
const url = new URL(signInUrl, req.url);
|
|
565
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
566
|
+
const response = NextResponse.redirect(url);
|
|
567
|
+
response.cookies.delete(cookieName);
|
|
568
|
+
return response;
|
|
569
|
+
}
|
|
570
|
+
const backendSession = await resolveBackendSession({
|
|
571
|
+
issuer: refreshConfig.issuer,
|
|
572
|
+
clientId: refreshConfig.clientId,
|
|
573
|
+
clientSecret: refreshConfig.clientSecret,
|
|
574
|
+
sessionId
|
|
575
|
+
}).catch((error) => {
|
|
576
|
+
console.error("[OAuth42 Middleware] Backend session resolve failed:", error);
|
|
577
|
+
return null;
|
|
578
|
+
});
|
|
579
|
+
if (!backendSession) {
|
|
580
|
+
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
581
|
+
const url = new URL(signInUrl, req.url);
|
|
582
|
+
url.searchParams.set("callbackUrl", pathname);
|
|
583
|
+
const response = NextResponse.redirect(url);
|
|
584
|
+
response.cookies.delete(cookieName);
|
|
585
|
+
return response;
|
|
586
|
+
}
|
|
587
|
+
const requestHeaders = new Headers(req.headers);
|
|
588
|
+
requestHeaders.set("x-oauth42-refreshed-token", backendSession.access_token);
|
|
589
|
+
return NextResponse.next({
|
|
590
|
+
request: {
|
|
591
|
+
headers: requestHeaders
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
}
|
|
406
595
|
const expiresAt = token.expiresAt;
|
|
407
596
|
const now = Math.floor(Date.now() / 1e3);
|
|
408
597
|
const bufferSeconds = 15;
|
|
@@ -416,7 +605,7 @@ function withOAuth42Auth(options = {}) {
|
|
|
416
605
|
path: pathname
|
|
417
606
|
});
|
|
418
607
|
}
|
|
419
|
-
if (needsRefresh && token.refreshToken && clientId && clientSecret) {
|
|
608
|
+
if (needsRefresh && token.refreshToken && refreshConfig.clientId && refreshConfig.clientSecret) {
|
|
420
609
|
const userId = token.sub;
|
|
421
610
|
let refreshPromise = pendingRefreshes.get(userId);
|
|
422
611
|
if (refreshPromise) {
|
|
@@ -425,11 +614,14 @@ function withOAuth42Auth(options = {}) {
|
|
|
425
614
|
console.log("[OAuth42 Middleware] Access token expired, refreshing...");
|
|
426
615
|
refreshPromise = refreshTokens(
|
|
427
616
|
token.refreshToken,
|
|
428
|
-
clientId,
|
|
429
|
-
clientSecret,
|
|
430
|
-
issuer
|
|
617
|
+
refreshConfig.clientId,
|
|
618
|
+
refreshConfig.clientSecret,
|
|
619
|
+
refreshConfig.issuer
|
|
431
620
|
).finally(() => {
|
|
432
|
-
setTimeout(() => pendingRefreshes.delete(userId), 3e4);
|
|
621
|
+
const cleanupTimer = setTimeout(() => pendingRefreshes.delete(userId), 3e4);
|
|
622
|
+
if (typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
|
|
623
|
+
cleanupTimer.unref();
|
|
624
|
+
}
|
|
433
625
|
});
|
|
434
626
|
pendingRefreshes.set(userId, refreshPromise);
|
|
435
627
|
}
|
|
@@ -456,7 +648,8 @@ function withOAuth42Auth(options = {}) {
|
|
|
456
648
|
httpOnly: true,
|
|
457
649
|
sameSite: "lax",
|
|
458
650
|
path: "/",
|
|
459
|
-
secure: process.env.NODE_ENV === "production"
|
|
651
|
+
secure: process.env.NODE_ENV === "production",
|
|
652
|
+
maxAge: options.session?.maxAge ?? DEFAULT_SESSION_MAX_AGE_SECONDS
|
|
460
653
|
};
|
|
461
654
|
if (newJwt.length <= CHUNK_SIZE) {
|
|
462
655
|
response.cookies.set(cookieName, newJwt, cookieOptions);
|
|
@@ -504,7 +697,23 @@ function withOAuth42Auth(options = {}) {
|
|
|
504
697
|
});
|
|
505
698
|
return response;
|
|
506
699
|
} else {
|
|
507
|
-
|
|
700
|
+
if (shouldPreserveSessionOnRefreshFailure({
|
|
701
|
+
error: refreshed.error,
|
|
702
|
+
expiresAt,
|
|
703
|
+
now
|
|
704
|
+
})) {
|
|
705
|
+
console.warn(
|
|
706
|
+
"[OAuth42 Middleware] Refresh failed with invalid_grant for a recently expired token; preserving session cookie for likely parallel refresh race",
|
|
707
|
+
{
|
|
708
|
+
expiresAt,
|
|
709
|
+
now,
|
|
710
|
+
expiredAgo: now - expiresAt,
|
|
711
|
+
path: pathname
|
|
712
|
+
}
|
|
713
|
+
);
|
|
714
|
+
return NextResponse.next();
|
|
715
|
+
}
|
|
716
|
+
console.log("[OAuth42 Middleware] Refresh failed, clearing session");
|
|
508
717
|
const signInUrl = options.pages?.signIn || "/auth/signin";
|
|
509
718
|
const url = new URL(signInUrl, req.url);
|
|
510
719
|
url.searchParams.set("callbackUrl", pathname);
|
|
@@ -650,12 +859,15 @@ export {
|
|
|
650
859
|
default2 as NextAuth,
|
|
651
860
|
OAuth42Provider,
|
|
652
861
|
createAuth,
|
|
862
|
+
createBackendSession,
|
|
653
863
|
createHandlers,
|
|
654
864
|
createHostedAuthCallback,
|
|
655
865
|
createMiddlewareConfig,
|
|
656
866
|
getOAuth42Session,
|
|
657
867
|
getServerSession,
|
|
658
868
|
refreshAccessToken,
|
|
869
|
+
resolveBackendSession,
|
|
870
|
+
revokeBackendSession,
|
|
659
871
|
withOAuth42Auth,
|
|
660
872
|
withOAuth42ServerSideProps,
|
|
661
873
|
withOAuth42Session
|