axum-oidc-client 0.1.0

OpenID Connect (OIDC) and OAuth2 client middleware for Axum web framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
# axum-oidc-client API Documentation

Complete API documentation and usage guide for the axum-oidc-client library.

## Table of Contents

1. [Overview]#overview
2. [Core Concepts]#core-concepts
3. [API Reference]#api-reference
4. [Usage Patterns]#usage-patterns
5. [Security Guidelines]#security-guidelines
6. [Examples]#examples

## Overview

`axum-oidc-client` is a comprehensive OAuth2/OIDC authentication library for Axum web applications. It provides:

- Full OAuth2 and OpenID Connect protocol support
- PKCE (Proof Key for Code Exchange) for enhanced security
- Automatic ID token and access token refresh using OAuth2 refresh token flow
- Pluggable cache backends
- Encrypted session management
- Type-safe extractors with automatic ID token and access token refresh
- Flexible logout handlers

## Core Concepts

### Authentication Flow

1. **User Request** → User accesses a protected route
2. **Auth Check** → Middleware checks for valid session
3. **Redirect to Provider** → If not authenticated, redirect to OAuth provider
4. **User Authenticates** → User logs in at provider
5. **Callback** → Provider redirects to `/auth/callback` with code
6. **Token Exchange** → Application exchanges code for tokens (with PKCE)
7. **Session Creation** → Session stored in cache, cookie set
8. **Access Granted** → User redirected to original route

### Token Refresh Flow

When a token expires, the library automatically handles refresh:

1. **Expiration Detection** → Extractor checks if token is expired
2. **Refresh Request** → Uses refresh token to request new access token
3. **Token Update** → Receives new access token and expiration time
4. **Session Update** → Updates session with new token information
5. **Cache Sync** → Saves updated session to cache
6. **Transparent Access** → Handler receives fresh token automatically

### PKCE (Proof Key for Code Exchange)

PKCE enhances OAuth2 security by:

- Generating a cryptographic random string (code verifier)
- Creating a challenge from the verifier (using S256 or Plain method)
- Sending the challenge during authorization
- Sending the verifier during token exchange
- Provider validates verifier matches challenge

## API Reference

### Module: `auth`

Core authentication types and middleware.

#### `AuthLayer`

Tower layer that adds OAuth2 authentication to your Axum application.

```rust
pub struct AuthLayer {
    configuration: Arc<OAuthConfiguration>,
    cache: Arc<dyn AuthCache + Send + Sync>,
    logout_handler: Arc<dyn LogoutHandler>,
}

impl AuthLayer {
    pub fn new(
        configuration: Arc<OAuthConfiguration>,
        cache: Arc<dyn AuthCache + Send + Sync>,
        logout_handler: Arc<dyn LogoutHandler>,
    ) -> Self
}
```

**Usage:**

```rust
let layer = AuthLayer::new(config, cache, logout_handler);
app.layer(layer)
```

**Methods:**

- `new()` - Create a new AuthLayer
- `with_logout_handler()` - Alias for `new()` (backwards compatibility)

#### `OAuthConfiguration`

OAuth2/OIDC configuration container.

**Fields:**

- `private_cookie_key: Key` - Session encryption key
- `client_id: String` - OAuth2 client identifier
- `base_path: String` - Base path for authentication routes (default: "/auth")
- `client_secret: String` - OAuth2 client secret
- `redirect_uri: String` - Callback URI
- `authorization_endpoint: String` - Provider's auth endpoint
- `token_endpoint: String` - Provider's token endpoint
- `end_session_endpoint: Option<String>` - OIDC logout endpoint
- `post_logout_redirect_uri: String` - Post-logout redirect
- `scopes: String` - Requested scopes (space-separated)
- `code_challenge_method: CodeChallengeMethod` - PKCE method
- `custom_ca_cert: Option<String>` - Custom CA certificate path
- `session_max_age: i64` - Session duration (minutes)
- `token_max_age: Option<i64>` - Token duration (seconds)

#### `CodeChallengeMethod`

PKCE code challenge methods.

```rust
pub enum CodeChallengeMethod {
    S256,   // SHA-256 (recommended)
    Plain,  // Plain text (not recommended)
}
```

#### `LogoutHandler` Trait

Customize logout behavior.

