fix: support gists through GitHub App login (#875)

* fix: prompt OAuth reconnection when gist credentials are rejected

* fix: support gist reads through GitHub App user grants

* test: adapt truncated gist regression to provider selection

* fix: address gist reconnect visibility and token renewal review

* fix: verify App authorization before revoking OAuth
This commit is contained in:
Thomas Durieux authored and GitHub committed 2026-10-08 10:26:13 +01:00
1 parent 4fd31a59c8
commit db966356ec
14 files changed
+283 -54

No files matched your search

+30 -6
View File
@@ -1,6 +1,7 @@
import { registerGitHubToken } from "./github-token-context";
import { isConnected } from "../server/database";
import { expireEmbeddedContent } from "./content-expiration";
import { APP_PROVIDER, appError } from "./github-app";
import { APP_PROVIDER, appUserToken } from "./github-app";
import CredentialModel from "./model/credentials/credentials.model";
import { getCredentialToken } from "./credentials";
import { RepositoryStatus } from "./types";
@@ -50,16 +51,39 @@ export default class Gist {
this.owner.model.isNew = false;
}
async getToken() {
if (config.GITHUB_APP_ENABLED && !(await getCredentialToken(this.owner.id)) && await CredentialModel.exists({ ownerId: this.owner.id, provider: APP_PROVIDER })) {
throw appError("github_oauth_required");
private async getAccess() {
if (config.GITHUB_APP_ENABLED && await CredentialModel.exists({ ownerId: this.owner.id, provider: APP_PROVIDER })) {
const ownerId = this.owner.id;
const token = await appUserToken(ownerId);
registerGitHubToken(token, { quotaKey: `app-user:${ownerId}`, renew: () => appUserToken(ownerId) });
return { token, connection: "github-app" };
}
return (await getCredentialToken(this.owner.id, "github", { collection: "anonymizedgists", id: this._model._id })) || config.GITHUB_TOKEN;
return { token: (await getCredentialToken(this.owner.id, "github", { collection: "anonymizedgists", id: this._model._id })) || config.GITHUB_TOKEN,
connection: "oauth" };
}
async getToken() {
return (await this.getAccess()).token;
}
async download() {
const access = await this.getAccess();
try {
await this.downloadContent(access.token);
} catch (error) {
// A stored credential can exist but have been revoked on GitHub.
// Ask the user to reconnect the provider that rejected the credential.
if ((error as { status?: number })?.status === 401) {
throw new AnonymousError(access.connection === "github-app" ? "github_app_reconnect_required" : "github_oauth_required",
{ httpStatus: 403, cause: error as Error });
}
throw error;
}
}
private async downloadContent(token: string) {
logger.info("downloading gist", { gistId: this._model.source.gistId });
const oct = octokit(await this.getToken());
const oct = octokit(token);
const gist_id = this._model.source.gistId;
+4 -2
View File
@@ -168,6 +168,7 @@ router.get("/connections", async (req, res) => {
const prs = await PullRequestModel.find({ owner: user.id, status: { $ne: "removed" } }).select("pullRequestId source.repositoryFullName githubAccess status statusDate").lean();
const gistCount = await GistModel.countDocuments({ owner: user.id, status: { $ne: "removed" } });
res.json({ csrf: req.session.githubConnectionCSRF, appEnabled: config.GITHUB_APP_ENABLED && config.GITHUB_APP_NEW_CONNECTIONS,
gistConnection: config.GITHUB_APP_ENABLED && credentials.some(c => c.provider === APP_PROVIDER) ? "github-app" : "oauth",
oauthEnabled: config.GITHUB_OAUTH_ENABLED, oauthConnected: !!(await getCredentialToken(user.id)), appConnected, appError: appErrorCode,
installations, gistCount, resources: [
...repos.map(r => ({ type: "repository", id: r.repoId, name: r.source.repositoryName, connection: r.githubAccess?.kind || "oauth", status: r.status, statusDate: r.statusDate })),
@@ -246,9 +247,10 @@ router.post("/connections/disconnect-oauth", async (req, res) => {
try {
const user = await getUser(req);
if (!(await CredentialModel.exists({ ownerId: user.id, provider: APP_PROVIDER, revoked: { $ne: true } }))) throw appError("another_login_required", 409);
await appUserToken(user.id);
// Confirm the remaining login still works on GitHub before revoking OAuth.
await userInstallations(user.id);
const active = { owner: user.id, status: { $ne: "removed" }, "githubAccess.kind": { $ne: "github-app" } };
if (await RepositoryModel.exists(active) || await PullRequestModel.exists(active) || await GistModel.exists({ owner: user.id, status: { $ne: "removed" } })) {
if (await RepositoryModel.exists(active) || await PullRequestModel.exists(active)) {
throw appError("oauth_resources_remaining", 409);
}
await revokeGrant(user.id, "github");