Skip to content

Stateless OAuth2

By default, OAuth2 logins often result in a server-side session being created. However, Authkestra allows you to build a strictly stateless flow where the authentication results in a JWT (JSON Web Token).

“Stateless” means that the entire flow can be completed without using a session store.

Authkestra strictly implements the OAuth 2.0 Authorization Framework (RFC 6749) and PKCE (RFC 7636). We didn’t invent these protocols; we just provide a memory-safe, idiomatic Rust engine to execute them.

If you are using multiple OAuth providers (e.g., GitHub and Google), you must enable their respective feature flags:

[dependencies]
authkestra = { version = "0.2.1", features = ["github", "google"] }

Notice how we completely omit .session_store() and use .jwt_secret() instead. You can also chain multiple providers back-to-back:

use authkestra::flow::{Engine, OAuth2Flow};
use authkestra_providers::github::GithubProvider;
use authkestra_providers::google::GoogleProvider;
// Initialize Authkestra in stateless mode supporting multiple providers
let auth_engine = Engine::builder()
.provider(OAuth2Flow::new(GithubProvider::new(...)))
.provider(OAuth2Flow::new(GoogleProvider::new(...)))
.jwt_secret(b"your-256-bit-secret-key-at-least-32-bytes-long")
.build();

Because we are not using sessions, we cannot use the automatic auth_engine.axum_router(). Instead, you must manually define your /auth/:provider and /auth/callback/:provider routes.

Here is a step-by-step breakdown of how the manual handlers work:

The login handler intercepts the user’s request and delegates to Authkestra’s axum_login_handler helper. This helper securely generates the PKCE challenges and cookies before redirecting to the provider.

use axum::{extract::{Path, State, Query}, response::IntoResponse};
use authkestra_axum::helpers;
async fn login_handler(
Path(provider): Path<String>,
State(state): State<AppState>,
Query(params): Query<helpers::OAuthLoginParams>,
cookies: tower_cookies::Cookies,
) -> impl IntoResponse {
// We pass the framework extractors directly into the helper.
// The helper handles generating the state, nonce, and the 302 Redirect.
helpers::axum_login_handler::<AppState, _, _>(Path(provider), State(state), Query(params), cookies).await
}

The callback handler is where the stateless magic happens. Instead of relying on a session store, we use handle_oauth_callback_jwt_erased.

async fn callback_handler(
Path(provider): Path<String>,
State(state): State<AppState>,
Query(params): Query<helpers::OAuthCallbackParams>,
cookies: tower_cookies::Cookies,
) -> Result<impl IntoResponse, AxumError> {
// Extract the token manager (injected via .jwt_secret() earlier)
let token_manager = <Result<Arc<TokenManager>, AxumError>>::from_ref(&state)?;
// Resolve which provider (e.g. github vs google) was requested
let flow = state.auth.providers.get(&provider).unwrap();
// The helper exchanges the code for tokens and issues a custom JWT
helpers::handle_oauth_callback_jwt_erased(
flow.as_ref(),
cookies,
params,
token_manager,
3600, // Token TTL in seconds
state.auth.session_config
).await
}

By manually exposing these handlers, you gain absolute control over the HTTP response. You can append headers, write to custom databases, or return the JWT inside a JSON body.