```rust
pub trait LogoutHandler: Send + Sync {
    fn handle_logout<'a>(
        &'a self,
        parts: &'a mut Parts,
        configuration: Arc<OAuthConfiguration>,
        cache: Arc<dyn AuthCache + Send + Sync>,
    ) -> BoxFuture<'a, Result<Response, Error>>;
}
```

**Implementations:**

- `DefaultLogoutHandler` - Simple logout with redirect
- `OidcLogoutHandler` - OIDC logout with provider notification

### Module: `auth_builder`

Builder pattern for OAuth configuration.

#### `OAuthConfigurationBuilder`

Fluent API for building configurations.

**Methods:**

| Method                               | Required | Description                                        |
| ------------------------------------ | -------- | -------------------------------------------------- |
| `with_client_id(id)`                 | Yes      | Set OAuth2 client ID                               |
| `with_client_secret(secret)`         | Yes      | Set OAuth2 client secret                           |
| `with_redirect_uri(uri)`             | Yes      | Set callback URI                                   |
| `with_authorization_endpoint(url)`   | Yes      | Set auth endpoint                                  |
| `with_token_endpoint(url)`           | Yes      | Set token endpoint                                 |
| `with_private_cookie_key(key)`       | Yes      | Set session encryption key                         |
| `with_session_max_age(minutes)`      | Yes      | Set session duration                               |
| `with_scopes(scopes)`                | No       | Set OAuth scopes (default: openid, email, profile) |
| `with_code_challenge_method(method)` | No       | Set PKCE method (default: S256)                    |
| `with_end_session_endpoint(url)`     | No       | Set OIDC logout endpoint                           |
| `with_post_logout_redirect_uri(uri)` | No       | Set post-logout redirect                           |
| `with_custom_ca_cert(path)`          | No       | Set custom CA certificate                          |
| `with_token_max_age(seconds)`        | No       | Set token max age                                  |
| `build()`                            | -        | Build the configuration                            |

**Example:**

```rust
let config = OAuthConfigurationBuilder::default()
    .with_client_id("client-id")
    .with_client_secret("client-secret")
    .with_redirect_uri("http://localhost:8080/auth/callback")
    .with_authorization_endpoint("https://provider.com/oauth/authorize")
    .with_token_endpoint("https://provider.com/oauth/token")
    .with_private_cookie_key("secret-key")
    .with_session_max_age(30)
    .with_base_path("/auth")  // Optional, default is "/auth"
    .build()?;
```

**Custom Base Path Example:**

```rust
let config = OAuthConfigurationBuilder::default()
    .with_base_path("/api/auth")  // Custom base path
    .with_redirect_uri("http://localhost:8080/api/auth/callback")  // Match base_path
    // ... other config
    .build()?;
```

### Module: `auth_cache`

Cache trait and implementations.

#### `AuthCache` Trait

```rust
#[async_trait]
pub trait AuthCache {
    async fn get(&self, key: &str) -> Option<String>;
    async fn set(&self, key: &str, value: &str);
    async fn delete(&self, key: &str);
}
```

**Built-in Implementations:**

**Redis Cache** (requires `redis` feature):

```rust
use axum_oidc_client::redis::AuthCache;

let cache = AuthCache::new("redis://127.0.0.1/", 3600);
```

**Custom Implementation:**

```rust
struct MyCache { /* ... */ }

#[async_trait]
impl AuthCache for MyCache {
    async fn get(&self, key: &str) -> Option<String> {
        // Implementation
    }

    async fn set(&self, key: &str, value: &str) {
        // Implementation
    }

    async fn delete(&self, key: &str) {
        // Implementation
    }
}
```

### Module: `auth_session`

Session management and token storage.

#### `AuthSession`

Contains authenticated user's session data.

**Fields:**

```rust
pub struct AuthSession {
    pub id_token: String,
    pub access_token: String,
    pub token_type: String,
    pub refresh_token: String,
    pub scope: String,
    pub expires: DateTime<Local>,
}
```

**Auto-Refresh:**
The `AuthSession` extractor automatically refreshes expired tokens when used in route handlers. If the session's access token has expired, the extractor:

1. Uses the refresh token to obtain a new access token
2. Updates all token fields (access_token, id_token if provided, expires)
3. Saves the updated session to cache
4. Returns the refreshed session to your handler

This means you never need to manually check expiration or refresh tokens.

**Usage as Extractor:**

```rust
async fn protected(session: AuthSession) -> String {
    format!("Token expires: {}", session.expires)
}
```

