|
| 1 | +//! Example demonstrating OIDC discovery and OAuth 2.0 token exchange. |
| 2 | +//! |
| 3 | +//! This example shows how to: |
| 4 | +//! 1. Discover OIDC configuration from a provider |
| 5 | +//! 2. Exchange authorization codes for tokens |
| 6 | +//! 3. Refresh access tokens |
| 7 | +//! 4. Handle CORS and network errors with retries |
| 8 | +
|
| 9 | +use pmcp::client::auth::{OidcDiscoveryClient, TokenResponse}; |
| 10 | +use pmcp::server::auth::oauth2::{ |
| 11 | + InMemoryOAuthProvider, OAuthClient, OAuthProvider, OidcDiscoveryMetadata, |
| 12 | +}; |
| 13 | +use pmcp::Result; |
| 14 | +use std::time::Duration; |
| 15 | +use tokio::time::sleep; |
| 16 | + |
| 17 | +/// Mock OIDC server for testing. |
| 18 | +struct MockOidcServer { |
| 19 | + metadata: OidcDiscoveryMetadata, |
| 20 | +} |
| 21 | + |
| 22 | +impl MockOidcServer { |
| 23 | + fn new() -> Self { |
| 24 | + Self { |
| 25 | + metadata: OidcDiscoveryMetadata { |
| 26 | + issuer: "https://auth.example.com".to_string(), |
| 27 | + authorization_endpoint: "https://auth.example.com/authorize".to_string(), |
| 28 | + token_endpoint: "https://auth.example.com/token".to_string(), |
| 29 | + jwks_uri: Some("https://auth.example.com/jwks".to_string()), |
| 30 | + userinfo_endpoint: Some("https://auth.example.com/userinfo".to_string()), |
| 31 | + registration_endpoint: Some("https://auth.example.com/register".to_string()), |
| 32 | + revocation_endpoint: Some("https://auth.example.com/revoke".to_string()), |
| 33 | + introspection_endpoint: Some("https://auth.example.com/introspect".to_string()), |
| 34 | + response_types_supported: vec![pmcp::server::auth::oauth2::ResponseType::Code], |
| 35 | + grant_types_supported: vec![ |
| 36 | + pmcp::server::auth::oauth2::GrantType::AuthorizationCode, |
| 37 | + pmcp::server::auth::oauth2::GrantType::RefreshToken, |
| 38 | + ], |
| 39 | + scopes_supported: vec![ |
| 40 | + "openid".to_string(), |
| 41 | + "profile".to_string(), |
| 42 | + "email".to_string(), |
| 43 | + ], |
| 44 | + token_endpoint_auth_methods_supported: vec![ |
| 45 | + "client_secret_basic".to_string(), |
| 46 | + "client_secret_post".to_string(), |
| 47 | + ], |
| 48 | + code_challenge_methods_supported: vec!["plain".to_string(), "S256".to_string()], |
| 49 | + }, |
| 50 | + } |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +/// Simulate OIDC discovery with retry logic. |
| 55 | +async fn discover_with_retries(issuer_url: &str) -> Result<OidcDiscoveryMetadata> { |
| 56 | + println!("🔍 Discovering OIDC configuration for: {}", issuer_url); |
| 57 | + |
| 58 | + // Create discovery client with custom retry settings |
| 59 | + let _client = OidcDiscoveryClient::with_settings( |
| 60 | + 5, // max retries |
| 61 | + Duration::from_millis(500), // retry delay |
| 62 | + ); |
| 63 | + |
| 64 | + // Simulate network issues for demonstration |
| 65 | + let mut attempt = 0; |
| 66 | + loop { |
| 67 | + attempt += 1; |
| 68 | + println!(" Attempt {}/5...", attempt); |
| 69 | + |
| 70 | + // Simulate occasional CORS/network errors |
| 71 | + if attempt < 3 { |
| 72 | + println!(" ❌ Simulated CORS error"); |
| 73 | + sleep(Duration::from_millis(500)).await; |
| 74 | + continue; |
| 75 | + } |
| 76 | + |
| 77 | + // Return mock metadata on success |
| 78 | + println!(" ✅ Discovery successful!"); |
| 79 | + return Ok(MockOidcServer::new().metadata); |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +/// Simulate token exchange. |
| 84 | +async fn exchange_authorization_code( |
| 85 | + token_endpoint: &str, |
| 86 | + auth_code: &str, |
| 87 | + client_id: &str, |
| 88 | + client_secret: Option<&str>, |
| 89 | +) -> Result<TokenResponse> { |
| 90 | + println!("\n🔄 Exchanging authorization code for tokens"); |
| 91 | + println!(" Token endpoint: {}", token_endpoint); |
| 92 | + println!(" Auth code: {}...", &auth_code[..8.min(auth_code.len())]); |
| 93 | + println!(" Client ID: {}", client_id); |
| 94 | + println!(" Using client secret: {}", client_secret.is_some()); |
| 95 | + |
| 96 | + // Simulate token response |
| 97 | + sleep(Duration::from_millis(100)).await; |
| 98 | + |
| 99 | + Ok(TokenResponse { |
| 100 | + access_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...".to_string(), |
| 101 | + token_type: "Bearer".to_string(), |
| 102 | + expires_in: Some(3600), |
| 103 | + refresh_token: Some("refresh_token_abc123".to_string()), |
| 104 | + scope: Some("openid profile email".to_string()), |
| 105 | + }) |
| 106 | +} |
| 107 | + |
| 108 | +/// Simulate token refresh. |
| 109 | +async fn refresh_access_token( |
| 110 | + token_endpoint: &str, |
| 111 | + refresh_token: &str, |
| 112 | + client_id: &str, |
| 113 | +) -> Result<TokenResponse> { |
| 114 | + println!("\n🔄 Refreshing access token"); |
| 115 | + println!(" Token endpoint: {}", token_endpoint); |
| 116 | + println!( |
| 117 | + " Refresh token: {}...", |
| 118 | + &refresh_token[..10.min(refresh_token.len())] |
| 119 | + ); |
| 120 | + println!(" Client ID: {}", client_id); |
| 121 | + |
| 122 | + // Simulate token response |
| 123 | + sleep(Duration::from_millis(100)).await; |
| 124 | + |
| 125 | + Ok(TokenResponse { |
| 126 | + access_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.new...".to_string(), |
| 127 | + token_type: "Bearer".to_string(), |
| 128 | + expires_in: Some(3600), |
| 129 | + refresh_token: Some("refresh_token_xyz789".to_string()), |
| 130 | + scope: Some("openid profile email".to_string()), |
| 131 | + }) |
| 132 | +} |
| 133 | + |
| 134 | +/// Demonstrate OAuth provider setup. |
| 135 | +async fn setup_oauth_provider() -> Result<()> { |
| 136 | + println!("\n📦 Setting up OAuth provider"); |
| 137 | + |
| 138 | + let provider = InMemoryOAuthProvider::new("https://auth.example.com"); |
| 139 | + |
| 140 | + // Register a client |
| 141 | + let client = OAuthClient { |
| 142 | + client_id: "example-client".to_string(), |
| 143 | + client_secret: Some("super-secret".to_string()), |
| 144 | + client_name: "Example Client".to_string(), |
| 145 | + redirect_uris: vec!["https://app.example.com/callback".to_string()], |
| 146 | + grant_types: vec![ |
| 147 | + pmcp::server::auth::oauth2::GrantType::AuthorizationCode, |
| 148 | + pmcp::server::auth::oauth2::GrantType::RefreshToken, |
| 149 | + ], |
| 150 | + response_types: vec![pmcp::server::auth::oauth2::ResponseType::Code], |
| 151 | + scopes: vec!["openid".to_string(), "profile".to_string()], |
| 152 | + metadata: std::collections::HashMap::new(), |
| 153 | + }; |
| 154 | + |
| 155 | + let registered = provider.register_client(client).await?; |
| 156 | + println!(" ✅ Registered client: {}", registered.client_id); |
| 157 | + |
| 158 | + // Get provider metadata |
| 159 | + let metadata = provider.metadata().await?; |
| 160 | + println!(" 📋 Provider metadata:"); |
| 161 | + println!(" - Issuer: {}", metadata.issuer); |
| 162 | + println!(" - Auth endpoint: {}", metadata.authorization_endpoint); |
| 163 | + println!(" - Token endpoint: {}", metadata.token_endpoint); |
| 164 | + |
| 165 | + // Try OIDC discovery (will fail with default implementation) |
| 166 | + match provider.discover("https://auth.example.com").await { |
| 167 | + Ok(discovered) => { |
| 168 | + println!(" ✅ Discovery succeeded: {}", discovered.issuer); |
| 169 | + }, |
| 170 | + Err(e) => { |
| 171 | + println!(" ℹ️ Discovery not implemented (expected): {}", e); |
| 172 | + }, |
| 173 | + } |
| 174 | + |
| 175 | + Ok(()) |
| 176 | +} |
| 177 | + |
| 178 | +/// Demonstrate transport isolation. |
| 179 | +async fn demonstrate_transport_isolation() -> Result<()> { |
| 180 | + use pmcp::shared::protocol::{Protocol, ProtocolOptions, TransportId}; |
| 181 | + use pmcp::types::{JSONRPCResponse, RequestId}; |
| 182 | + |
| 183 | + println!("\n🔒 Demonstrating transport isolation"); |
| 184 | + |
| 185 | + // Create two separate transports |
| 186 | + let transport1 = TransportId::from_string("websocket-1".to_string()); |
| 187 | + let transport2 = TransportId::from_string("http-sse-1".to_string()); |
| 188 | + |
| 189 | + let mut protocol1 = Protocol::with_transport_id(ProtocolOptions::default(), transport1.clone()); |
| 190 | + let mut protocol2 = Protocol::with_transport_id(ProtocolOptions::default(), transport2.clone()); |
| 191 | + |
| 192 | + println!(" Created transport 1: {:?}", transport1); |
| 193 | + println!(" Created transport 2: {:?}", transport2); |
| 194 | + |
| 195 | + // Register the same request ID on both transports |
| 196 | + let request_id = RequestId::from("test-request"); |
| 197 | + let mut rx1 = protocol1.register_request(request_id.clone()); |
| 198 | + let mut rx2 = protocol2.register_request(request_id.clone()); |
| 199 | + |
| 200 | + println!(" Registered request '{}' on both transports", request_id); |
| 201 | + |
| 202 | + // Complete request for transport 1 |
| 203 | + let response1 = JSONRPCResponse::success( |
| 204 | + request_id.clone(), |
| 205 | + serde_json::json!({"source": "transport1"}), |
| 206 | + ); |
| 207 | + protocol1.complete_request(&request_id, response1).unwrap(); |
| 208 | + |
| 209 | + // Complete request for transport 2 |
| 210 | + let response2 = JSONRPCResponse::success( |
| 211 | + request_id.clone(), |
| 212 | + serde_json::json!({"source": "transport2"}), |
| 213 | + ); |
| 214 | + protocol2.complete_request(&request_id, response2).unwrap(); |
| 215 | + |
| 216 | + // Wait for async completion |
| 217 | + sleep(Duration::from_millis(50)).await; |
| 218 | + |
| 219 | + // Verify isolation |
| 220 | + match rx1.try_recv() { |
| 221 | + Ok(resp) => { |
| 222 | + println!(" ✅ Transport 1 received: {:?}", resp.result()); |
| 223 | + }, |
| 224 | + Err(_) => { |
| 225 | + println!(" ❌ Transport 1 didn't receive response"); |
| 226 | + }, |
| 227 | + } |
| 228 | + |
| 229 | + match rx2.try_recv() { |
| 230 | + Ok(resp) => { |
| 231 | + println!(" ✅ Transport 2 received: {:?}", resp.result()); |
| 232 | + }, |
| 233 | + Err(_) => { |
| 234 | + println!(" ❌ Transport 2 didn't receive response"); |
| 235 | + }, |
| 236 | + } |
| 237 | + |
| 238 | + Ok(()) |
| 239 | +} |
| 240 | + |
| 241 | +#[tokio::main] |
| 242 | +#[allow(clippy::result_large_err)] |
| 243 | +async fn main() -> Result<()> { |
| 244 | + println!("🚀 OIDC Discovery and OAuth 2.0 Example\n"); |
| 245 | + println!("{}", "=".repeat(50)); |
| 246 | + |
| 247 | + // 1. Discover OIDC configuration |
| 248 | + let metadata = discover_with_retries("https://auth.example.com").await?; |
| 249 | + |
| 250 | + println!("\n📋 Discovered Configuration:"); |
| 251 | + println!(" Issuer: {}", metadata.issuer); |
| 252 | + println!(" Authorization: {}", metadata.authorization_endpoint); |
| 253 | + println!(" Token: {}", metadata.token_endpoint); |
| 254 | + if let Some(jwks) = &metadata.jwks_uri { |
| 255 | + println!(" JWKS: {}", jwks); |
| 256 | + } |
| 257 | + if let Some(userinfo) = &metadata.userinfo_endpoint { |
| 258 | + println!(" UserInfo: {}", userinfo); |
| 259 | + } |
| 260 | + println!(" Supported scopes: {:?}", metadata.scopes_supported); |
| 261 | + |
| 262 | + // 2. Exchange authorization code for tokens |
| 263 | + let tokens = exchange_authorization_code( |
| 264 | + &metadata.token_endpoint, |
| 265 | + "auth_code_from_callback", |
| 266 | + "example-client", |
| 267 | + Some("client-secret"), |
| 268 | + ) |
| 269 | + .await?; |
| 270 | + |
| 271 | + println!("\n🎫 Received Tokens:"); |
| 272 | + println!(" Access token: {}...", &tokens.access_token[..20]); |
| 273 | + println!(" Token type: {}", tokens.token_type); |
| 274 | + println!(" Expires in: {:?} seconds", tokens.expires_in); |
| 275 | + if let Some(refresh) = &tokens.refresh_token { |
| 276 | + println!(" Refresh token: {}...", &refresh[..10]); |
| 277 | + } |
| 278 | + if let Some(scope) = &tokens.scope { |
| 279 | + println!(" Scope: {}", scope); |
| 280 | + } |
| 281 | + |
| 282 | + // 3. Refresh the access token |
| 283 | + if let Some(refresh_token) = tokens.refresh_token { |
| 284 | + let new_tokens = |
| 285 | + refresh_access_token(&metadata.token_endpoint, &refresh_token, "example-client") |
| 286 | + .await?; |
| 287 | + |
| 288 | + println!("\n🔄 Refreshed Tokens:"); |
| 289 | + println!(" New access token: {}...", &new_tokens.access_token[..20]); |
| 290 | + println!(" Expires in: {:?} seconds", new_tokens.expires_in); |
| 291 | + } |
| 292 | + |
| 293 | + // 4. Setup OAuth provider |
| 294 | + setup_oauth_provider().await?; |
| 295 | + |
| 296 | + // 5. Demonstrate transport isolation |
| 297 | + demonstrate_transport_isolation().await?; |
| 298 | + |
| 299 | + println!("\n✅ Example completed successfully!"); |
| 300 | + |
| 301 | + Ok(()) |
| 302 | +} |
0 commit comments