Skip to main content

oci_rust_sdk/auth/
instance_principals.rs

1use crate::auth::federation::{generate_session_keypair, request_security_token};
2use crate::auth::imds::ImdsClient;
3use crate::auth::provider::AuthProvider;
4use crate::auth::x509_utils::extract_tenant_id;
5use crate::core::region::Region;
6use chrono::{DateTime, Duration, Utc};
7use std::str::FromStr;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11/// Session state for instance principals authentication
12struct SessionState {
13    security_token: String,
14    session_private_key_pem: String,
15    expires_at: DateTime<Utc>,
16}
17
18/// Instance Principals Authentication Provider
19///
20/// Enables applications running on OCI compute instances to authenticate
21/// using X.509 certificates from the Instance Metadata Service (IMDS).
22///
23/// # Example
24///
25/// ```no_run
26/// use oci_rust_sdk::auth::InstancePrincipalsAuthProvider;
27/// use std::sync::Arc;
28///
29/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
30/// // Initialize provider (auto-detects region and fetches certs from IMDS)
31/// let auth = InstancePrincipalsAuthProvider::new().await?;
32/// let auth_ref = Arc::new(auth);
33///
34/// // Use with OciClient
35/// // let client = OciClient::new(auth_ref, endpoint)?;
36/// # Ok(())
37/// # }
38/// ```
39pub struct InstancePrincipalsAuthProvider {
40    runtime_handle: tokio::runtime::Handle,
41    region: Region,
42    tenancy_id: String,
43    leaf_certificate: String,
44    leaf_private_key_pem: String,
45    intermediate_certificates: Vec<String>,
46    session_state: Arc<RwLock<SessionState>>,
47}
48
49impl InstancePrincipalsAuthProvider {
50    /// Create a new instance principals authentication provider
51    ///
52    /// This will:
53    /// 1. Fetch region and certificates from IMDS
54    /// 2. Extract tenant ID from certificate
55    /// 3. Generate initial session keypair
56    /// 4. Request initial security token from federation service
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if:
61    /// - IMDS is unavailable (not running on OCI instance)
62    /// - Certificate parsing fails
63    /// - Federation service is unreachable
64    /// - Token request fails
65    pub async fn new() -> crate::core::Result<Self> {
66        let runtime_handle = tokio::runtime::Handle::current();
67
68        // 1. Create IMDS client
69        let imds = ImdsClient::new()?;
70
71        // 2. Fetch credentials from IMDS
72        let region_str = imds.get_region().await?;
73        let region = Region::from_str(&region_str).map_err(|e| {
74            crate::core::OciError::ConfigError(format!("Invalid region from IMDS: {}", e))
75        })?;
76
77        let leaf_cert = imds.get_leaf_certificate().await?;
78        let leaf_key = imds.get_leaf_private_key().await?;
79        let intermediate_certs = imds.get_intermediate_certificates().await?;
80
81        // 3. Parse certificate and extract tenant ID
82        let tenancy_id = extract_tenant_id(&leaf_cert)?;
83
84        // 4. Generate initial session keypair
85        let session_keypair = generate_session_keypair()?;
86
87        // 5. Request initial security token
88        let security_token = request_security_token(
89            &region,
90            &tenancy_id,
91            &leaf_cert,
92            &leaf_key,
93            &intermediate_certs,
94            &session_keypair.public_key_pem,
95        )
96        .await?;
97
98        // 6. Initialize session state
99        let session_state = Arc::new(RwLock::new(SessionState {
100            security_token: security_token.token,
101            session_private_key_pem: session_keypair.private_key_pem,
102            expires_at: security_token.expires_at,
103        }));
104
105        Ok(Self {
106            runtime_handle,
107            region,
108            tenancy_id,
109            leaf_certificate: leaf_cert,
110            leaf_private_key_pem: leaf_key,
111            intermediate_certificates: intermediate_certs,
112            session_state,
113        })
114    }
115
116    /// Get the region for this instance
117    pub fn region(&self) -> Region {
118        self.region
119    }
120
121    /// Ensure the security token is valid, refreshing if necessary
122    async fn ensure_token_valid(&self) -> crate::core::Result<()> {
123        // Read lock - check expiration (with 5-minute buffer)
124        {
125            let state = self.session_state.read().await;
126            if !Self::is_expired(&state.expires_at) {
127                return Ok(());
128            }
129        }
130
131        // Write lock - refresh token
132        {
133            let mut state = self.session_state.write().await;
134
135            // Double-check expiration (another thread may have refreshed)
136            if !Self::is_expired(&state.expires_at) {
137                return Ok(());
138            }
139
140            // Generate new session keypair
141            let session_keypair = generate_session_keypair()?;
142
143            // Request new token
144            let security_token = request_security_token(
145                &self.region,
146                &self.tenancy_id,
147                &self.leaf_certificate,
148                &self.leaf_private_key_pem,
149                &self.intermediate_certificates,
150                &session_keypair.public_key_pem,
151            )
152            .await?;
153
154            // Update state
155            state.security_token = security_token.token;
156            state.session_private_key_pem = session_keypair.private_key_pem;
157            state.expires_at = security_token.expires_at;
158        }
159
160        Ok(())
161    }
162
163    /// Check if token is expired (with 5-minute buffer)
164    fn is_expired(expires_at: &DateTime<Utc>) -> bool {
165        let now = Utc::now();
166        let buffer = Duration::minutes(5);
167        now + buffer >= *expires_at
168    }
169}
170
171impl AuthProvider for InstancePrincipalsAuthProvider {
172    fn get_key_id(&self) -> String {
173        // Ensure token is valid (blocking on async operation)
174        self.runtime_handle
175            .block_on(self.ensure_token_valid())
176            .unwrap_or_else(|e| {
177                eprintln!("Warning: Failed to refresh token: {}", e);
178            });
179
180        // Return "ST$<token>" format
181        let state = self.session_state.blocking_read();
182        format!("ST${}", state.security_token)
183    }
184
185    fn get_private_key(&self) -> &str {
186        // Ensure token is valid (blocking on async operation)
187        self.runtime_handle
188            .block_on(self.ensure_token_valid())
189            .unwrap_or_else(|e| {
190                eprintln!("Warning: Failed to refresh token: {}", e);
191            });
192
193        // Return session private key
194        // We use Box::leak() to satisfy the &str lifetime requirement
195        // This creates a memory leak of ~2KB per refresh (once/hour)
196        // which is acceptable for server applications
197        let pem = self
198            .session_state
199            .blocking_read()
200            .session_private_key_pem
201            .clone();
202        Box::leak(pem.into_boxed_str())
203    }
204
205    fn get_passphrase(&self) -> Option<&str> {
206        // Instance principals don't use passphrases
207        None
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use chrono::Utc;
215
216    #[test]
217    fn test_is_expired() {
218        // Token expires in 10 minutes - should not be considered expired (buffer is 5 min)
219        let expires_at = Utc::now() + Duration::minutes(10);
220        assert!(!InstancePrincipalsAuthProvider::is_expired(&expires_at));
221
222        // Token expires in 4 minutes - should be considered expired (buffer is 5 min)
223        let expires_at = Utc::now() + Duration::minutes(4);
224        assert!(InstancePrincipalsAuthProvider::is_expired(&expires_at));
225
226        // Token already expired
227        let expires_at = Utc::now() - Duration::minutes(10);
228        assert!(InstancePrincipalsAuthProvider::is_expired(&expires_at));
229    }
230
231    #[test]
232    fn test_key_id_format() {
233        // The key ID should start with "ST$"
234        let token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.test.sig";
235        let key_id = format!("ST${}", token);
236        assert!(key_id.starts_with("ST$"));
237        assert!(key_id.contains("eyJ"));
238    }
239}