### Module: `extractors`

Type-safe extractors for route handlers with automatic ID token and access token refresh support.

All extractors automatically check token expiration and refresh ID tokens and access tokens when needed, providing seamless token management without manual intervention.

#### `AuthSession`

Requires authentication. Redirects to OAuth if not authenticated. Automatically refreshes expired ID token and access token.

```rust
async fn protected_route(session: AuthSession) -> String {
    // ID token and access token are automatically refreshed if expired
    format!("Hello! Your token: {}", session.access_token)
}
```

#### `AccessToken`

Extracts only the access token with automatic refresh if expired.

```rust
use axum_oidc_client::extractors::AccessToken;

async fn api_call(token: AccessToken) -> String {
    // Access token is automatically refreshed if expired
    format!("API call with: {}", *token)
}
```

#### `IdToken`

Extracts only the ID token with automatic refresh if expired.

```rust
use axum_oidc_client::extractors::IdToken;

async fn user_info(token: IdToken) -> String {
    // ID token is automatically refreshed if expired
    format!("User ID: {}", *token)
}
```

#### `OptionalAccessToken`

Optional access token for public routes with automatic refresh if expired.

```rust
use axum_oidc_client::extractors::OptionalAccessToken;

async fn maybe_protected(OptionalAccessToken(token): OptionalAccessToken) -> String {
    match token {
        Some(access_token) => format!("Authenticated with: {}", access_token),
        None => format!("Public access"),
    }
}
```

#### `OptionalIdToken`

Optional ID token for public routes with automatic refresh if expired.

```rust
use axum_oidc_client::extractors::OptionalIdToken;

async fn public_route(OptionalIdToken(token): OptionalIdToken) -> String {
    match token {
        Some(id_token) => format!("Welcome back!"),
        None => format!("Please log in"),
    }
}
```

### Module: `logout`

Logout handler implementations.

#### `DefaultLogoutHandler`

Simple local logout with session cleanup and redirect.

**When to use:**

- The OAuth provider doesn't support OIDC logout (e.g., Google, GitHub)
- You only need to clear the local session without notifying the provider
- You're implementing custom logout logic

```rust
use axum_oidc_client::logout::handle_default_logout::DefaultLogoutHandler;

let handler = Arc::new(DefaultLogoutHandler);
```

**Behavior:**

1. Removes session cookie
2. Deletes session from cache
3. Redirects to `post_logout_redirect_uri` (default: "/")

#### `OidcLogoutHandler`

OIDC-compliant logout with provider notification (RP-Initiated Logout).

**When to use:**

- The OAuth provider supports OIDC RP-Initiated Logout (e.g., Keycloak, Azure AD, Okta, Auth0)
- You need to end the session at the provider
- You want single logout across multiple applications

```rust
use axum_oidc_client::logout::handle_oidc_logout::OidcLogoutHandler;

let handler = Arc::new(OidcLogoutHandler::new(
    "https://provider.com/oidc/logout"
));
```

**Behavior:**

1. Removes session cookie
2. Deletes session from cache
3. Redirects to provider's `end_session_endpoint` with `id_token_hint`
4. Provider logs out user and redirects to `post_logout_redirect_uri`

#### Custom `LogoutHandler`

You can implement the `LogoutHandler` trait to create custom logout behavior:

```rust
use axum_oidc_client::auth::{LogoutHandler, OAuthConfiguration, SESSION_KEY};
use axum_oidc_client::auth_cache::AuthCache;
use axum_oidc_client::errors::Error;
use axum::response::{Redirect, IntoResponse, Response};
use axum_extra::extract::{cookie::Cookie, PrivateCookieJar};
use futures_util::future::BoxFuture;
use http::request::Parts;
use std::sync::Arc;

struct CustomLogoutHandler {
    custom_redirect: String,
}

impl LogoutHandler for CustomLogoutHandler {
    fn handle_logout<'a>(
        &'a self,
        parts: &'a mut Parts,
        configuration: Arc<OAuthConfiguration>,
        cache: Arc<dyn AuthCache + Send + Sync>,
    ) -> BoxFuture<'a, Result<Response, Error>> {
        Box::pin(async move {
            // Custom logout logic: audit logging, custom redirects, etc.

            // Clean up session (similar to DefaultLogoutHandler)
            let jar = PrivateCookieJar::from_headers(
                &parts.headers,
                configuration.private_cookie_key.clone(),
            );

            if let Some(session_cookie) = jar.get(SESSION_KEY) {
                cache.invalidate_auth_session(session_cookie.value()).await?;
            }

            let jar = jar.remove(Cookie::build(SESSION_KEY).path("/"));

            Ok((jar, Redirect::to(&self.custom_redirect)).into_response())
        })
    }
}
```

