OAuth2 Providers Guide
Complete guide to using OAuth2/OIDC providers with Armature authentication.
Table of Contents
- Overview
- Supported Providers
- Google OAuth2
- Microsoft Entra
- AWS Cognito
- Okta
- Auth0
- OAuth2 Flow
- Complete Example
- Best Practices
Overview
armature-auth provides built-in support for major enterprise OAuth2 and OIDC identity providers. This allows you to integrate with existing authentication systems without building your own user management.
Benefits
- Single Sign-On (SSO): Users authenticate with their existing accounts
- Enterprise Ready: Integrates with corporate identity providers
- Security: Leverages battle-tested OAuth2/OIDC protocols
- No User Management: Delegate user storage to providers
- Multi-Provider: Support multiple providers simultaneously
Supported Providers
| Provider | Type | Use Case |
|---|---|---|
| Consumer | Gmail users, Google Workspace | |
| Microsoft Entra | Enterprise | Azure AD, Office 365, Enterprise SSO |
| AWS Cognito | Cloud | AWS-native applications |
| Okta | Enterprise | Enterprise SSO, SAML bridge |
| Auth0 | Universal | Multi-provider, custom branding |
Google OAuth2
Setup
- Go to Google Cloud Console
- Create a new project or select existing
- Navigate to "APIs & Services" > "Credentials"
- Create OAuth 2.0 Client ID
- Add authorized redirect URI:
http://localhost:3000/auth/google/callback
Configuration
use armature_auth::providers::{GoogleConfig, GoogleProvider};
let config = GoogleConfig::new(
"your-client-id.apps.googleusercontent.com".to_string(),
"your-client-secret".to_string(),
"http://localhost:3000/auth/google/callback".to_string(),
);
// Optional: Custom scopes
let config = config.with_scopes(vec![
"openid".to_string(),
"email".to_string(),
"profile".to_string(),
"https://www.googleapis.com/auth/userinfo.profile".to_string(),
]);
let provider = GoogleProvider::new(config)?;
User Info
Google returns:
sub: Google user IDemail: User's Gmail addressname: Full namepicture: Profile picture URLemail_verified: Email verification status
Microsoft Entra (Azure AD)
Setup
- Go to Azure Portal
- Navigate to "Azure Active Directory"
- Select "App registrations" > "New registration"
- Add redirect URI:
http://localhost:3000/auth/microsoft/callback - Create a client secret in "Certificates & secrets"
Configuration
use armature_auth::providers::{MicrosoftEntraConfig, MicrosoftEntraProvider};
// Option 1: Common tenant (any Azure AD account)
let config = MicrosoftEntraConfig::common(
"your-application-id".to_string(),
"your-client-secret".to_string(),
"http://localhost:3000/auth/microsoft/callback".to_string(),
);
// Option 2: Organizations only (work/school accounts)
let config = MicrosoftEntraConfig::organizations(
"your-application-id".to_string(),
"your-client-secret".to_string(),
"http://localhost:3000/auth/microsoft/callback".to_string(),
);
// Option 3: Consumers only (personal Microsoft accounts)
let config = MicrosoftEntraConfig::consumers(
"your-application-id".to_string(),
"your-client-secret".to_string(),
"http://localhost:3000/auth/microsoft/callback".to_string(),
);
// Option 4: Specific tenant
let config = MicrosoftEntraConfig::new(
"your-application-id".to_string(),
"your-client-secret".to_string(),
"http://localhost:3000/auth/microsoft/callback".to_string(),
"your-tenant-id".to_string(),
);
let provider = MicrosoftEntraProvider::new(config)?;
Tenant Types
- common: Any Azure AD account (personal or work/school)
- organizations: Work or school accounts only
- consumers: Personal Microsoft accounts only
- {tenant-id}: Specific Azure AD tenant
User Info
Microsoft Graph returns:
id: Azure AD user ID (mapped tosub)userPrincipalName: User's UPNdisplayName: Full namemail: Email addressjobTitle: Job title
AWS Cognito
Setup
- Go to AWS Console
- Create a User Pool
- Configure "App integration" > "App client"
- Enable "Hosted UI"
- Add callback URL:
http://localhost:3000/auth/cognito/callback - Note your domain:
your-app.auth.{region}.amazoncognito.com
Configuration
use armature_auth::providers::{AwsCognitoConfig, AwsCognitoProvider};
let config = AwsCognitoConfig::new(
"your-client-id".to_string(),
"your-client-secret".to_string(),
"http://localhost:3000/auth/cognito/callback".to_string(),
"your-app.auth.us-east-1.amazoncognito.com".to_string(),
"us-east-1".to_string(),
);
// Optional: Custom scopes
let config = config.with_scopes(vec![
"openid".to_string(),
"email".to_string(),
"profile".to_string(),
"aws.cognito.signin.user.admin".to_string(),
]);
let provider = AwsCognitoProvider::new(config)?;
User Info
Cognito returns:
sub: Cognito user UUIDemail: User's emailemail_verified: Email verification status- Custom attributes you've configured
Okta
Setup
- Go to Okta Admin Console
- Navigate to "Applications" > "Create App Integration"
- Select "OIDC - OpenID Connect"
- Choose "Web Application"
- Add redirect URI:
http://localhost:3000/auth/okta/callback - Note your Okta domain:
dev-12345.okta.com
Configuration
use armature_auth::providers::{OktaConfig, OktaProvider};
let config = OktaConfig::new(
"your-client-id".to_string(),
"your-client-secret".to_string(),
"http://localhost:3000/auth/okta/callback".to_string(),
"dev-12345.okta.com".to_string(),
);
// Optional: Additional scopes
let config = config.with_scopes(vec![
"openid".to_string(),
"email".to_string(),
"profile".to_string(),
"groups".to_string(), // Include group membership
]);
let provider = OktaProvider::new(config)?;
User Info
Okta returns:
sub: Okta user IDemail: User's emailname: Full namepreferred_username: Usernamegroups: Group membership (if requested)
Auth0
Setup
- Go to Auth0 Dashboard
- Navigate to "Applications" > "Create Application"
- Select "Regular Web Applications"
- Add callback URL:
http://localhost:3000/auth/auth0/callback - Note your domain:
your-tenant.us.auth0.com
Configuration
use armature_auth::providers::{Auth0Config, Auth0Provider};
let config = Auth0Config::new(
"your-client-id".to_string(),
"your-client-secret".to_string(),
"http://localhost:3000/auth/auth0/callback".to_string(),
"your-tenant.us.auth0.com".to_string(),
);
// Optional: API audience for access tokens
let config = config.with_audience("https://api.example.com".to_string());
// Optional: Custom scopes
let config = config.with_scopes(vec![
"openid".to_string(),
"email".to_string(),
"profile".to_string(),
"offline_access".to_string(), // For refresh tokens
]);
let provider = Auth0Provider::new(config)?;
User Info
Auth0 returns:
sub: Auth0 user ID (format:provider|id)email: User's emailname: Full namepicture: Profile pictureemail_verified: Email verification- Custom user metadata
OAuth2 Flow
Step 1: Generate Authorization URL
use armature_auth::OAuth2Provider;
let (auth_url, csrf_token) = provider.authorization_url()?;
// IMPORTANT: Armature is stateless - no server-side sessions.
// `csrf_token` (an oauth2::CsrfToken) must be preserved across the redirect
// so it can be compared against the `state` parameter the provider echoes
// back on callback. Since there's no server-side session to stash it in,
// embed it in a signed/encrypted cookie or a signed value passed through
// the flow (e.g. as part of the state parameter itself).
// Redirect user to auth_url
response.redirect(auth_url.as_str());
Step 2: Handle Callback
// Extract code and state from query parameters
let code = request.query("code")?;
let state = request.query("state")?;
// Verify the state parameter matches the csrf_token generated in Step 1
// (read back from the signed cookie or wherever it was stored) before
// trusting the callback.
//
// Plain `!=` is acceptable here: state values are single-use and
// short-lived, so timing side-channels are a much lower risk than for
// long-lived secrets like TOTP codes or API tokens (which should use a
// constant-time comparison instead).
if state != csrf_token.secret().as_str() {
return Err(Error::AuthenticationFailed);
}
// Exchange code for token
let token = provider.exchange_code(code.to_string()).await?;
Step 3: Get User Info
// Fetch user information
let user_info = provider.get_user_info(&token).await?;
// Create JWT token (stateless authentication)
let user_claims = UserClaims {
sub: user_info.sub,
email: user_info.email.unwrap_or_default(),
name: user_info.name,
};
// Generate JWT instead of session
let jwt_token = jwt_manager.create_token(user_claims)?;
// Return JWT to client
// Client stores token and sends it with each request
// No server-side session needed
Step 4: Token Refresh
// When access token expires
if let Some(refresh_token) = token.refresh_token {
let new_token = provider.refresh_token(refresh_token).await?;
// Update stored token
}
Complete Example
use armature_framework::prelude::*;
use armature_auth::providers::{GoogleConfig, GoogleProvider};
use armature_auth::OAuth2Provider;
#[controller("/auth")]
struct AuthController {
google_provider: Arc<GoogleProvider>,
}
impl AuthController {
#[get("/google")]
async fn google_login(&self) -> Result<Response> {
// Generate authorization URL
let (auth_url, csrf_token) = self.google_provider
.authorization_url()
.map_err(|e| Error::Internal(e.to_string()))?;
// Armature is stateless - there's no server-side session to stash
// `csrf_token` in. Persist it (e.g. in a signed/encrypted cookie)
// so it can be compared against the `state` query parameter the
// provider sends back to the callback route below.
// Redirect to Google
Ok(Response::redirect(auth_url.as_str()))
}
#[get("/google/callback")]
async fn google_callback(&self, request: HttpRequest) -> Result<Response> {
// Extract authorization code and state
let code = request.query_param("code")
.ok_or_else(|| Error::BadRequest("Missing code".into()))?;
let state = request.query_param("state")
.ok_or_else(|| Error::BadRequest("Missing state".into()))?;
// Verify `state` against the csrf_token persisted during login
// before proceeding (omitted here for brevity).
// Exchange code for token
let token = self.google_provider
.exchange_code(code.to_string())
.await
.map_err(|e| Error::Internal(e.to_string()))?;
// Get user info
let user_info = self.google_provider
.get_user_info(&token)
.await
.map_err(|e| Error::Internal(e.to_string()))?;
// Create user session
let user = UserContext::new(user_info.sub)
.with_email(user_info.email.unwrap_or_default());
// In production:
// 1. Find or create user in database
// 2. Generate JWT token (stateless auth)
// 3. Return JWT to client
// 4. Client stores JWT and includes in subsequent requests
Ok(Response::json(serde_json::json!({
"user": user,
"token": token.access_token
})))
}
}
Best Practices
1. CSRF Protection
authorization_url() returns a CsrfToken alongside the authorization URL. Persist
it and verify it against the state parameter the provider echoes back on callback:
// Generate the authorization URL and its CSRF token
let (auth_url, csrf_token) = provider.authorization_url()?;
// Persist csrf_token.secret() somewhere that survives the redirect
// (a signed/encrypted cookie, since Armature keeps no server-side session)
// before sending the user to auth_url.
// On callback, compare the `state` query parameter against the
// persisted token before exchanging the code.
//
// Plain `!=` is acceptable here: state values are single-use and
// short-lived, so timing side-channels are a much lower risk than for
// long-lived secrets like TOTP codes or API tokens (which should use a
// constant-time comparison instead).
if state != csrf_token.secret().as_str() {
return Err(Error::AuthenticationFailed);
}
let token = provider.exchange_code(code).await?;
Note: Armature is stateless, so there's no server-side session to hold the
CsrfToken between the redirect and the callback. Store it client-side in a
signed/encrypted cookie (or otherwise bind it to the redirect) so it can be
compared against state when the callback arrives.
2. Secure Redirect URIs
- Use HTTPS in production
- Whitelist exact URIs (no wildcards)
- Use separate URIs per environment
let redirect_url = if cfg!(debug_assertions) {
"http://localhost:3000/callback"
} else {
"https://app.example.com/callback"
};
3. Token Storage
// โ Store tokens securely
let encrypted_token = encrypt(token.access_token)?;
db.save_user_token(user_id, encrypted_token)?;
// โ Use refresh tokens
if token_expired && refresh_token.is_some() {
let new_token = provider.refresh_token(refresh_token).await?;
}
// โ Don't expose tokens to client
// โ Don't log tokens
4. Error Handling
match provider.exchange_code(code).await {
Ok(token) => { /* Success */ },
Err(e) => {
log::error!("OAuth2 error: {}", e);
// Show user-friendly message
return Err(Error::AuthenticationFailed);
}
}
5. Multi-Provider Support
enum Provider {
Google(GoogleProvider),
Microsoft(MicrosoftEntraProvider),
Okta(OktaProvider),
}
impl Provider {
async fn authenticate(&self, code: String) -> Result<OAuth2Token> {
match self {
Provider::Google(p) => p.exchange_code(code).await,
Provider::Microsoft(p) => p.exchange_code(code).await,
Provider::Okta(p) => p.exchange_code(code).await,
}
}
}
6. User Linking
async fn link_oauth_user(
oauth_sub: String,
provider: String,
user_info: OAuth2UserInfo,
) -> Result<User> {
// Check if OAuth account already linked
if let Some(user) = db.find_by_oauth(provider, oauth_sub).await? {
return Ok(user);
}
// Check if email matches existing user
if let Some(email) = user_info.email {
if let Some(user) = db.find_by_email(&email).await? {
// Link OAuth account to existing user
db.link_oauth(user.id, provider, oauth_sub).await?;
return Ok(user);
}
}
// Create new user
let user = User::new(user_info);
db.save_user(&user).await?;
db.link_oauth(user.id, provider, oauth_sub).await?;
Ok(user)
}
Summary
The armature-auth OAuth2 provider system offers:
- โ 5 major providers out of the box
- โ Consistent API across all providers
- โ Type-safe configuration
- โ Async-first design
- โ Production-ready error handling
- โ Extensible for custom providers
For more information:
- Auth Guide - Complete authentication documentation
- JWT Guide - JWT token management (coming soon)
- Examples - Working code examples