Authentication
View on GitHubAuthentication
Password auth (AuthService, RegisteredUserController,
AuthenticatedSessionController) plus an OAuth login pathway, so a
user can sign in with an external identity provider instead of (or in
addition to) an Orbit password. GitHub is the first provider wired up
(v0.9.5); Google and Microsoft are shown but disabled in
SocialLoginButtons until they're wired the same way.
Guides, in the order you'd actually need them
- Add an OAuth providerAuthenticationAdd an OAuth providerWorked example: wiring up Google the same way GitHub is wired today, so SocialLoginButtons can stop disabling it. Since orbit-api is the centralized sign-i… — worked example wiring up Google the same way GitHub is wired today.
The architecture in one paragraph
Orbit Local never talks to GitHub's OAuth endpoints and never holds a
GitHub OAuth App client secret - orbit-api is a centralized sign-in
broker, shared by every self-hosted Orbit Local instance, using the
one GitHub App/OAuth client it already has installed for the
repository integration (see
../integrations/06-github-integration.mdIntegrationsThe GitHub integrationUnlike the notify (webhook) and import (pull) integrations covered in the other guides in this category, GitHub is a third kind: a GitHub App installation,…).
This means a self-hosted deployment gets "Sign in with GitHub" for
free, without its operator ever registering their own GitHub OAuth
App. The hand-off between the two codebases (src/auth/github-login
in orbit-api, GithubAuthController in Local) is:
- Local's
redirect()generates its own CSRFstate, stores it in the session, and redirects the browser to orbit-api'sGET /v1/auth/github/redirect?return_to=<Local's own callback URL, state included>. - orbit-api's
GitHubLoginService::start()stores thatreturn_toagainst a second, orbit-api-ownedstate(its own CSRF protection for the GitHub round-trip - Local'sstateis just an opaque part of the URL to orbit-api at this point) and redirects to GitHub's real authorize URL. - GitHub redirects back to orbit-api's fixed
GITHUB_LOGIN_CALLBACK_URL.GitHubLoginService::callback()exchanges the code, fetches the profile (GET /user, falling back toGET /user/emailsfor a private email - seegithub-user.client.ts), mints a random, single-useexchange_tokenvalid for 60 seconds, and redirects the browser back to the storedreturn_to(Local's own callback, itsstatestill attached) withexchange_tokenappended. - Local's
callback()checks its ownstateagainst the session (standard CSRF check, unrelated to step 2's), then callsOrbitRelayClient::resolveGithubLoginToken($exchangeToken)- server-to-server, unauthenticated (possession of the token is itself the authorization, exactly like an OAuth code) - which redeems the token exactly once for aGithubIdentityDTO(githubId,githubUsername,email,name). A GitHub access token never reaches Local, and the exchange_token itself is useless after its first (and only) redemption.
App\Services\GithubOAuthService::loginOrRegister()/linkToUser()/unlink()
are unchanged by any of this - they only ever see a GithubIdentityDTO,
never knowing or caring whether it came from Socialite directly or
through the broker. loginOrRegister() resolves, in order: an existing
github_id match (sign in), a matching verified email on an unlinked
account (auto-link - GitHub already proved the caller owns that
email), or, failing both, a brand-new account with no password
(users.password is nullable for exactly this reason).
This is a separate identity from the GitHub App integration
(app/Services/Integrations/Github). That one is project-scoped and
grants repository access via an installation token; this one is
user-scoped and only proves "this Orbit account is owned by this
GitHub account." Neither implies the other - a project can be
connected to GitHub with nobody's personal account linked, and a user
can link their GitHub account without their project having any GitHub
integration at all.
Using the link as a gate elsewhere
A linked GitHub account is also used as a precondition for an
unrelated action: creating a branch or pull request from an issue
(IssueGithubDevelopmentController::assertGithubAccountLinked())
requires $user->github_id !== null, checked separately from the
createGithubDevelopment permission (IssuePolicy) — permission asks
"is your role allowed to," this asks "did you prove who you are on
GitHub." Follow this pattern (a small private guard method throwing
ValidationException with a field key the frontend already expects,
e.g. branch/pullRequest) rather than folding an identity check into
a policy, if a future feature needs the same kind of gate.
Unlinking safety
GithubOAuthService::unlink() refuses to clear github_id when
users.password is null — a GitHub-only account would otherwise be
permanently locked out. The Security & Access settings page
(AccountSettingsSecurityTab) disables its "Unlink" button for the
same reason, but the backend check is the one that actually matters;
the frontend disable is just UX.