### Module: `errors`

Error types used throughout the library.

```rust
pub enum Error {
    MissingParameter(String),
    InvalidToken,
    CacheError,
    NetworkError,
    // ... more variants
}
```

## Usage Patterns

### Automatic ID Token and Access Token Refresh

The library provides automatic ID token and access token refresh without any manual intervention required.

#### How Token Refresh Works

```rust
use axum_oidc_client::{auth_session::AuthSession, extractors::AccessToken};

// Example 1: Full session with automatic token refresh
async fn dashboard(session: AuthSession) -> String {
    // If ID token and access token expired, they're automatically refreshed before this handler runs
    // You always get valid, fresh tokens
    format!("Token expires at: {}", session.expires)
}

// Example 2: Access token only with automatic refresh
async fn api_endpoint(token: AccessToken) -> String {
    // Access token is automatically refreshed if expired
    // You can safely use it for API calls
    call_external_api(&token).await
}
```

#### Refresh Process

When an extractor detects expired ID token and access token:

1. **Check Expiration**: Compares `session.expires` with current time
2. **Refresh Request**: POSTs to token endpoint with refresh token:
   ```
   grant_type=refresh_token
   refresh_token={session.refresh_token}
   client_id={config.client_id}
   ```
3. **Update Session**: Updates session with new tokens:
   - `access_token` - Always updated with new access token
   - `id_token` - Updated with new ID token if provider returns it
   - `refresh_token` - Updated if provider returns new refresh token
   - `expires` - Calculated from new `expires_in`
4. **Save to Cache**: Persists updated session
5. **Return Fresh Tokens**: Handler receives valid ID token and access token

#### Error Handling

If ID token and access token refresh fails (e.g., refresh token expired or revoked):

```rust
// The extractor will return an error response
// User will be redirected to re-authenticate
async fn protected(session: AuthSession) -> String {
    // If refresh fails, user never reaches here
    // They're automatically redirected to OAuth provider
    format!("Valid session with fresh tokens: {} / {}", session.access_token, session.id_token)
}
```

### Basic Application

```rust
use axum::{Router, routing::get};
use axum_oidc_client::{
    auth::AuthLayer,
    auth_builder::OAuthConfigurationBuilder,
    logout::handle_default_logout::DefaultLogoutHandler,
};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build configuration
    let config = OAuthConfigurationBuilder::default()
        .with_client_id(std::env::var("OAUTH_CLIENT_ID")?)
        .with_client_secret(std::env::var("OAUTH_CLIENT_SECRET")?)
        .with_redirect_uri("http://localhost:8080/auth/callback")
        .with_authorization_endpoint("https://provider.com/authorize")
        .with_token_endpoint("https://provider.com/token")
        .with_private_cookie_key(&std::env::var("COOKIE_KEY")?)
        .with_session_max_age(30)
        .build()?;

    // Create cache
    let cache = Arc::new(
        axum_oidc_client::redis::AuthCache::new("redis://127.0.0.1/", 3600)
    );

    // Create logout handler
    let logout_handler = Arc::new(DefaultLogoutHandler);

    // Build app
    let app = Router::new()
        .route("/", get(home))
        .route("/protected", get(protected))
        .layer(AuthLayer::new(Arc::new(config), cache, logout_handler));

    // Start server
    let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await?;
    axum::serve(listener, app).await?;

    Ok(())
}

async fn home() -> &'static str {
    "Home Page"
}

async fn protected(session: axum_oidc_client::auth_session::AuthSession) -> String {
    format!("Protected! Expires: {}", session.expires)
}
```

### Public and Protected Routes

