Skip to content

Commit d691e90

Browse files
chore(web): make session and OAuth token lifetimes configurable (#1162)
* chore(web): make session and OAuth token lifetimes configurable - Auth.js JWT browser sessions now respect `AUTH_SESSION_MAX_AGE_SECONDS` and `AUTH_SESSION_UPDATE_AGE_SECONDS` (defaults: 30 days / 1 day, matching Auth.js's own defaults). - OAuth flow TTLs (authorization code, access token, refresh token) now respect `OAUTH_AUTHORIZATION_CODE_TTL_SECONDS`, `OAUTH_ACCESS_TOKEN_TTL_SECONDS`, and `OAUTH_REFRESH_TOKEN_TTL_SECONDS` (defaults: 10 minutes / 1 hour / 90 days, matching the previously hard-coded values). Defaults preserve today's behavior; operators who want shorter sessions can lower these values without code changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add CHANGELOG entry for #1162 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: mock env in oauth server tests Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7733ec9 commit d691e90

7 files changed

Lines changed: 60 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
### Changed
2020
- Added `/api/avatar` to resolve user profile pictures. [#1159](https://github.com/sourcebot-dev/sourcebot/pull/1159)
2121
- Hardened post-auth redirects with an explicit same-origin `redirect` callback in the NextAuth config, and switched the legacy `/~/...` URL rewrite from a 308 to a 301. [#1161](https://github.com/sourcebot-dev/sourcebot/pull/1161)
22+
- Made the Auth.js JWT session lifetime and OAuth token TTLs configurable via `AUTH_SESSION_MAX_AGE_SECONDS`, `AUTH_SESSION_UPDATE_AGE_SECONDS`, `OAUTH_AUTHORIZATION_CODE_TTL_SECONDS`, `OAUTH_ACCESS_TOKEN_TTL_SECONDS`, and `OAUTH_REFRESH_TOKEN_TTL_SECONDS`. Defaults preserve existing behavior. [#1162](https://github.com/sourcebot-dev/sourcebot/pull/1162)
2223

2324
### Fixed
2425
- Bumped `postcss` to `8.5.10`. [#1155](https://github.com/sourcebot-dev/sourcebot/pull/1155)

docs/docs/configuration/environment-variables.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ The following environment variables allow you to configure your Sourcebot deploy
1313
| `AUTH_CREDENTIALS_LOGIN_ENABLED` | `true` | <p>Enables/disables authentication with basic credentials. Username and passwords are stored encrypted at rest within the postgres database. Checkout the [auth docs](/docs/configuration/auth/overview) for more info</p> |
1414
| `AUTH_EMAIL_CODE_LOGIN_ENABLED` | `false` | <p>Enables/disables authentication with a login code that's sent to a users email. `SMTP_CONNECTION_URL` and `EMAIL_FROM_ADDRESS` must also be set. Checkout the [auth docs](/docs/configuration/auth/overview) for more info </p> |
1515
| `AUTH_SECRET` | Automatically generated at startup if no value is provided. Generated using `openssl rand -base64 33` | <p>Used to validate login session cookies</p> |
16+
| `AUTH_SESSION_MAX_AGE_SECONDS` | `2592000` (30 days) | <p>Relative time from now in seconds when to expire the session.</p> |
17+
| `AUTH_SESSION_UPDATE_AGE_SECONDS` | `86400` (1 day) | <p>How often the session should be updated in seconds. If set to `0`, session is updated every time.</p> |
18+
| `OAUTH_AUTHORIZATION_CODE_TTL_SECONDS` | `600` (10 minutes) | <p>Lifetime of an OAuth authorization code, in seconds.</p> |
19+
| `OAUTH_ACCESS_TOKEN_TTL_SECONDS` | `3600` (1 hour) | <p>Lifetime of an OAuth access token, in seconds.</p> |
20+
| `OAUTH_REFRESH_TOKEN_TTL_SECONDS` | `7776000` (90 days) | <p>Lifetime of an OAuth refresh token, in seconds.</p> |
1621
| `AUTH_URL` | - | <p>URL of your Sourcebot deployment, e.g., `https://example.com` or `http://localhost:3000`.</p> |
1722
| `CONFIG_PATH` | `-` | <p>The container relative path to the declarative configuration file. See [this doc](/docs/configuration/declarative-config) for more info.</p> |
1823
| `DATA_CACHE_DIR` | `$DATA_DIR/.sourcebot` | <p>The root data directory in which all data written to disk by Sourcebot will be located.</p> |

packages/shared/src/env.server.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,41 @@ const options = {
139139
AUTH_CREDENTIALS_LOGIN_ENABLED: booleanSchema.default('true'),
140140
AUTH_EMAIL_CODE_LOGIN_ENABLED: booleanSchema.default('false'),
141141

142+
/**
143+
* Relative time from now in seconds when to expire the session.
144+
*
145+
* @default 30 days
146+
*/
147+
AUTH_SESSION_MAX_AGE_SECONDS: numberSchema.default(60 * 60 * 24 * 30),
148+
149+
/**
150+
* How often the session should be updated in seconds. If set to 0, session is updated every time.
151+
*
152+
* @default 1 day
153+
*/
154+
AUTH_SESSION_UPDATE_AGE_SECONDS: numberSchema.default(60 * 60 * 24),
155+
156+
/**
157+
* Lifetime of an OAuth authorization code, in seconds.
158+
*
159+
* @default 10 minutes
160+
*/
161+
OAUTH_AUTHORIZATION_CODE_TTL_SECONDS: numberSchema.default(60 * 10),
162+
163+
/**
164+
* Lifetime of an OAuth access token, in seconds.
165+
*
166+
* @default 1 hour
167+
*/
168+
OAUTH_ACCESS_TOKEN_TTL_SECONDS: numberSchema.default(60 * 60),
169+
170+
/**
171+
* Lifetime of an OAuth refresh token, in seconds.
172+
*
173+
* @default 90 days
174+
*/
175+
OAUTH_REFRESH_TOKEN_TTL_SECONDS: numberSchema.default(60 * 60 * 24 * 90),
176+
142177
// Enterprise Auth
143178
AUTH_EE_ALLOW_EMAIL_ACCOUNT_LINKING:
144179
booleanSchema

packages/web/src/app/api/(server)/ee/oauth/token/route.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { verifyAndExchangeCode, verifyAndRotateRefreshToken, ACCESS_TOKEN_TTL_SECONDS } from '@/ee/features/oauth/server';
1+
import { verifyAndExchangeCode, verifyAndRotateRefreshToken } from '@/ee/features/oauth/server';
22
import { apiHandler } from '@/lib/apiHandler';
3-
import { hasEntitlement } from '@sourcebot/shared';
3+
import { env, hasEntitlement } from '@sourcebot/shared';
44
import { NextRequest } from 'next/server';
55
import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants';
66

@@ -59,7 +59,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
5959
access_token: result.token,
6060
refresh_token: result.refreshToken,
6161
token_type: 'Bearer',
62-
expires_in: ACCESS_TOKEN_TTL_SECONDS,
62+
expires_in: env.OAUTH_ACCESS_TOKEN_TTL_SECONDS,
6363
scope: '',
6464
});
6565
}
@@ -91,7 +91,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
9191
access_token: result.token,
9292
refresh_token: result.refreshToken,
9393
token_type: 'Bearer',
94-
expires_in: ACCESS_TOKEN_TTL_SECONDS,
94+
expires_in: env.OAUTH_ACCESS_TOKEN_TTL_SECONDS,
9595
scope: '',
9696
});
9797
}

packages/web/src/auth.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
148148
adapter: EncryptedPrismaAdapter(__unsafePrisma),
149149
session: {
150150
strategy: "jwt",
151+
maxAge: env.AUTH_SESSION_MAX_AGE_SECONDS,
152+
updateAge: env.AUTH_SESSION_UPDATE_AGE_SECONDS,
151153
},
152154
trustHost: true,
153155
events: {

packages/web/src/ee/features/oauth/server.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ vi.mock('@sourcebot/shared', () => ({
1515
generateOAuthRefreshToken: vi.fn(() => ({ token: 'sbor_newrefresh', hash: 'newrefresh' })),
1616
OAUTH_ACCESS_TOKEN_PREFIX: 'sboa_',
1717
OAUTH_REFRESH_TOKEN_PREFIX: 'sbor_',
18+
env: {
19+
OAUTH_AUTHORIZATION_CODE_TTL_SECONDS: 60 * 10,
20+
OAUTH_ACCESS_TOKEN_TTL_SECONDS: 60 * 60,
21+
OAUTH_REFRESH_TOKEN_TTL_SECONDS: 60 * 60 * 24 * 90,
22+
},
1823
}));
1924

2025
const VALID_CODE_HASH = 'validcode';

packages/web/src/ee/features/oauth/server.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'server-only';
33
import { __unsafePrisma } from '@/prisma';
44
import { Prisma } from '@prisma/client';
55
import {
6+
env,
67
generateOAuthRefreshToken,
78
generateOAuthToken,
89
hashSecret,
@@ -11,12 +12,6 @@ import {
1112
} from '@sourcebot/shared';
1213
import crypto from 'crypto';
1314

14-
const AUTH_CODE_TTL_MS = 10 * 60 * 1000; // 10 minutes
15-
const ACCESS_TOKEN_TTL_MS = 60 * 60 * 1000; // 1 hour
16-
const REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1000; // 90 days
17-
18-
export const ACCESS_TOKEN_TTL_SECONDS = Math.floor(ACCESS_TOKEN_TTL_MS / 1000);
19-
2015
// Generates a random authorization code, hashes it, and stores it alongside the
2116
// PKCE code challenge. Returns the raw code to be sent to the client.
2217
export async function generateAndStoreAuthCode({
@@ -43,7 +38,7 @@ export async function generateAndStoreAuthCode({
4338
redirectUri,
4439
codeChallenge,
4540
resource,
46-
expiresAt: new Date(Date.now() + AUTH_CODE_TTL_MS),
41+
expiresAt: new Date(Date.now() + env.OAUTH_AUTHORIZATION_CODE_TTL_SECONDS * 1000),
4742
},
4843
});
4944

@@ -124,7 +119,7 @@ export async function verifyAndExchangeCode({
124119
clientId,
125120
userId: authCode.userId,
126121
resource: authCode.resource,
127-
expiresAt: new Date(Date.now() + ACCESS_TOKEN_TTL_MS),
122+
expiresAt: new Date(Date.now() + env.OAUTH_ACCESS_TOKEN_TTL_SECONDS * 1000),
128123
},
129124
}),
130125
__unsafePrisma.oAuthRefreshToken.create({
@@ -133,12 +128,12 @@ export async function verifyAndExchangeCode({
133128
clientId,
134129
userId: authCode.userId,
135130
resource: authCode.resource,
136-
expiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL_MS),
131+
expiresAt: new Date(Date.now() + env.OAUTH_REFRESH_TOKEN_TTL_SECONDS * 1000),
137132
},
138133
}),
139134
]);
140135

141-
return { token, refreshToken, expiresIn: ACCESS_TOKEN_TTL_SECONDS };
136+
return { token, refreshToken, expiresIn: env.OAUTH_ACCESS_TOKEN_TTL_SECONDS };
142137
}
143138

