Skip to content
Draft
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- Fixed token refresh error "Provider config not found or invalid for: x" when a sso is configured using deprecated env vars. [#841](https://github.com/sourcebot-dev/sourcebot/pull/841)

## [4.10.21] - 2026-02-02

### Added
Expand Down
155 changes: 102 additions & 53 deletions packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,93 @@ export async function refreshLinkedAccountTokens(
return updatedTokens;
}

type ProviderCredentials = {
clientId: string;
clientSecret: string;
baseUrl?: string;
};

/**
* Get credentials from deprecated environment variables.
* This is for backwards compatibility with deployments using env vars instead of config file.
*/
function getDeprecatedEnvCredentials(provider: string): ProviderCredentials | null {
if (provider === 'github' && env.AUTH_EE_GITHUB_CLIENT_ID && env.AUTH_EE_GITHUB_CLIENT_SECRET) {
return {
clientId: env.AUTH_EE_GITHUB_CLIENT_ID,
clientSecret: env.AUTH_EE_GITHUB_CLIENT_SECRET,
baseUrl: env.AUTH_EE_GITHUB_BASE_URL,
};
}
if (provider === 'gitlab' && env.AUTH_EE_GITLAB_CLIENT_ID && env.AUTH_EE_GITLAB_CLIENT_SECRET) {
return {
clientId: env.AUTH_EE_GITLAB_CLIENT_ID,
clientSecret: env.AUTH_EE_GITLAB_CLIENT_SECRET,
baseUrl: env.AUTH_EE_GITLAB_BASE_URL,
};
}
return null;
}

async function tryRefreshToken(
provider: string,
refreshToken: string,
credentials: ProviderCredentials
): Promise<{ accessToken: string; refreshToken: string | null; expiresAt: number } | null> {
const { clientId, clientSecret, baseUrl } = credentials;

let url: string;
if (baseUrl) {
url = provider === 'github'
? `${baseUrl}/login/oauth/access_token`
: `${baseUrl}/oauth/token`;
} else if (provider === 'github') {
url = 'https://github.com/login/oauth/access_token';
} else if (provider === 'gitlab') {
url = 'https://gitlab.com/oauth/token';
} else {
logger.error(`Unsupported provider for token refresh: ${provider}`);
return null;
}

// Build request body parameters
const bodyParams: Record<string, string> = {
client_id: clientId,
client_secret: clientSecret,
grant_type: 'refresh_token',
refresh_token: refreshToken,
};

// GitLab requires redirect_uri to match the original authorization request
// even when refreshing tokens. Use URL constructor to handle trailing slashes.
if (provider === 'gitlab') {
bodyParams.redirect_uri = new URL('/api/auth/callback/gitlab', env.AUTH_URL).toString();
}

const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
},
body: new URLSearchParams(bodyParams),
});

if (!response.ok) {
const errorText = await response.text();
logger.debug(`Failed to refresh ${provider} token: ${response.status} ${errorText}`);
return null;
}

const data = await response.json();

return {
accessToken: data.access_token,
refreshToken: data.refresh_token ?? null,
expiresAt: data.expires_in ? Math.floor(Date.now() / 1000) + data.expires_in : 0,
};
}

export async function refreshOAuthToken(
provider: string,
refreshToken: string,
Expand All @@ -85,14 +172,24 @@ export async function refreshOAuthToken(
const identityProviders = config?.identityProviders ?? [];

const providerConfigs = identityProviders.filter(idp => idp.provider === provider);

// If no provider configs in the config file, try deprecated env vars
if (providerConfigs.length === 0) {
const envCredentials = getDeprecatedEnvCredentials(provider);
if (envCredentials) {
logger.debug(`Using deprecated env vars for ${provider} token refresh`);
const result = await tryRefreshToken(provider, refreshToken, envCredentials);
if (result) {
return result;
}
}
logger.error(`Provider config not found or invalid for: ${provider}`);
return null;
}

// Loop through all provider configs and return on first successful fetch
//
// The reason we have to do this is because 1) we might have multiple providers of the same type (ex. we're connecting to multiple gitlab instances) and 2) there isn't
// The reason we have to do this is because 1) we might have multiple providers of the same type (ex. we're connecting to multiple gitlab instances) and 2) there isn't
// a trivial way to map a provider config to the associated Account object in the DB. The reason the config is involved at all here is because we need the client
// id/secret in order to refresh the token, and that info is in the config. We could in theory bypass this by storing the client id/secret for the provider in the
// Account table but we decided not to do that since these are secret. Instead, we simply try all of the client/id secrets for the associated provider type. This is safe
Expand All @@ -103,60 +200,12 @@ export async function refreshOAuthToken(
const linkedAccountProviderConfig = providerConfig as GitHubIdentityProviderConfig | GitLabIdentityProviderConfig
const clientId = await getTokenFromConfig(linkedAccountProviderConfig.clientId);
const clientSecret = await getTokenFromConfig(linkedAccountProviderConfig.clientSecret);
const baseUrl = linkedAccountProviderConfig.baseUrl

let url: string;
if (baseUrl) {
url = provider === 'github'
? `${baseUrl}/login/oauth/access_token`
: `${baseUrl}/oauth/token`;
} else if (provider === 'github') {
url = 'https://github.com/login/oauth/access_token';
} else if (provider === 'gitlab') {
url = 'https://gitlab.com/oauth/token';
} else {
logger.error(`Unsupported provider for token refresh: ${provider}`);
continue;
}

// Build request body parameters
const bodyParams: Record<string, string> = {
client_id: clientId,
client_secret: clientSecret,
grant_type: 'refresh_token',
refresh_token: refreshToken,
};

// GitLab requires redirect_uri to match the original authorization request
// even when refreshing tokens. Use URL constructor to handle trailing slashes.
if (provider === 'gitlab') {
bodyParams.redirect_uri = new URL('/api/auth/callback/gitlab', env.AUTH_URL).toString();
}
const baseUrl = linkedAccountProviderConfig.baseUrl;

const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
},
body: new URLSearchParams(bodyParams),
});

if (!response.ok) {
const errorText = await response.text();
logger.debug(`Failed to refresh ${provider} token with config: ${response.status} ${errorText}`);
continue;
const result = await tryRefreshToken(provider, refreshToken, { clientId, clientSecret, baseUrl });
if (result) {
return result;
}

const data = await response.json();

const result = {
accessToken: data.access_token,
refreshToken: data.refresh_token ?? null,
expiresAt: data.expires_in ? Math.floor(Date.now() / 1000) + data.expires_in : 0,
};

return result;
} catch (configError) {
logger.debug(`Error trying provider config for ${provider}:`, configError);
continue;
Expand Down