```rust
use axum_oidc_client::{
    auth_session::AuthSession,
    extractors::{AccessToken, OptionalIdToken}
};

// Public route with optional auth
async fn home(OptionalIdToken(token): OptionalIdToken) -> Html<String> {
    let content = match token {
        Some(_) => r#"
            <a href="/protected">Go to Protected Area</a>
            <a href="/auth/logout">Logout</a>
        "#,
        None => r#"
            <a href="/auth">Login</a>
        "#,
    };
    Html(format!("<html><body>{}</body></html>", content))
}

// Protected route - requires auth with full session
async fn protected(session: AuthSession) -> String {
    // ID token and access token automatically refreshed if expired
    format!("Welcome! Token type: {}", session.token_type)
}

// Protected route - requires auth with just access token
async fn api_data(token: AccessToken) -> String {
    // Access token automatically refreshed if expired
    format!("Fetching data with token: {}", *token)
}
```

### Using Different Extractors

```rust
use axum_oidc_client::{
    auth_session::AuthSession,
    extractors::{AccessToken, IdToken, OptionalAccessToken}
};

// Use AuthSession when you need full session info
async fn dashboard(session: AuthSession) -> String {
    format!(
        "Session info:\n\
         Token Type: {}\n\
         Expires: {}\n\
         Scopes: {}",
        session.token_type,
        session.expires,
        session.scope
    )
}

// Use AccessToken for API calls
async fn external_api(token: AccessToken) -> Result<String, Error> {
    let client = reqwest::Client::new();
    let response = client
        .get("https://api.example.com/data")
        .bearer_auth(&*token)  // Access token is automatically fresh
        .send()
        .await?;
    Ok(response.text().await?)
}

// Use IdToken to get user identity
async fn user_profile(token: IdToken) -> String {
    // Decode ID token to get user info
    format!("User ID token: {}", *token)
}

// Use OptionalAccessToken for mixed public/private content
async fn personalized_content(OptionalAccessToken(token): OptionalAccessToken) -> String {
    if let Some(access_token) = token {
        // User is authenticated, show personalized content
        format!("Personalized content for user")
    } else {
        // User not authenticated, show public content
        format!("Public content")
    }
}
```

### Custom Logout Handler

```rust
use axum_oidc_client::auth::LogoutHandler;
use futures_util::future::BoxFuture;

struct CustomLogoutHandler {
    custom_redirect: String,
}

impl LogoutHandler for CustomLogoutHandler {
    fn handle_logout<'a>(
        &'a self,
        parts: &'a mut Parts,
        configuration: Arc<OAuthConfiguration>,
        cache: Arc<dyn AuthCache + Send + Sync>,
    ) -> BoxFuture<'a, Result<Response, Error>> {
        Box::pin(async move {
            // Custom logout logic
            // 1. Log the logout event
            println!("User logging out...");

            // 2. Clean up session (similar to default handler)
            // ... session cleanup code ...

            // 3. Redirect to custom location
            Ok(Redirect::to(&self.custom_redirect).into_response())
        })
    }
}
```

### Environment-based Configuration

```rust
use dotenv::dotenv;
use std::env;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    dotenv().ok();

    let config = OAuthConfigurationBuilder::default()
        .with_client_id(&env::var("OAUTH_CLIENT_ID")?)
        .with_client_secret(&env::var("OAUTH_CLIENT_SECRET")?)
        .with_redirect_uri(&env::var("OAUTH_REDIRECT_URI")?)
        .with_authorization_endpoint(&env::var("OAUTH_AUTH_ENDPOINT")?)
        .with_token_endpoint(&env::var("OAUTH_TOKEN_ENDPOINT")?)
        .with_private_cookie_key(&env::var("PRIVATE_COOKIE_KEY")?)
        .with_session_max_age(
            env::var("SESSION_MAX_AGE")?.parse().unwrap_or(30)
        )
        .build()?;

    // ... rest of app
    Ok(())
}
```

## Security Guidelines

### 1. PKCE Code Challenge Method

**Always use S256 in production:**

```rust
// ✅ Recommended
.with_code_challenge_method(CodeChallengeMethod::S256)

// ❌ Not recommended for production
.with_code_challenge_method(CodeChallengeMethod::Plain)
```

### 2. Private Cookie Key

**Generate strong random keys:**

```bash
# Generate a secure key
openssl rand -base64 64
```

```rust
// ✅ Good - Use environment variable with generated key
.with_private_cookie_key(&env::var("PRIVATE_COOKIE_KEY")?)

// ❌ Bad - Hardcoded weak key
.with_private_cookie_key("my-secret-key")
```

### 3. HTTPS in Production

**Use HTTPS for all OAuth endpoints:**

