GitHub App Authentication
This documentation describes the GitHub App authentication utility used for secure user login across multiple pages in the application. The implementation follows GitHub's web application flow with PKCE (Proof Key for Code Exchange) security hardening.
Overview
The GitHub Auth system provides a reusable, secure way to authenticate users with their GitHub accounts using GitHub App credentials. It is currently integrated with the Policy Reports page and can be easily adopted by other pages in the application.
Reference: GitHub Official Documentation on User Access Tokens for Apps
Architecture
The authentication system consists of three components working together:
- Frontend Utility (
frontend/src/utilities/githubAuth.js) - Orchestrates the authentication flow - Backend Utility (
backend/src/utilities/githubAuth.js) - Builds URLs and calls GitHub APIs - Backend Routes (
backend/src/routes/githubAuth.js) - Exposes shared authentication endpoints
Authentication Flow
The system implements the GitHub web application flow with PKCE:
User Frontend Backend GitHub
| | | |
|--Login Click------->| | |
| |--Generate State------>| |
| |--Generate PKCE------->| |
| | |--Redirect--------->|
| | | to /authorize |
|<------GitHub Callback----(code, state)------|<------------------|
| |--Validate State------>| |
| |--Exchange Code+PKCE-->| |
| | |--Token Request---->|
| | |<--Access Token-----|
| |<-Set Cookie-----------| |
|<----Redirect--------| (httpOnly, 3 hours) | |
| (authenticated) | | |
Security Features
PKCE (RFC 7636)
Proof Key for Code Exchange protects against authorisation code interception attacks:
- Code Verifier: A cryptographically random 32-byte string generated on the frontend
- Code Challenge: SHA-256 hash of the verifier, base64url encoded
- Challenge Method: Always uses 'S256' (SHA-256)
- Backend Verification: GitHub validates that the code verifier matches the challenge
State Parameter
Prevents CSRF attacks by:
- Generating a random state string on the frontend
- Storing it in sessionStorage
- Validating the returned state matches the original
- Clearing after successful validation
Secure Cookie Storage
Access tokens are stored in httpOnly cookies to prevent XSS attacks:
- httpOnly: Token cannot be accessed from JavaScript
- secure: Cookie only sent over HTTPS in production
- sameSite: 'lax' prevents CSRF via cookie-based requests
- maxAge: 3 hours (180 minutes)
API Endpoints
All endpoints are mounted at /api/github/auth/ and are shared across the entire application. Any page can use these same endpoints without duplication.
GET /api/github/auth/login
Initiates the GitHub authorisation request.
Query Parameters:
state(string, required) - CSRF protection state generated by frontendcode_challenge(string, required) - PKCE code challenge (base64url SHA-256)code_challenge_method(string, required) - Always 'S256'
Response: Redirects to https://github.com/login/oauth/authorize with encoded parameters
POST /api/github/auth/token
Exchanges the authorisation code for an access token.
Request Body:
{
"code": "string", // Authorisation code from GitHub
"codeVerifier": "string" // PKCE code verifier (base64url encoded)
}
Response (Success):
Sets githubUserToken cookie (httpOnly, 3 hours, secure, sameSite=lax)
Response (Error):
Status: 400 Bad Request
GET /api/github/auth/status
Checks whether a valid authenticated session exists.
Response:
No authentication required - checks for presence of githubUserToken cookie
GET /api/github/auth/user
Retrieves the authenticated user's GitHub profile.
Response (Authenticated):
{
"login": "string", // GitHub username
"name": "string", // User's full name
"avatar_url": "string" // URL to avatar image
}
Response (Unauthenticated):
Status: 401 Unauthorized
POST /api/github/auth/logout
Clears the authentication cookie and ends the session.
Response:
Frontend Integration
Basic Usage
Import the utility functions:
import {
loginWithGitHub,
logoutUser,
checkAuthStatus,
fetchGitHubUserProfile,
handleAuthCallback,
retrievePersistedFormState,
} from '../utilities/githubAuth';
Initialising on Page Load
Run this effect on page mount to handle OAuth redirects and restore auth state:
useEffect(() => {
const initialiseAuth = async () => {
// Process OAuth callback if returning from GitHub
await handleAuthCallback({ redirectPath: '/your-page-path' });
// Check if user is authenticated
const authenticated = await checkAuthStatus();
if (authenticated) {
// Fetch user profile
const profile = await fetchGitHubUserProfile();
// Use profile.login, profile.name, profile.avatar_url
}
setIsAuthenticated(authenticated);
};
initialiseAuth();
}, []);
Login Button
Call loginWithGitHub() when user clicks the login button:
const handleLogin = async () => {
await loginWithGitHub({
redirectPath: '/your-page-path',
formState: {
// Optional: persist form state during auth
fieldName: fieldValue,
anotherField: anotherValue,
},
});
};
<button onClick={handleLogin}>Login with GitHub</button>;
Logout Button
Call logoutUser() to clear authentication:
const handleLogout = async () => {
const success = await logoutUser();
if (success) {
setIsAuthenticated(false);
// Reset user-specific state
}
};
<button onClick={handleLogout}>Logout</button>;
Form State Persistence
The utility can save form state before redirecting to GitHub and restore it after authentication completes. This is useful to preserve user selections during the auth flow:
During Login:
Pass formState object to loginWithGitHub() with fields you want to preserve:
await loginWithGitHub({
redirectPath: '/your-page-path',
formState: {
fieldOne: valueOne,
fieldTwo: valueTwo,
},
});
During Initialisation:
Retrieve persisted state after auth completes:
const persistedState = retrievePersistedFormState();
if (persistedState.fieldOne) {
setFieldOne(persistedState.fieldOne);
}
Form state is only saved during an active login flow. It is never persisted between page loads.
Adding GitHub Auth to Another Page
Backend Setup
The backend authentication endpoints are already implemented and shared across all pages at /api/github/auth/. No page-specific backend route setup is required.
Important: The
redirectPathfor your new page (e.g./your-page-path) must be registered as a callback URL in the GitHub App settings (GitHub Developer Settings → Your App → Callback URLs). GitHub will reject the OAuth flow for any unregistered redirect URI.
Frontend Setup
The frontend utility automatically uses the shared /api/github/auth/ endpoint path. No per-page configuration needed.
- Import authentication utilities in your page component:
import {
loginWithGitHub,
logoutUser,
checkAuthStatus,
fetchGitHubUserProfile,
handleAuthCallback,
} from '../utilities/githubAuth';
- Add authentication state:
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isAuthLoading, setIsAuthLoading] = useState(true);
const [username, setUsername] = useState(null);
-
Add initialisation effect (see "Initialising on Page Load" above)
-
Add login and logout button handlers
-
Conditionally render authenticated content:
{
isAuthenticated ? (
<div>Logged in as {username}</div>
) : (
<button onClick={handleLogin}>Login with GitHub</button>
);
}
Environment Configuration
Ensure these variables are set in the backend environment (this is application-wide, not per-page):
GITHUB_APP_CLIENT_ID- GitHub App Client IDGITHUB_APP_CLIENT_SECRET- GitHub App Client SecretFRONTEND_URL- Frontend base URL (e.g., https://example.com) used for all auth redirectsNODE_ENV- Set to 'production' for secure cookies
Token Scope
The authentication requests the following scopes from GitHub:
user:email- Access to user email addressesread:org- Read access to organisation data
These scopes enable the utility to fetch user profile information and access organisation data. To modify scopes, update backend/src/utilities/githubAuth.js in the buildGitHubAuthoriseUrl() function.
Troubleshooting
"400 Bad Request" on Token Exchange
Cause: Missing or invalid PKCE parameters
Solution: Ensure loginWithGitHub() is called with the default parameters and that sessionStorage is not being cleared before token exchange
Cookie Not Being Set
Cause: Insecure connection in production or incorrect secure flag
Solution: Verify NODE_ENV=production, ensure HTTPS is used, and check that secure flag in cookie settings is correct
User Profile Returns Null
Cause: Token is expired (3-hour expiration) or cookie was cleared
Solution: User must re-authenticate. Call logoutUser() and show login button again.
Form State Not Persisting
Cause: Form state passed to loginWithGitHub() was null or empty
Solution: Ensure formState object has at least one truthy value. Only truthy values will be saved.
Implementation Notes
- The backend auth router is centralised in
backend/src/routes/githubAuth.jsand mounted once at/api/github/auth. - The frontend utility is page-agnostic and can be used on any page that needs GitHub authentication.
- Token expiration is 3 hours. Consider adding a token refresh mechanism if longer sessions are needed.
- Copilot already has its own OAuth implementation in
backend/src/routes/copilot.js. This can be refactored in future to use the shared backend utility.