Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Added `/api/avatar` to resolve user profile pictures. [#1159](https://github.com/sourcebot-dev/sourcebot/pull/1159)
- 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)
- 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)

### Fixed
- Bumped `postcss` to `8.5.10`. [#1155](https://github.com/sourcebot-dev/sourcebot/pull/1155)
Expand Down
5 changes: 5 additions & 0 deletions docs/docs/configuration/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ The following environment variables allow you to configure your Sourcebot deploy
| `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> |
| `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> |
| `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> |
| `AUTH_SESSION_MAX_AGE_SECONDS` | `2592000` (30 days) | <p>Relative time from now in seconds when to expire the session.</p> |
| `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> |
| `OAUTH_AUTHORIZATION_CODE_TTL_SECONDS` | `600` (10 minutes) | <p>Lifetime of an OAuth authorization code, in seconds.</p> |
| `OAUTH_ACCESS_TOKEN_TTL_SECONDS` | `3600` (1 hour) | <p>Lifetime of an OAuth access token, in seconds.</p> |
| `OAUTH_REFRESH_TOKEN_TTL_SECONDS` | `7776000` (90 days) | <p>Lifetime of an OAuth refresh token, in seconds.</p> |
| `AUTH_URL` | - | <p>URL of your Sourcebot deployment, e.g., `https://example.com` or `http://localhost:3000`.</p> |
| `CONFIG_PATH` | `-` | <p>The container relative path to the declarative configuration file. See [this doc](/docs/configuration/declarative-config) for more info.</p> |
| `DATA_CACHE_DIR` | `$DATA_DIR/.sourcebot` | <p>The root data directory in which all data written to disk by Sourcebot will be located.</p> |
Expand Down
35 changes: 35 additions & 0 deletions packages/shared/src/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,41 @@ const options = {
AUTH_CREDENTIALS_LOGIN_ENABLED: booleanSchema.default('true'),
AUTH_EMAIL_CODE_LOGIN_ENABLED: booleanSchema.default('false'),

/**
* Relative time from now in seconds when to expire the session.
*
* @default 30 days
*/
AUTH_SESSION_MAX_AGE_SECONDS: numberSchema.default(60 * 60 * 24 * 30),

/**
* How often the session should be updated in seconds. If set to 0, session is updated every time.
*
* @default 1 day
*/
AUTH_SESSION_UPDATE_AGE_SECONDS: numberSchema.default(60 * 60 * 24),

/**
* Lifetime of an OAuth authorization code, in seconds.
*
* @default 10 minutes
*/
OAUTH_AUTHORIZATION_CODE_TTL_SECONDS: numberSchema.default(60 * 10),

/**
* Lifetime of an OAuth access token, in seconds.
*
* @default 1 hour
*/
OAUTH_ACCESS_TOKEN_TTL_SECONDS: numberSchema.default(60 * 60),

/**
* Lifetime of an OAuth refresh token, in seconds.
*
* @default 90 days
*/
OAUTH_REFRESH_TOKEN_TTL_SECONDS: numberSchema.default(60 * 60 * 24 * 90),
Comment thread
brendan-kellam marked this conversation as resolved.

// Enterprise Auth
AUTH_EE_ALLOW_EMAIL_ACCOUNT_LINKING:
booleanSchema
Expand Down
8 changes: 4 additions & 4 deletions packages/web/src/app/api/(server)/ee/oauth/token/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { verifyAndExchangeCode, verifyAndRotateRefreshToken, ACCESS_TOKEN_TTL_SECONDS } from '@/ee/features/oauth/server';
import { verifyAndExchangeCode, verifyAndRotateRefreshToken } from '@/ee/features/oauth/server';
import { apiHandler } from '@/lib/apiHandler';
import { hasEntitlement } from '@sourcebot/shared';
import { env, hasEntitlement } from '@sourcebot/shared';
import { NextRequest } from 'next/server';
import { OAUTH_NOT_SUPPORTED_ERROR_MESSAGE } from '@/ee/features/oauth/constants';

Expand Down Expand Up @@ -59,7 +59,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
access_token: result.token,
refresh_token: result.refreshToken,
token_type: 'Bearer',
expires_in: ACCESS_TOKEN_TTL_SECONDS,
expires_in: env.OAUTH_ACCESS_TOKEN_TTL_SECONDS,
scope: '',
});
}
Expand Down Expand Up @@ -91,7 +91,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
access_token: result.token,
refresh_token: result.refreshToken,
token_type: 'Bearer',
expires_in: ACCESS_TOKEN_TTL_SECONDS,
expires_in: env.OAUTH_ACCESS_TOKEN_TTL_SECONDS,
scope: '',
});
}
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: EncryptedPrismaAdapter(__unsafePrisma),
session: {
strategy: "jwt",
maxAge: env.AUTH_SESSION_MAX_AGE_SECONDS,
updateAge: env.AUTH_SESSION_UPDATE_AGE_SECONDS,
},
trustHost: true,
events: {
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/ee/features/oauth/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ vi.mock('@sourcebot/shared', () => ({
generateOAuthRefreshToken: vi.fn(() => ({ token: 'sbor_newrefresh', hash: 'newrefresh' })),
OAUTH_ACCESS_TOKEN_PREFIX: 'sboa_',
OAUTH_REFRESH_TOKEN_PREFIX: 'sbor_',
env: {
OAUTH_AUTHORIZATION_CODE_TTL_SECONDS: 60 * 10,
OAUTH_ACCESS_TOKEN_TTL_SECONDS: 60 * 60,
OAUTH_REFRESH_TOKEN_TTL_SECONDS: 60 * 60 * 24 * 90,
},
}));

const VALID_CODE_HASH = 'validcode';
Expand Down
21 changes: 8 additions & 13 deletions packages/web/src/ee/features/oauth/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'server-only';
import { __unsafePrisma } from '@/prisma';
import { Prisma } from '@prisma/client';
import {
env,
generateOAuthRefreshToken,
generateOAuthToken,
hashSecret,
Expand All @@ -11,12 +12,6 @@ import {
} from '@sourcebot/shared';
import crypto from 'crypto';

const AUTH_CODE_TTL_MS = 10 * 60 * 1000; // 10 minutes
const ACCESS_TOKEN_TTL_MS = 60 * 60 * 1000; // 1 hour
const REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1000; // 90 days

export const ACCESS_TOKEN_TTL_SECONDS = Math.floor(ACCESS_TOKEN_TTL_MS / 1000);

// Generates a random authorization code, hashes it, and stores it alongside the
// PKCE code challenge. Returns the raw code to be sent to the client.
export async function generateAndStoreAuthCode({
Expand All @@ -43,7 +38,7 @@ export async function generateAndStoreAuthCode({
redirectUri,
codeChallenge,
resource,
expiresAt: new Date(Date.now() + AUTH_CODE_TTL_MS),
expiresAt: new Date(Date.now() + env.OAUTH_AUTHORIZATION_CODE_TTL_SECONDS * 1000),
},
});

Expand Down Expand Up @@ -124,7 +119,7 @@ export async function verifyAndExchangeCode({
clientId,
userId: authCode.userId,
resource: authCode.resource,
expiresAt: new Date(Date.now() + ACCESS_TOKEN_TTL_MS),
expiresAt: new Date(Date.now() + env.OAUTH_ACCESS_TOKEN_TTL_SECONDS * 1000),
},
}),
__unsafePrisma.oAuthRefreshToken.create({
Expand All @@ -133,12 +128,12 @@ export async function verifyAndExchangeCode({
clientId,
userId: authCode.userId,
resource: authCode.resource,
expiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL_MS),
expiresAt: new Date(Date.now() + env.OAUTH_REFRESH_TOKEN_TTL_SECONDS * 1000),
},
}),
]);