```rust
// ✅ Production
.with_redirect_uri("https://myapp.com/auth/callback")
.with_authorization_endpoint("https://provider.com/authorize")

// ⚠️ Development only
.with_redirect_uri("http://localhost:8080/auth/callback")
```

### 4. Session Expiration

**Configure appropriate timeouts:**

```rust
// Balance security and user experience
.with_session_max_age(30)    // 30 minutes session
.with_token_max_age(300)     // 5 minutes token max age
```

### 5. Scope Minimization

**Request only necessary scopes:**

```rust
// ✅ Good - Only request what you need
.with_scopes(vec!["openid", "email"])

// ❌ Bad - Requesting unnecessary permissions
.with_scopes(vec!["openid", "email", "profile", "admin", "write:all"])
```

### 6. Redirect URI Validation

**Ensure redirect URI matches provider configuration:**

```rust
// Must exactly match what's configured in OAuth provider
.with_redirect_uri("https://myapp.com/auth/callback")
```

### 7. Error Handling

**Don't leak sensitive information in errors:**

```rust
match result {
    Ok(session) => { /* ... */ },
    Err(e) => {
        // ❌ Bad - Exposes details
        eprintln!("Auth error: {:?}", e);

        // ✅ Good - Log internally, show generic message to user
        log::error!("Authentication failed: {:?}", e);
        return "Authentication failed. Please try again.";
    }
}
```

## Examples

### Google OAuth

Google supports OAuth2 but **does not implement OIDC logout**. Use `DefaultLogoutHandler`.

```rust
use axum_oidc_client::logout::handle_default_logout::DefaultLogoutHandler;

let config = OAuthConfigurationBuilder::default()
    .with_authorization_endpoint("https://accounts.google.com/o/oauth2/auth")
    .with_token_endpoint("https://oauth2.googleapis.com/token")
    .with_client_id("your-client-id.apps.googleusercontent.com")
    .with_client_secret("your-client-secret")
    .with_redirect_uri("http://localhost:8080/auth/callback")
    .with_private_cookie_key(&env::var("COOKIE_KEY")?)
    .with_scopes(vec!["openid", "email", "profile"])
    .with_session_max_age(30)
    // Note: DO NOT set end_session_endpoint for Google
    .build()?;

let logout_handler = Arc::new(DefaultLogoutHandler);
```

### GitHub OAuth

GitHub uses OAuth2 (not OIDC). Use `DefaultLogoutHandler`.

```rust
use axum_oidc_client::logout::handle_default_logout::DefaultLogoutHandler;

let config = OAuthConfigurationBuilder::default()
    .with_authorization_endpoint("https://github.com/login/oauth/authorize")
    .with_token_endpoint("https://github.com/login/oauth/access_token")
    .with_client_id("your-github-client-id")
    .with_client_secret("your-github-client-secret")
    .with_redirect_uri("http://localhost:8080/auth/callback")
    .with_private_cookie_key(&env::var("COOKIE_KEY")?)
    .with_scopes(vec!["read:user", "user:email"])
    .with_session_max_age(30)
    // Note: GitHub doesn't support OIDC logout
    .build()?;

let logout_handler = Arc::new(DefaultLogoutHandler);
```

### Keycloak

Keycloak fully supports OIDC including RP-Initiated Logout. Use `OidcLogoutHandler`.

```rust
use axum_oidc_client::logout::handle_oidc_logout::OidcLogoutHandler;

let realm = "your-realm";
let keycloak_url = "https://keycloak.example.com";

let config = OAuthConfigurationBuilder::default()
    .with_authorization_endpoint(&format!("{}/realms/{}/protocol/openid-connect/auth", keycloak_url, realm))
    .with_token_endpoint(&format!("{}/realms/{}/protocol/openid-connect/token", keycloak_url, realm))
    .with_end_session_endpoint(&format!("{}/realms/{}/protocol/openid-connect/logout", keycloak_url, realm))
    .with_client_id("your-client-id")
    .with_client_secret("your-client-secret")
    .with_redirect_uri("http://localhost:8080/auth/callback")
    .with_post_logout_redirect_uri("http://localhost:8080")
    .with_private_cookie_key(&env::var("COOKIE_KEY")?)
    .with_scopes(vec!["openid", "email", "profile"])
    .with_session_max_age(30)
    .build()?;

// Use OidcLogoutHandler with the end_session_endpoint
let logout_handler = Arc::new(OidcLogoutHandler::new(
    &format!("{}/realms/{}/protocol/openid-connect/logout", keycloak_url, realm)
));
```

