OAuth2 / OpenID Connect Authentication
OAuth2 and OpenID Connect (OIDC) authentication for social login and enterprise SSO.
Overview
OAuth2/OIDC authentication is ideal for:
- Social login (Google, GitHub, Facebook, etc.)
- Enterprise SSO (Okta, Auth0, Keycloak, Azure AD)
- Single Sign-On across multiple applications
- Delegated authentication
Feature Flag
Enable OAuth2 support in your Cargo.toml:
[dependencies]
actix-security = { version = "0.2", features = ["oauth2"] }
Quick Start
Google OAuth2
use actix_security::http::security::oauth2::{
OAuth2Config, OAuth2Provider, OAuth2Client
};
// Configure Google OAuth2
let config = OAuth2Config::new(
std::env::var("GOOGLE_CLIENT_ID").unwrap(),
std::env::var("GOOGLE_CLIENT_SECRET").unwrap(),
"http://localhost:8080/oauth2/callback/google"
)
.provider(OAuth2Provider::Google);
// Create client (async - performs OIDC discovery)
let client = OAuth2Client::new(config).await?;
// Generate authorization URL
let (auth_url, state, pkce_verifier, nonce) = client.authorization_url();
// Redirect user to auth_url...
// Store state, pkce_verifier, and nonce in session for callback verification
GitHub OAuth2
let config = OAuth2Config::new(
std::env::var("GITHUB_CLIENT_ID").unwrap(),
std::env::var("GITHUB_CLIENT_SECRET").unwrap(),
"http://localhost:8080/oauth2/callback/github"
)
.provider(OAuth2Provider::GitHub);
// GitHub doesn't support OIDC, so no discovery needed
let client = OAuth2Client::new_basic(config)?;
Supported Providers
| Provider | OIDC Support | PKCE Support |
|---|---|---|
| Yes | Yes | |
| GitHub | No | No |
| Microsoft | Yes | Yes |
| No | No | |
| Apple | Yes | Yes |
| Okta | Yes | Yes |
| Auth0 | Yes | Yes |
| Keycloak | Yes | Yes |
Configuration Options
let config = OAuth2Config::new("client-id", "client-secret", "redirect-uri")
// Use a pre-configured provider
.provider(OAuth2Provider::Google)
// Or configure custom endpoints
.authorization_uri("https://auth.example.com/authorize")
.token_uri("https://auth.example.com/token")
.userinfo_uri("https://auth.example.com/userinfo")
// OIDC issuer for auto-discovery
.issuer_uri("https://auth.example.com")
// Scopes
.scopes(vec!["openid", "email", "profile"])
.add_scope("custom_scope")
// PKCE (enabled by default for supported providers)
.use_pkce(true)
// Username attribute extraction
.username_attribute("email") // Use email as username
// Custom authorization parameters
.authorization_param("prompt", "consent");
Authorization Code Flow
Step 1: Generate Authorization URL
use actix_web::{get, web, HttpResponse};
use actix_session::Session;
#[get("/oauth2/authorize/{provider}")]
async fn authorize(
provider: web::Path<String>,
session: Session,
clients: web::Data<OAuth2ClientRepository>,
) -> HttpResponse {
let client = clients.get_client(&provider).unwrap();
// Generate authorization URL with PKCE and nonce
let (auth_url, state, pkce_verifier, nonce) = client.authorization_url();
// Store state in session for CSRF protection
session.insert("oauth2_state", state.secret()).unwrap();
// Store PKCE verifier for token exchange
if let Some(verifier) = pkce_verifier {
session.insert("oauth2_pkce", verifier.secret()).unwrap();
}
// Store nonce for OIDC token validation
if let Some(n) = nonce {
session.insert("oauth2_nonce", n.secret()).unwrap();
}
HttpResponse::Found()
.append_header(("Location", auth_url.to_string()))
.finish()
}
Step 2: Handle Callback
use oauth2::{CsrfToken, PkceCodeVerifier};
use openidconnect::Nonce;
#[derive(Deserialize)]
struct CallbackQuery {
code: String,
state: String,
}
#[get("/oauth2/callback/{provider}")]
async fn callback(
provider: web::Path<String>,
query: web::Query<CallbackQuery>,
session: Session,
clients: web::Data<OAuth2ClientRepository>,
) -> HttpResponse {
let client = clients.get_client(&provider).unwrap();
// Verify state (CSRF protection)
let stored_state: String = session.get("oauth2_state").unwrap().unwrap();
if query.state != stored_state {
return HttpResponse::BadRequest().body("Invalid state");
}
// Retrieve PKCE verifier
let pkce_verifier = session
.get::<String>("oauth2_pkce")
.unwrap()
.map(|s| PkceCodeVerifier::new(s));
// Retrieve nonce for OIDC
let nonce = session
.get::<String>("oauth2_nonce")
.unwrap()
.map(|s| Nonce::new(s));
// Exchange code for tokens
let (oauth2_user, oidc_user) = client
.exchange_code(&query.code, pkce_verifier, nonce.as_ref())
.await
.map_err(|e| HttpResponse::InternalServerError().body(e.to_string()))?;
// Convert to authenticated user
let user = oauth2_user.to_user();
// Store user in session
session.insert("user", serde_json::to_string(&oauth2_user).unwrap()).unwrap();
HttpResponse::Found()
.append_header(("Location", "/"))
.finish()
}
OAuth2User
The OAuth2User contains information retrieved from the OAuth2 provider:
pub struct OAuth2User {
pub sub: String, // Unique identifier
pub name: Option<String>,
pub email: Option<String>,
pub email_verified: Option<bool>,
pub picture: Option<String>,
pub locale: Option<String>,
pub attributes: HashMap<String, Value>, // Provider-specific
pub access_token: Option<String>,
pub refresh_token: Option<String>,
pub expires_at: Option<i64>,
pub provider: String,
}
// Get username (prefers email, falls back to sub)
let username = oauth2_user.username();
// Convert to security User
let user = oauth2_user.to_user();
// User has role "USER" and authority "OAUTH2_USER_GOOGLE"
OidcUser
For OIDC providers, you also get ID token claims:
pub struct OidcUser {
pub oauth2_user: OAuth2User,
pub id_token_claims: Option<IdTokenClaims>,
pub id_token: Option<String>, // Raw JWT
}
pub struct IdTokenClaims {
pub iss: String, // Issuer
pub sub: String, // Subject
pub aud: Vec<String>, // Audience
pub exp: i64, // Expiration
pub iat: i64, // Issued at
pub auth_time: Option<i64>,
pub nonce: Option<String>,
pub at_hash: Option<String>,
}
Multiple Providers
Use OAuth2ClientRepository to manage multiple providers:
use actix_security::http::security::oauth2::OAuth2ClientRepository;
// Build repository from configs
let configs = vec![
OAuth2Config::new(google_id, google_secret, google_redirect)
.provider(OAuth2Provider::Google),
OAuth2Config::new(github_id, github_secret, github_redirect)
.provider(OAuth2Provider::GitHub),
];
let repository = OAuth2ClientRepository::from_configs(configs).await?;
// Use in Actix Web
App::new()
.app_data(web::Data::new(repository))
.service(authorize)
.service(callback)
Custom Provider
Configure a custom OAuth2/OIDC provider:
let config = OAuth2Config::new("client-id", "secret", "redirect-uri")
.registration_id("custom")
.authorization_uri("https://custom.example.com/oauth/authorize")
.token_uri("https://custom.example.com/oauth/token")
.userinfo_uri("https://custom.example.com/oauth/userinfo")
// For OIDC with discovery:
.issuer_uri("https://custom.example.com")
.scopes(vec!["openid", "email", "profile"]);
Complete Example
use actix_web::{get, web, App, HttpServer, HttpResponse};
use actix_session::{Session, SessionMiddleware, storage::CookieSessionStore};
use actix_web::cookie::Key;
use actix_security::http::security::oauth2::{
OAuth2Config, OAuth2Provider, OAuth2Client, OAuth2ClientRepository
};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Configure OAuth2 providers
let google_config = OAuth2Config::new(
std::env::var("GOOGLE_CLIENT_ID").unwrap(),
std::env::var("GOOGLE_CLIENT_SECRET").unwrap(),
"http://localhost:8080/oauth2/callback/google"
)
.provider(OAuth2Provider::Google);
let repository = OAuth2ClientRepository::from_configs(vec![google_config])
.await
.expect("Failed to create OAuth2 repository");
let secret_key = Key::generate();
HttpServer::new(move || {
App::new()
.wrap(SessionMiddleware::new(
CookieSessionStore::default(),
secret_key.clone()
))
.app_data(web::Data::new(repository.clone()))
.service(login_page)
.service(authorize)
.service(callback)
.service(profile)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
#[get("/login")]
async fn login_page() -> HttpResponse {
HttpResponse::Ok().body(r#"
<a href="/oauth2/authorize/google">Login with Google</a>
"#)
}
#[get("/profile")]
async fn profile(session: Session) -> HttpResponse {
if let Some(user_json) = session.get::<String>("user").unwrap() {
HttpResponse::Ok().body(format!("Logged in as: {}", user_json))
} else {
HttpResponse::Found()
.append_header(("Location", "/login"))
.finish()
}
}
Security Best Practices
- Always validate state - Prevents CSRF attacks
- Use PKCE - Prevents authorization code interception
- Validate nonce for OIDC - Prevents replay attacks
- Use HTTPS - Always in production
- Validate redirect URIs - Prevent open redirects
- Store tokens securely - Use encrypted sessions
- Handle token expiration - Implement refresh token flow
Spring Security Comparison
Spring Security:
@Configuration
public class OAuth2Config {
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(
CommonOAuth2Provider.GOOGLE.getBuilder("google")
.clientId("client-id")
.clientSecret("client-secret")
.build()
);
}
}
Actix Security:
let config = OAuth2Config::new("client-id", "client-secret", "redirect-uri")
.provider(OAuth2Provider::Google);
let repository = OAuth2ClientRepository::from_configs(vec![config]).await?;