return { token, refreshToken, expiresIn: ACCESS_TOKEN_TTL_SECONDS };
return { token, refreshToken, expiresIn: env.OAUTH_ACCESS_TOKEN_TTL_SECONDS };
}

// Verifies a refresh token, rotates it, and issues a new access token + refresh token.
Expand Down Expand Up @@ -189,7 +184,7 @@ export async function verifyAndRotateRefreshToken({
clientId,
userId: existing.userId,
resource: existing.resource,
expiresAt: new Date(Date.now() + ACCESS_TOKEN_TTL_MS),
expiresAt: new Date(Date.now() + env.OAUTH_ACCESS_TOKEN_TTL_SECONDS * 1000),
},
}),
__unsafePrisma.oAuthRefreshToken.create({
Expand All @@ -198,12 +193,12 @@ export async function verifyAndRotateRefreshToken({
clientId,
userId: existing.userId,
resource: existing.resource,
expiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL_MS),
expiresAt: new Date(Date.now() + env.OAUTH_REFRESH_TOKEN_TTL_SECONDS * 1000),
},
}),
]);

return { token, refreshToken, expiresIn: ACCESS_TOKEN_TTL_SECONDS };
return { token, refreshToken, expiresIn: env.OAUTH_ACCESS_TOKEN_TTL_SECONDS };
}

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