### Microsoft Azure AD

Azure AD supports OIDC logout. Use `OidcLogoutHandler`.

```rust
use axum_oidc_client::logout::handle_oidc_logout::OidcLogoutHandler;

let tenant_id = "common"; // or specific tenant ID

let config = OAuthConfigurationBuilder::default()
    .with_authorization_endpoint(&format!(
        "https://login.microsoftonline.com/{}/oauth2/v2.0/authorize",
        tenant_id
    ))
    .with_token_endpoint(&format!(
        "https://login.microsoftonline.com/{}/oauth2/v2.0/token",
        tenant_id
    ))
    .with_end_session_endpoint(&format!(
        "https://login.microsoftonline.com/{}/oauth2/v2.0/logout",
        tenant_id
    ))
    .with_client_id("your-azure-client-id")
    .with_client_secret("your-azure-client-secret")
    .with_redirect_uri("http://localhost:8080/auth/callback")
    .with_post_logout_redirect_uri("http://localhost:8080")
    .with_private_cookie_key(&env::var("COOKIE_KEY")?)
    .with_scopes(vec!["openid", "email", "profile"])
    .with_session_max_age(30)
    .build()?;

// Use OidcLogoutHandler for Azure AD
let logout_handler = Arc::new(OidcLogoutHandler::new(
    &format!("https://login.microsoftonline.com/{}/oauth2/v2.0/logout", tenant_id)
));
```

### Provider Compatibility Summary

| Provider | OIDC Support   | Logout Support         | Recommended Handler    |
| -------- | -------------- | ---------------------- | ---------------------- |
| Google   | Partial        | ❌ No OIDC logout      | `DefaultLogoutHandler` |
| GitHub   | ❌ OAuth2 only | ❌ No OIDC logout      | `DefaultLogoutHandler` |
| Keycloak | ✅ Full        | ✅ RP-Initiated Logout | `OidcLogoutHandler`    |
| Azure AD | ✅ Full        | ✅ RP-Initiated Logout | `OidcLogoutHandler`    |
| Okta     | ✅ Full        | ✅ RP-Initiated Logout | `OidcLogoutHandler`    |
| Auth0    | ✅ Full        | ✅ RP-Initiated Logout | `OidcLogoutHandler`    |

```

## Automatic Routes

The `AuthLayer` adds these routes automatically:

| Route                         | Method | Description                                       |
| ----------------------------- | ------ | ------------------------------------------------- |
| `/auth`                       | GET    | Initiates OAuth flow, redirects to provider       |
| `/auth/callback`              | GET    | Handles OAuth callback, exchanges code for tokens |
| `/auth/logout`                | GET    | Logs out user, clears session                     |
| `/auth/logout?redirect=/path` | GET    | Logs out and redirects to custom path             |

## Troubleshooting

### Common Issues

**Issue: "Missing parameter" error**

```

Solution: Ensure all required configuration is set before calling build()

```

**Issue: Session not persisting**

```

Solution: Check Redis connection and ensure cookies are enabled

```

**Issue: Redirect loop**

```

Solution: Verify redirect_uri matches exactly in provider settings

```

**Issue: Token expired too quickly**

```

Solution: Adjust session_max_age and token_max_age settings

```

**Issue: ID token and access token refresh failing**

```

Solution:

1. Ensure your OAuth provider supports refresh tokens for obtaining new ID tokens and access tokens
2. Check that the 'offline_access' or equivalent scope is requested
3. Verify refresh_token is being stored in session
4. Check provider logs for refresh token errors

```

**Issue: Frequent re-authentication required**

```

Solution:

1. Verify refresh token is being returned by provider
2. Check token_max_age isn't set too low (tokens will refresh frequently)
3. Ensure cache is properly storing updated sessions with refreshed tokens
4. Verify provider's refresh token expiration policy

```

## Additional Resources

- [RFC 6749 - OAuth 2.0]https://tools.ietf.org/html/rfc6749
- [RFC 7636 - PKCE]https://tools.ietf.org/html/rfc7636
- [OpenID Connect Core]https://openid.net/specs/openid-connect-core-1_0.html
- [Axum Documentation]https://docs.rs/axum

---

**Last Updated:** 2024
**Version:** 0.1.0
```