Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed
- Fixed issue where certain file and folder names would cause type errors. [#862](https://github.com/sourcebot-dev/sourcebot/pull/862)
- 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)
Comment thread
brendan-kellam marked this conversation as resolved.

## [4.10.27] - 2026-02-05

Expand Down
153 changes: 101 additions & 52 deletions packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,93 @@ export async function refreshLinkedAccountTokens(userId: string): Promise<Linked
return errors;
}

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;
}
Comment thread
brendan-kellam marked this conversation as resolved.

// 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,
};
}
Comment thread
brendan-kellam marked this conversation as resolved.
Outdated

export async function refreshOAuthToken(
provider: string,
refreshToken: string,
Expand All @@ -92,7 +179,17 @@ 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;
}
Expand All @@ -110,60 +207,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