A complete guide to creating and installing an external GitHub OAuth plugin for Suprnova, configuring a GitHub OAuth App, registering the provider, and handling the callback safely.
--
Build GitHub sign-in into a Suprnova application with a real external OAuth provider, verified primary email handling, PKCE, safe account linking, and application-grant revocation.
GitHub sign-in looks like a small OAuth integration until you reach the email address. The basic profile endpoint returns an email only when the user made it public, and public does not mean verified. If an authentication plugin quietly treats that field as proof of account ownership, a convenient login button becomes an account-linking vulnerability.
The external suprnova-oauth-github plugin handles that boundary explicitly. It uses GitHub's stable numeric user ID as the external identity, fetches the authenticated user's email list, and accepts an address only when GitHub marks it both primary and verified.
It is also a useful example of how to build against Suprnova's public OAuth SDK. The plugin lives in its own repository, depends on the published Suprnova v1.3.2 tag, and imports everything through suprnova::. There is no path dependency, workspace membership, direct Magnetar dependency, or private framework hook.
This guide installs the plugin, creates the GitHub OAuth App, registers the provider, and adds the two application routes needed to sign in.
What the plugin does
The plugin supplies two public pieces:
GitHubOAuthProviderimplements Suprnova's publicOAuthProvidercontract. It declares GitHub's endpoints and request shapes, maps a GitHub profile into a verified provider identity, and renders grant-revocation requests.GitHubOAuthTransportwraps Suprnova's publicOAuthHttpTransport. It performs the two GitHub REST requests needed for identity resolution and combines their responses before the provider parses them.
Suprnova still owns the security-sensitive parts of the OAuth ceremony:
- It generates and validates state.
- It generates the PKCE verifier and sends the
S256challenge. - It binds the ceremony to the initiating framework session.
- It adds the bearer
Authorizationheader to user-information requests. - It applies the configured account-linking and factor-gate policies.
- It creates the application session after the provider identity has been verified.
The provider never performs network I/O inside resolve_identity. The host transport performs the network calls, which keeps the plugin on the same public boundary used by every other external Suprnova provider.
You can learn how to build a plugin here: https://github.com/eas4ai/suprnova-oauth-github/blob/main/BUILDING.md
Why GitHub email needs two requests
GitHub's authenticated-user endpoint is:
GET https://api.github.com/user
That response includes a stable numeric id, the mutable login, and profile fields such as name. Its email field is populated only when the user made an address public. It does not carry a separate verification flag.
The plugin also requests:
GET https://api.github.com/user/emails?per_page=100
That endpoint requires the user:email scope and returns primary and verified for each address. The plugin accepts exactly one address matching both conditions:
A public but unverified address is ignored. A verified secondary address is also ignored. If GitHub does not return one verified primary address, the provider returns no email and Suprnova fails closed instead of guessing.
Create a GitHub OAuth App
Create one OAuth App for each environment whose callback URL differs.
-
Select OAuth Apps.
-
Select New OAuth App.
-
Enter your application's public URL as the Homepage URL.
-
Enter the callback route as the Authorization callback URL. For example:
https://app.example.com/auth/github/callback -
Create the app.
-
Generate a client secret.
-
Store the client ID and secret in your deployment's secret manager.
The callback URL in your application configuration must match the URL registered with GitHub.
Add the dependencies
Add the released plugin and the Suprnova SDK tag it targets:
[dependencies]
suprnova = { git = "https://github.com/eas4ai/suprnova.git", tag = "v1.3.2" }
suprnova-oauth-github = { git = "https://github.com/eas4ai/suprnova-oauth-github.git", tag = "v0.1.0" }
url = "2"
The url dependency decodes the callback query string in the controller below. The provider itself uses suprnova::SecretString, so the application does not need a direct Magnetar dependency.
Configure the environment
Add these values to your local environment and production secret configuration:
GITHUB_OAUTH_CLIENT_ID=your_github_oauth_client_id
GITHUB_OAUTH_CLIENT_SECRET=your_github_oauth_client_secret
GITHUB_OAUTH_REDIRECT_URI=https://app.example.com/auth/github/callback
GITHUB_OAUTH_USER_AGENT=example-app/1.0 (security@example.com)
GitHub requires a User-Agent on REST requests. Use a stable product identifier and a monitored contact address. Do not put credentials in the value.
Register GitHub during bootstrap
Register GitHub on the same MagnetarConfig that initializes the rest of your authentication stack. Suprnova builds the OAuth service first, then publishes password, passkey, and OAuth engines atomically. If any part of initialization fails, none of the engines becomes visible.
use env;
use Arc;
use ;
use ;
pub async
Call register_github_oauth().await during application bootstrap after the database, encryption key, session store, and rate-limiter driver have been registered. Call init_magnetar only once.
ReqwestOAuthTransport::try_default() gives the plugin the framework's production HTTP posture: redirects are disabled, requests time out after 30 seconds, responses are limited to 1 MiB, and a default Suprnova User-Agent is available. The provider-specific value from GITHUB_OAUTH_USER_AGENT is sent on GitHub REST requests.
FrameworkAbuseLimiter uses the application's configured RateLimiterDriver. Production deployments normally use the shared Redis driver, so OAuth start attempts are limited consistently across application processes.
Add the routes
The application owns the HTTP routes. Add one route to start the ceremony and one for GitHub's callback:
get!,
get!,
Apply SessionMiddleware to both routes. Suprnova binds the OAuth ceremony to a digest of the initiating session. Moving the callback to another browser session causes validation to fail.
Add the controller
Create src/controllers/github_oauth.rs:
use HashMap;
use ;
pub async
async
pub async
async
begin() persists a single-use ceremony, generates state and PKCE values, and returns GitHub's authorization URL. The controller only has to redirect the browser.
complete() exchanges the callback code, validates the session-bound state, fetches the GitHub profile and email list, resolves the linked identity, applies account-link and factor policy, rotates the framework session, and returns the application user and Magnetar session values.
What GitHub receives
The authorization redirect includes:
client_idredirect_uriscope=user:email- An unguessable
state - A PKCE
code_challenge code_challenge_method=S256
The token exchange sends the single-use code, the original redirect URI, the PKCE verifier, the client ID, and the client secret as a form-encoded request. It asks GitHub for a JSON response with Accept: application/json.
The profile and email requests include:
Authorization: Bearer <access-token>
User-Agent: example-app/1.0 (security@example.com)
Accept: application/vnd.github+json
X-GitHub-Api-Version: 2026-03-10
Suprnova, not the provider, adds the bearer header. The SDK rejects any provider that tries to replace it.
Handle a missing verified primary email
Most GitHub accounts have a verified primary address, including accounts that keep it private. The user:email scope lets the plugin verify that private address without making it public.
If GitHub does not return exactly one verified primary address, the plugin returns email: None. Auth::oauth("github").complete(...) then returns an HTTP 409 Conflict response with this message:
OAuth identity requires verified email completion
A basic application can ask the user to verify a primary address in GitHub and restart sign-in. If your application already has a separate verified-email completion or explicit account-linking flow, route the conflict into that flow. Do not weaken the provider by trusting GET /user's public email field or a secondary address.
Account linking stays explicit
The default AutoLinkPolicy does not silently attach a GitHub identity to an
existing account merely because the email strings match. This applies whether
the existing account's email is verified or still unverified. The authenticated
owner must explicitly authorize the link.
The verified primary email still matters for safe new-account creation. When no account owns that normalized address, Magnetar can create the application user and linked GitHub identity through its verified-provider transaction. A matching existing account instead produces the explicit-link outcome rather than transferring ownership.
GitHub's numeric id remains the provider subject throughout this process. A user can rename their GitHub login without creating a second application identity.
Revoke the GitHub grant
GitHub revocation is not the generic RFC 7009 form. The plugin sends the application-grant request GitHub documents:
DELETE /applications/{client_id}/grant
Authorization: Basic base64(client_id:client_secret)
Accept: application/vnd.github+json
X-GitHub-Api-Version: 2026-03-10
Content-Type: application/json
{"access_token":"..."}
GitHub removes the application's grant and all OAuth tokens associated with that user. Version 0.1.0 does not enable GitHub's optional expiring-token and refresh-token mode.
Test the complete flow
- Start the application with the four
GITHUB_OAUTH_*values set. - Open
/auth/githubin a browser. - Approve the
user:emailscope on GitHub. - Confirm that GitHub returns to the exact callback URL registered for the OAuth App.
- Confirm that the callback signs the user in and redirects to
/. - Sign out and repeat the flow.
- Confirm that the second sign-in resolves the same account, even if the GitHub login has changed.
Do not test by inventing a callback code and sending it directly. Start the flow through the same browser session so state and PKCE validation exercise the real boundary.
The plugin repository also carries an offline integration suite. It drives Auth::oauth("github").begin() and verify_oauth_identity(...) through the public Suprnova engine against an in-process mock GitHub server. The test verifies the token exchange, PKCE, required headers, /user, /user/emails, and identity mapping without contacting GitHub.com.
Run the project checks with:
Use GitHub Enterprise Server
Compatible GitHub Enterprise Server installations can use explicit endpoints:
let endpoints = GitHubEndpoints ;
The plugin requires HTTPS outside loopback tests. The user and email endpoints must share the same origin because the transport forwards the same bearer token to both.
Confirm that your GitHub Enterprise Server version supports PKCE and the configured REST API version before deployment. The plugin sends X-GitHub-Api-Version: 2026-03-10 in version 0.1.0.
The external SDK boundary is the point
The implementation is intentionally ordinary. A developer can reproduce its dependency graph from the public tags:
suprnova v1.3.2 10d94d2d
suprnova-oauth-github v0.1.0 96ad9926
A clean consumer project using those two Git tags compiles the provider, transport, limiter, and MagnetarOAuthHostConfig without access to either repository's workspace. The plugin's source firewall rejects a direct Magnetar dependency, a path dependency, or a Magnetar import.
That gives Suprnova applications a real GitHub sign-in option today, and it demonstrates the contract every external OAuth provider can use: provider-specific proof at the edge, host-owned transport and session authority, and no private shortcut between them.
Install the plugin from GitHub or start with its complete README tutorial.


Comments 0