This application supports OAuth 2.0 authentication with multiple providers, allowing users to sign in using their existing accounts from GitHub, Google, or Microsoft.
OAuth provides a secure way for users to authenticate without creating a new password. Users created through OAuth:
- Do not have passwords -
hashed_passwordisNULL - Cannot use password authentication - Password login is automatically rejected for OAuth users
- Can only authenticate via their OAuth provider
- Have usernames from email - Username is extracted from the email's local part and converted to lowercase
- Login endpoint:
/api/v1/login/github - Callback endpoint:
/api/v1/callback/github
- Login endpoint:
/api/v1/login/google - Callback endpoint:
/api/v1/callback/google
- Login endpoint:
/api/v1/login/microsoft - Callback endpoint:
/api/v1/callback/microsoft
Add the following to your .env file (in src/):
# Backend URL for OAuth callbacks
APP_BACKEND_HOST="http://localhost:8000"
# GitHub OAuth
GITHUB_CLIENT_ID="your_github_client_id"
GITHUB_CLIENT_SECRET="your_github_client_secret"
# Google OAuth
GOOGLE_CLIENT_ID="your_google_client_id"
GOOGLE_CLIENT_SECRET="your_google_client_secret"
# Microsoft OAuth
MICROSOFT_CLIENT_ID="your_microsoft_client_id"
MICROSOFT_CLIENT_SECRET="your_microsoft_client_secret"
MICROSOFT_TENANT="your_microsoft_tenant_id"- Go to GitHub Developer Settings
- Click "New OAuth App"
- Configure:
- Application name: Your app name
- Homepage URL:
http://localhost:8000(or your production URL) - Authorization callback URL:
http://localhost:8000/api/v1/callback/github
- Copy the Client ID and Client Secret
- Go to Google Cloud Console
- Create a project (if needed)
- Configure OAuth consent screen:
- Choose "External"
- Add your email as a test user
- Create OAuth 2.0 Client ID:
- Type: "Web application"
- Authorized redirect URIs:
http://localhost:8000/api/v1/callback/google
- Copy the Client ID and Client Secret
- Go to Azure Portal
- Click "New registration"
- Configure:
- Name: Your app name
- Redirect URI:
http://localhost:8000/api/v1/callback/microsoft
- Copy the Application (client) ID, Client Secret, and Directory (tenant) ID
Providers are automatically enabled when their credentials are configured. If credentials are missing or incomplete, the provider's routes will not be registered.
User navigates to the login endpoint:
GET /api/v1/login/{provider}
# Example: GET /api/v1/login/githubThe application redirects the user to the OAuth provider's authorization page.
The user authorizes the application on the provider's website.
The provider redirects back to the callback endpoint with an authorization code:
GET /api/v1/callback/{provider}?code=AUTHORIZATION_CODEThe application:
- Exchanges the code for an access token
- Retrieves user information (email, name, username)
- Checks if a user with that email exists:
- If exists: Logs in the existing user
- If new: Creates a new user with
hashed_password = NULL
- Returns JWT tokens
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}A refresh token is also set as an HTTP-only cookie.
Usernames are derived from the user's email address:
Validation pattern: ^[a-z0-9._+-]+$ (lowercase letters, numbers, and valid email characters: . _ + -)
Extraction process:
- Extract username from email (part before
@) - Convert to lowercase
Examples:
| Derived Username | |
|---|---|
john.doe@example.com |
john.doe |
Jane_Smith@example.com |
jane_smith |
user+tag@example.com |
user+tag |
Test.User-123@example.com |
test.user-123 |
The username keeps all valid email local part characters (letters, numbers, ., _, +, -) and only converts to lowercase. This aligns perfectly with email standards and ensures usernames match the validation pattern.
OAuth users have hashed_password = NULL in the database. This provides several security benefits:
- Prevents password authentication: Password login is automatically rejected for OAuth users
- No password to compromise: OAuth users cannot have their passwords leaked
- Forces OAuth authentication: Users must authenticate through their OAuth provider
The authentication function explicitly checks for NULL passwords:
if db_user["hashed_password"] is None:
return False # Reject authenticationNULL passwords can only be created through OAuth:
- Public API (
/userendpoint) requiresUserCreateschema with a mandatorypasswordfield - OAuth flow uses internal
UserCreateInternalschema that allowshashed_password: None - No way to bypass: FastAPI validates all incoming requests against the schema
If both password authentication and OAuth are enabled, a warning is logged:
Both password authentication and {provider} OAuth are enabled.
For enterprise or B2B deployments, it is recommended to disable password
authentication by setting ENABLE_PASSWORD_AUTH=false and relying solely on OAuth.
To disable password authentication, set in .env:
ENABLE_PASSWORD_AUTH=falseAfter receiving the access token, include it in API requests:
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
http://localhost:8000/api/v1/users/me- Go to
http://localhost:8000/docs - Click "Authorize" button
- Enter:
Bearer YOUR_ACCESS_TOKEN - Test authenticated endpoints
// Step 1: Redirect user to OAuth login
window.location.href = 'http://localhost:8000/api/v1/login/github';
// Step 2: Handle callback (your frontend callback page)
// Extract tokens from the response
const response = await fetch(window.location.href);
const { access_token } = await response.json();
// Step 3: Store and use the token
localStorage.setItem('access_token', access_token);
// Step 4: Make authenticated requests
fetch('http://localhost:8000/api/v1/users/me', {
headers: {
'Authorization': `Bearer ${access_token}`
}
});The refresh token is automatically stored as an HTTP-only cookie. To refresh the access token:
const response = await fetch('http://localhost:8000/api/v1/refresh', {
method: 'POST',
credentials: 'include' // Include cookies
});
const { access_token } = await response.json();OAuth users are stored with the following characteristics:
-- OAuth user example
INSERT INTO "user" (
name,
username,
email,
hashed_password, -- NULL for OAuth users
profile_image_url,
created_at,
is_superuser
) VALUES (
'John Doe',
'johndoe',
'john.doe@example.com',
NULL, -- No password
'https://avatars.github.com/...',
NOW(),
false
);- Configure credentials in
.env - Start the application:
docker compose up
- Open browser to
http://localhost:8000/api/v1/login/github - Authorize the application
- Verify you receive an access token
See tests/test_oauth.py for comprehensive OAuth tests including:
- Provider enablement logic
- Username extraction from emails
- User creation with NULL passwords
- Callback handling
- Security validations
Run tests:
uv run pytest tests/test_oauth.py -vProblem: /api/v1/login/github returns 404
Solution: The provider is disabled because credentials are missing or incomplete. Check:
GITHUB_CLIENT_IDis setGITHUB_CLIENT_SECRETis set- Restart the application after adding credentials
Problem: Error during callback about redirect URI mismatch
Solution: Ensure the callback URL in the provider settings exactly matches:
http://localhost:8000/api/v1/callback/{provider}
Problem: NULL value in column "hashed_password" violates not-null constraint
Solution: The database schema hasn't been updated. Run:
docker compose exec db psql -U postgres -d postgres \
-c "ALTER TABLE \"user\" ALTER COLUMN hashed_password DROP NOT NULL;"Problem: String should match pattern '^[a-z0-9._+-]+$'
Solution: The username contains characters not allowed in the pattern. Valid characters are:
- Lowercase letters (a-z)
- Numbers (0-9)
- Period (
.), underscore (_), plus (+), hyphen (-)
This should not occur with standard email addresses, as the extraction process preserves these valid characters. If you see this error, the email address may contain unusual characters not supported by standard email specifications.
All OAuth providers inherit from BaseOAuthProvider, ensuring consistent behavior:
class BaseOAuthProvider(ABC):
provider_config: dict[str, Any]
sso_provider: type[SSOBase]
async def _login_handler(self) -> RedirectResponse:
# Redirects to OAuth provider
async def _callback_handler(self, request, response, db):
# Handles OAuth callback and user creation
async def _get_user_details(self, oauth_user) -> UserCreateInternal:
# Extracts username from email and creates user objectTo add support for a new OAuth provider:
- Install the provider's SSO library
- Create a new provider class:
class NewOAuthProvider(BaseOAuthProvider):
sso_provider = NewProviderSSO
provider_config = {
"client_id": settings.NEW_PROVIDER_CLIENT_ID,
"client_secret": settings.NEW_PROVIDER_CLIENT_SECRET,
}
# Register the provider
NewOAuthProvider(router)- Add configuration to
settings.py - Add credentials to
.env
- Use HTTPS: Set
APP_BACKEND_HOST=https://your-domain.com - Update callback URLs in provider settings
- Disable password auth for OAuth-only deployments
- Secure credentials: Never commit
.envto version control - Use environment-specific credentials: Different keys for dev/staging/production
- Validate redirect URIs: Only allow specific callback URLs
- Use HTTPS in production: OAuth tokens should never be transmitted over HTTP
- Implement rate limiting: Prevent OAuth callback abuse
- Monitor failed attempts: Log and alert on repeated OAuth failures
- Review OAuth scopes: Only request necessary permissions from providers
- Clear error messages: Inform users when OAuth fails
- Handle edge cases: Users without email addresses, denied permissions
- Provide alternatives: Offer both OAuth and password auth (if applicable)
- Account linking: Allow users to link multiple OAuth providers to one account (future enhancement)