144139
// Verifies a refresh token, rotates it, and issues a new access token + refresh token.
@@ -189,7 +184,7 @@ export async function verifyAndRotateRefreshToken({
189184
clientId,
190185
userId: existing.userId,
191186
resource: existing.resource,
192-
expiresAt: new Date(Date.now() + ACCESS_TOKEN_TTL_MS),
187+
expiresAt: new Date(Date.now() + env.OAUTH_ACCESS_TOKEN_TTL_SECONDS * 1000),
193188
},
194189
}),
195190
__unsafePrisma.oAuthRefreshToken.create({
@@ -198,12 +193,12 @@ export async function verifyAndRotateRefreshToken({
198193
clientId,
199194
userId: existing.userId,
200195
resource: existing.resource,
201-
expiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL_MS),
196+
expiresAt: new Date(Date.now() + env.OAUTH_REFRESH_TOKEN_TTL_SECONDS * 1000),
202197
},
203198
}),
204199
]);
205200

206-
return { token, refreshToken, expiresIn: ACCESS_TOKEN_TTL_SECONDS };
201+
return { token, refreshToken, expiresIn: env.OAUTH_ACCESS_TOKEN_TTL_SECONDS };
207202
}
208203

209204
// Revokes an access token or refresh token by hashing it and deleting the DB record.

0 commit comments

Comments
 (0)