Sign in with open-git
Put a “Sign in with open-git” button on your site. The user consents, you
receive a stable user id (sub), and you create or log in your customer.
This does not let you read or write repositories. After they install your bot, use an installation token for that.
Register an OAuth app
In the organization that owns the bot: Organization settings → Developer → New OAuth app.
You need:
- A name (shown on the consent screen)
- At least one redirect URI, for example
https://your-app.com/oauth/open-git/callback - Homepage URL (optional)
Creating a bot with a Setup URL or Redirect URI also creates a linked OAuth app, unless you attach one you already have. The registered callback is the Redirect URI if you set one, otherwise the Setup URL. Use that client for connect-after-install. Saving the bot again adds that callback to the client’s redirect list and leaves any extra callbacks in place. For a callback that is not the Setup URL, set Redirect URI on the bot — that is not the webhook URL.
Store client_id and client_secret. The secret is shown once; rotate it
from the same page if you lose it. The create-bot dump names them
OPEN_GIT_OAUTH_CLIENT_ID and OPEN_GIT_OAUTH_CLIENT_SECRET.
const OPEN_GIT_URL = process.env.OPEN_GIT_URL ?? "https://open-git.com"
const OPEN_GIT_OAUTH_CLIENT_ID = process.env.OPEN_GIT_OAUTH_CLIENT_ID!
const OPEN_GIT_OAUTH_CLIENT_SECRET = process.env.OPEN_GIT_OAUTH_CLIENT_SECRET!
const REDIRECT_URI = "https://your-app.com/oauth/open-git/callback"
The redirect URI you send in the authorize request must match one you
registered, character for character. A mismatch returns you to open-git
with invalid_redirect and a visible error — it is not the webhook URL.
Allowed identity scopes: openid, profile, email.
Start the sign-in (send them to open-git)
@opengit/sdk discovers the endpoints, builds PKCE (S256), and returns
the authorize URL. You still persist state + code_verifier (and
installation_id if they came from an install). That store is yours — a
cookie, session, or short-lived row, one-time use.
Discovery reads /.well-known/openid-configuration. If those three
endpoints are missing, the SDK falls back to
/api/auth/oauth2/authorize, /api/auth/oauth2/token, and
/api/auth/oauth2/userinfo. /.well-known/oauth-authorization-server is
also published if you want to read it yourself.
import { startOpenGitOAuth } from "@opengit/sdk"
export async function beginSignIn(options: {
installationId?: string | null
}) {
const started = await startOpenGitOAuth({
clientId: OPEN_GIT_OAUTH_CLIENT_ID,
origin: OPEN_GIT_URL,
redirectUri: REDIRECT_URI,
})
await saveOAuthPending(started.state, {
codeVerifier: started.codeVerifier,
installationId: options.installationId ?? null,
})
return started.url
}
Standalone button:
app.get("/sign-in/open-git", async (_req, res) => {
res.redirect(await beginSignIn({}))
})
Setup URL after install (https://your-app.com/connect?installation_id=…):
app.get("/connect", async (req, res) => {
const installationId =
typeof req.query.installation_id === "string"
? req.query.installation_id
: null
res.redirect(await beginSignIn({ installationId }))
})
The user signs in (or creates an account), then sees your app name and the identity scopes. Consent copy says this does not grant repository access. A disabled client, or a client linked to an unpublished bot, shows “This application is unavailable.”
Redirect URI (the callback)
open-git sends them back to your registered redirect:
https://your-app.com/oauth/open-git/callback?code=…&state=…
Verify state, then let the SDK exchange code and load userinfo.
Create or look up your customer by sub. If you stored an installation_id
with that state, attach it to the customer.
import { exchangeOpenGitCode } from "@opengit/sdk"
export async function handleOpenGitCallback(request: Request) {
const url = new URL(request.url)
const code = url.searchParams.get("code")
const state = url.searchParams.get("state")
const oauthError = url.searchParams.get("error")
if (oauthError) {
throw new Error(url.searchParams.get("error_description") ?? oauthError)
}
if (!code || !state) {
throw new Error("Missing code or state.")
}
const pending = await takeOAuthPending(state)
if (!pending) {
throw new Error("OAuth state was missing or already used.")
}
const { user } = await exchangeOpenGitCode({
clientId: OPEN_GIT_OAUTH_CLIENT_ID,
clientSecret: OPEN_GIT_OAUTH_CLIENT_SECRET,
code,
codeVerifier: pending.codeVerifier,
origin: OPEN_GIT_URL,
redirectUri: REDIRECT_URI,
})
const customer = await upsertCustomer({
openGitUserId: user.sub,
email: user.email ?? null,
username: user.username ?? user.preferred_username ?? null,
installationId: pending.installationId,
})
return customer
}
A Next.js App Router route at app/oauth/open-git/callback/route.ts:
import { NextResponse } from "next/server"
export async function GET(request: Request) {
try {
const customer = await handleOpenGitCallback(request)
return NextResponse.redirect(
new URL(`/dashboard?customer=${customer.id}`, request.url)
)
} catch (error) {
const message = error instanceof Error ? error.message : "Sign-in failed"
return NextResponse.redirect(
new URL(`/sign-in?error=${encodeURIComponent(message)}`, request.url)
)
}
}
| Claim | Meaning |
|---|---|
sub |
Stable account id from userinfo. Store this. Do not key off username. |
username / preferred_username |
Current handle, added when profile is granted. Can change. |
email |
Requested with the email scope. Treat as optional; key the customer on sub. |
saveOAuthPending / takeOAuthPending / upsertCustomer are yours — a
cookie, Redis key, or database row keyed by state, deleted after one use.
Two ways people arrive
They start on your site. Sign in with open-git → you have a customer →
later they install the bot → your webhook includes installation_id → you
attach it to the customer you already know.
They start on the marketplace. They install first. If One install +
Sign in screen is on, that page grants bot access and identity together.
We send them to your Setup URL with ?installation_id=… unless this
install is already connected. Start the same OAuth flow from your origin
(PKCE has to live on your app). The second consent page is skipped. Save
sub + installation_id. Optionally
link so Connections can show
“connected”.
If they install and never finish Sign in, the grant still exists. You simply do not have a customer yet — do not run paid work until you do.
What this token is not
The OAuth access token is identity only. It cannot list pull requests, post checks, or comment. Mint an installation token for that.
Users can revoke the consent under Settings → Connections → Authorized
applications. Revoke access does not uninstall the bot. If the OAuth
app is tied to an install they can manage, we clear Connected
(external_customer_id) on those installs. Installation tokens and
webhooks keep working until they uninstall. They can also choose
Revoke and uninstall, which revokes the consent and uninstalls every
install of that bot they can manage — you get installation.deleted for
each, the same as a normal uninstall. If they revoke, stop treating them
as signed in on your side.