Skip to content

Commit fce3218

Browse files
fix
1 parent f30cd76 commit fce3218

1 file changed

Lines changed: 102 additions & 53 deletions

File tree

packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts

Lines changed: 102 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,93 @@ export async function refreshLinkedAccountTokens(
7676
return updatedTokens;
7777
}
7878

79+
type ProviderCredentials = {
80+
clientId: string;
81+
clientSecret: string;
82+
baseUrl?: string;
83+
};
84+
85+
/**
86+
* Get credentials from deprecated environment variables.
87+
* This is for backwards compatibility with deployments using env vars instead of config file.
88+
*/
89+
function getDeprecatedEnvCredentials(provider: string): ProviderCredentials | null {
90+
if (provider === 'github' && env.AUTH_EE_GITHUB_CLIENT_ID && env.AUTH_EE_GITHUB_CLIENT_SECRET) {
91+
return {
92+
clientId: env.AUTH_EE_GITHUB_CLIENT_ID,
93+
clientSecret: env.AUTH_EE_GITHUB_CLIENT_SECRET,
94+
baseUrl: env.AUTH_EE_GITHUB_BASE_URL,
95+
};
96+
}
97+
if (provider === 'gitlab' && env.AUTH_EE_GITLAB_CLIENT_ID && env.AUTH_EE_GITLAB_CLIENT_SECRET) {
98+
return {
99+
clientId: env.AUTH_EE_GITLAB_CLIENT_ID,
100+
clientSecret: env.AUTH_EE_GITLAB_CLIENT_SECRET,
101+
baseUrl: env.AUTH_EE_GITLAB_BASE_URL,
102+
};
103+
}
104+
return null;
105+
}
106+
107+
async function tryRefreshToken(
108+
provider: string,
109+
refreshToken: string,
110+
credentials: ProviderCredentials
111+
): Promise<{ accessToken: string; refreshToken: string | null; expiresAt: number } | null> {
112+
const { clientId, clientSecret, baseUrl } = credentials;
113+
114+
let url: string;
115+
if (baseUrl) {
116+
url = provider === 'github'
117+
? `${baseUrl}/login/oauth/access_token`
118+
: `${baseUrl}/oauth/token`;
119+
} else if (provider === 'github') {
120+
url = 'https://github.com/login/oauth/access_token';
121+
} else if (provider === 'gitlab') {
122+
url = 'https://gitlab.com/oauth/token';
123+
} else {
124+
logger.error(`Unsupported provider for token refresh: ${provider}`);
125+
return null;
126+
}
127+
128+
// Build request body parameters
129+
const bodyParams: Record<string, string> = {
130+
client_id: clientId,
131+
client_secret: clientSecret,
132+
grant_type: 'refresh_token',
133+
refresh_token: refreshToken,
134+
};
135+
136+
// GitLab requires redirect_uri to match the original authorization request
137+
// even when refreshing tokens. Use URL constructor to handle trailing slashes.
138+
if (provider === 'gitlab') {
139+
bodyParams.redirect_uri = new URL('/api/auth/callback/gitlab', env.AUTH_URL).toString();
140+
}
141+
142+
const response = await fetch(url, {
143+
method: 'POST',
144+
headers: {
145+
'Content-Type': 'application/x-www-form-urlencoded',
146+
'Accept': 'application/json',
147+
},
148+
body: new URLSearchParams(bodyParams),
149+
});
150+
151+
if (!response.ok) {
152+
const errorText = await response.text();
153+
logger.debug(`Failed to refresh ${provider} token: ${response.status} ${errorText}`);
154+
return null;
155+
}
156+
157+
const data = await response.json();
158+
159+
return {
160+
accessToken: data.access_token,
161+
refreshToken: data.refresh_token ?? null,
162+
expiresAt: data.expires_in ? Math.floor(Date.now() / 1000) + data.expires_in : 0,
163+
};
164+
}
165+
79166
export async function refreshOAuthToken(
80167
provider: string,
81168
refreshToken: string,
@@ -85,14 +172,24 @@ export async function refreshOAuthToken(
85172
const identityProviders = config?.identityProviders ?? [];
86173

87174
const providerConfigs = identityProviders.filter(idp => idp.provider === provider);
175+
176+
// If no provider configs in the config file, try deprecated env vars
88177
if (providerConfigs.length === 0) {
178+
const envCredentials = getDeprecatedEnvCredentials(provider);
179+
if (envCredentials) {
180+
logger.debug(`Using deprecated env vars for ${provider} token refresh`);
181+
const result = await tryRefreshToken(provider, refreshToken, envCredentials);
182+
if (result) {
183+
return result;
184+
}
185+
}
89186
logger.error(`Provider config not found or invalid for: ${provider}`);
90187
return null;
91188
}
92189

93190
// Loop through all provider configs and return on first successful fetch
94191
//
95-
// 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
192+
// 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
96193
// 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
97194
// 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
98195
// 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
@@ -103,60 +200,12 @@ export async function refreshOAuthToken(
103200
const linkedAccountProviderConfig = providerConfig as GitHubIdentityProviderConfig | GitLabIdentityProviderConfig
104201
const clientId = await getTokenFromConfig(linkedAccountProviderConfig.clientId);
105202
const clientSecret = await getTokenFromConfig(linkedAccountProviderConfig.clientSecret);
106-
const baseUrl = linkedAccountProviderConfig.baseUrl
107-
108-
let url: string;
109-
if (baseUrl) {
110-
url = provider === 'github'
111-
? `${baseUrl}/login/oauth/access_token`
112-
: `${baseUrl}/oauth/token`;
113-
} else if (provider === 'github') {
114-
url = 'https://github.com/login/oauth/access_token';
115-
} else if (provider === 'gitlab') {
116-
url = 'https://gitlab.com/oauth/token';
117-
} else {
118-
logger.error(`Unsupported provider for token refresh: ${provider}`);
119-
continue;
120-
}
121-
122-
// Build request body parameters
123-
const bodyParams: Record<string, string> = {
124-
client_id: clientId,
125-
client_secret: clientSecret,
126-
grant_type: 'refresh_token',
127-
refresh_token: refreshToken,
128-
};
129-
130-
// GitLab requires redirect_uri to match the original authorization request
131-
// even when refreshing tokens. Use URL constructor to handle trailing slashes.
132-
if (provider === 'gitlab') {
133-
bodyParams.redirect_uri = new URL('/api/auth/callback/gitlab', env.AUTH_URL).toString();
134-
}
203+
const baseUrl = linkedAccountProviderConfig.baseUrl;
135204

136-
const response = await fetch(url, {
137-
method: 'POST',
138-
headers: {
139-
'Content-Type': 'application/x-www-form-urlencoded',
140-
'Accept': 'application/json',
141-
},
142-
body: new URLSearchParams(bodyParams),
143-
});
144-
145-
if (!response.ok) {
146-
const errorText = await response.text();
147-
logger.debug(`Failed to refresh ${provider} token with config: ${response.status} ${errorText}`);
148-
continue;
205+
const result = await tryRefreshToken(provider, refreshToken, { clientId, clientSecret, baseUrl });
206+
if (result) {
207+
return result;
149208
}
150-
151-
const data = await response.json();
152-
153-
const result = {
154-
accessToken: data.access_token,
155-
refreshToken: data.refresh_token ?? null,
156-
expiresAt: data.expires_in ? Math.floor(Date.now() / 1000) + data.expires_in : 0,
157-
};
158-
159-
return result;
160209
} catch (configError) {
161210
logger.debug(`Error trying provider config for ${provider}:`, configError);
162211
continue;

0 commit comments

Comments
 (0)