ones_oidc/
oidc.rs

1use super::device::{get_jwks, make_device_jwt, make_device_jwt_ciba, request_device_access_token, device_access_token};
2use super::oidc_types::JwtPayload;
3use super::oidc_types::{
4    AuthenticationMethod, AuthenticationResult, CibaResponse, CibaStatusResponse,
5    ClientCredentialsIntrospection, SubjectIdentity,
6};
7use super::{AuthenticatedEntityKind, LoginHint};
8use super::http_client::default_client;
9use super::config::OnesOidcConfig;
10use crate::errors::{DeviceError, OidcError, OidcRequirementsError};
11use crate::oidc_backend::QrStatusRequest;
12use crate::oidc_types::{LoginHintKind, OidcErrorResponse, QrAuthSessionIdp};
13use jsonwebtoken::jwk::JwkSet;
14use jsonwebtoken::EncodingKey;
15use log::debug;
16use openidconnect::core::{CoreProviderMetadata, CoreTokenType};
17use openidconnect::{
18    AccessToken, ClientId, EmptyExtraTokenFields, IssuerUrl, OAuth2TokenResponse, StandardTokenResponse
19};
20use serde::de::DeserializeOwned;
21use std::str::FromStr;
22use uuid::Uuid;
23
24/// Configuration for CIBA request parameters
25#[derive(Debug, Clone)]
26pub struct CibaRequestConfig {
27    pub login_hint: LoginHint,
28    pub scope: String,
29    pub binding_message: String,
30    pub resource: Option<String>,
31    pub qr_session_id: Option<String>,
32}
33
34/**
35 * Handle OIDC response and parse it to the specified type or return an appropriate error
36 */
37async fn handle_oidc_response<T>(response: Result<reqwest::Response, reqwest::Error>) -> Result<T, OidcError> 
38where 
39    T: DeserializeOwned,
40{
41    match response {
42        Ok(res) => {
43            let status = res.status();
44            let status_code = status.as_u16();
45            
46            if status.is_success() {
47                match res.json::<T>().await {
48                    Ok(parsed_response) => Ok(parsed_response),
49                    Err(e) => Err(OidcError::RequestErrorWithDetails {
50                        status_code,
51                        error: "request_error".to_string(),
52                        error_description: format!("Failed to parse response: {}", e),
53                    }),
54                }
55            } else {
56                // Get the response body as text
57                match res.text().await {
58                    Ok(body) => {
59                        // Try to parse as JSON
60                        match serde_json::from_str::<OidcErrorResponse>(&body) {
61                            Ok(error_response) => {
62                                if error_response.error == "authorization_pending" {
63                                    Err(OidcError::CibaAuthenticationPending)
64                                } else {
65                                    Err(OidcError::RequestErrorWithDetails {
66                                        status_code,
67                                        error: error_response.error,
68                                        error_description: error_response.error_description,
69                                    })
70                                }
71                            },
72                            Err(_) => {
73                                Err(OidcError::RequestErrorWithDetails {
74                                    status_code,
75                                    error: "request_error".to_string(),
76                                    error_description: body,
77                                })
78                            }
79                        }
80                    },
81                    Err(e) => {
82                        Err(OidcError::RequestErrorWithDetails {
83                            status_code,
84                            error: "request_error".to_string(),
85                            error_description: format!("Failed to read response body: {}", e),
86                        })
87                    }
88                }
89            }
90        },
91        Err(e) => {
92            Err(OidcError::RequestErrorWithDetails {
93                status_code: 0, // No status code available for connection errors
94                error: "request_error".to_string(),
95                error_description: e.to_string(),
96            })
97        }
98    }
99}
100
101/**
102 * Check if provided token is in JWT format
103 * @param token: &String
104 */
105pub fn is_jwt_token(token: &str) -> bool {
106    token.contains(".")
107}
108
109/**
110 * IDP token introspection
111 * for non-JWT tokens; uses introspection endpoint
112 */
113async fn token_introspection(
114    base_url: &str,
115    access_token: &AccessToken,
116    device_jwt: &String,
117    device_client_id: &String,
118) -> Result<ClientCredentialsIntrospection, OidcError> {
119    let client = default_client()?;
120    let params = [
121        ("token", access_token.secret()),
122        ("client_id", &device_client_id.to_string()),
123        (
124            "client_assertion_type",
125            &"urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string(),
126        ),
127        ("client_assertion", device_jwt),
128    ];
129    let response = client
130        .post(format!("{}/token/introspection", base_url))
131        .form(&params)
132        .header("content-type", "application/x-www-form-urlencoded")
133        .send()
134        .await?
135        .error_for_status()?
136        .json::<ClientCredentialsIntrospection>()
137        .await?;
138
139    Ok(response)
140}
141
142/**
143 * IDP subject identification
144 * for non-JWT tokens; uses identify endpoint
145 */
146async fn identify_subject(
147    base_url: &str,
148    access_token: &String,
149) -> Result<SubjectIdentity, OidcError> {
150    let client = default_client()?;
151    let response = client
152        .get(format!("{}/identify", base_url))
153        .header("Authorization", format!("Bearer {}", access_token))
154        .send()
155        .await?
156        .error_for_status()?
157        .json::<SubjectIdentity>()
158        .await?;
159
160    Ok(response)
161}
162
163/**
164 * Sub: Make a CIBA request to IDP
165 */
166async fn ciba_request(
167    base_url: &str,
168    device_client_id: &ClientId,
169    device_jwt: &str,
170    device_jwt_ciba: &str,
171) -> Result<CibaResponse, OidcError> {
172    let client = default_client()?;
173
174    let params = [
175        ("client_id", device_client_id.as_str()),
176        (
177            "client_assertion_type",
178            "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
179        ),
180        ("client_assertion", device_jwt),
181        ("request", device_jwt_ciba),
182    ];
183
184    let response = client
185        .post(format!("{}/backchannel", base_url))
186        .form(&params)
187        .header("content-type", "application/x-www-form-urlencoded")
188        .send()
189        .await;
190        
191    handle_oidc_response::<CibaResponse>(response).await
192}
193
194/**
195 * Make a CIBA request to IDP
196 */
197async fn make_ciba_request(
198    device_client_id: &ClientId,
199    provider_metadata: &CoreProviderMetadata,
200    private_key: &EncodingKey,
201    device_jwt: &str,
202    config: &CibaRequestConfig,
203) -> Result<CibaResponse, OidcError> {
204    let signed_request = make_device_jwt_ciba(
205        device_client_id,
206        provider_metadata,
207        &config.login_hint,
208        &config.scope,
209        &config.binding_message,
210        config.resource.clone(),
211        private_key,
212        config.qr_session_id.clone(),
213    )?;
214
215    let ciba_response = ciba_request(
216        provider_metadata.issuer().as_str(),
217        device_client_id,
218        device_jwt,
219        &signed_request,
220    )
221    .await?;
222
223    Ok(ciba_response)
224}
225
226/**
227 * Check CIBA status with IDP
228 */
229async fn check_ciba_status(
230    provider_metadata: &CoreProviderMetadata,
231    auth_request_id: &str,
232    device_jwt: &str,
233    device_client_id: &ClientId,
234) -> Result<CibaStatusResponse, OidcError> {
235    let client = default_client()?;
236    let token_endpoint = provider_metadata
237        .token_endpoint()
238        .ok_or(OidcRequirementsError::MissingTokenEndpoint)?;
239
240    let params = [
241        ("grant_type", "urn:openid:params:grant-type:ciba"),
242        ("auth_req_id", auth_request_id),
243        ("client_id", device_client_id.as_str()),
244        (
245            "client_assertion_type",
246            "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
247        ),
248        ("client_assertion", device_jwt),
249    ];
250
251    let response = client
252        .post(token_endpoint.as_str())
253        .form(&params)
254        .header("content-type", "application/x-www-form-urlencoded")
255        .send()
256        .await;
257        
258    handle_oidc_response::<CibaStatusResponse>(response).await
259}
260
261async fn make_qr_auth_session_idp(
262    base_url: &str,
263    device_access_token: &str,
264) -> Result<QrAuthSessionIdp, OidcError> {
265    let client = default_client()?;
266
267    let response = client
268        .post(format!("{}/qr/make", base_url))
269        .bearer_auth(device_access_token)
270        .send()
271        .await?
272        .error_for_status()?
273        .json::<QrAuthSessionIdp>()
274        .await?;
275
276    Ok(response)
277}
278
279
280async fn verify_qr_auth_session_idp(
281    base_url: &str,
282    session: QrStatusRequest,
283    device_access_token: &str,
284) -> Result<Option<QrAuthSessionIdp>, OidcError> {
285    let client = default_client()?;
286    
287    let response = client
288        .post(format!("{}/qr/status", base_url))
289        .bearer_auth(device_access_token)
290        .json(&session)
291        .send()
292        .await?
293        .error_for_status()?
294        .json::<Option<QrAuthSessionIdp>>()
295        .await?;
296    
297    Ok(response)
298}
299
300/**
301 * Validate a JWT token received from IDP
302 */
303pub fn validate_jwt(
304    jwt: &str,
305    issuer_jwks: &JwkSet,
306    _client_id: &ClientId,
307) -> Result<JwtPayload, OidcError> {
308    if issuer_jwks.keys.is_empty() {
309        return Err(OidcError::InvalidJwkSet);
310    }
311    
312    let decoding_key = jsonwebtoken::DecodingKey::from_jwk(&issuer_jwks.keys[0].clone())
313        .map_err(|e| OidcError::InvalidJwk(e.to_string()))?;
314    let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);
315    // debug!("Setting audience to Client ID: {}", client_id.to_string());
316    // TODO: How'to handle this in tests, with dynamic apps?
317    // validation.set_audience(&vec![client_id.to_string()]);
318    validation.validate_aud = false;
319    debug!("Validating JWT: {}", jwt);
320    let token_data = jsonwebtoken::decode::<JwtPayload>(jwt, &decoding_key, &validation)?;
321
322    Ok(token_data.claims)
323}
324
325#[derive(Clone)]
326pub struct OpenIdconnectClient {
327    pub client_id: ClientId,
328    pub issuer_url: IssuerUrl,
329    pub issuer_jwks: Option<JwkSet>,
330    pub provider_metadata: CoreProviderMetadata,
331    pub private_key: EncodingKey,
332    pub config: OnesOidcConfig,
333}
334
335impl OpenIdconnectClient {
336    pub fn new(
337        client_id: ClientId,
338        issuer_url: IssuerUrl,
339        provider_metadata: CoreProviderMetadata,
340        private_key: EncodingKey,
341    ) -> Self {
342        OpenIdconnectClient {
343            client_id,
344            issuer_url,
345            issuer_jwks: None,
346            provider_metadata,
347            private_key,
348            config: OnesOidcConfig::default(),
349        }
350    }
351
352    pub fn with_config(
353        client_id: ClientId,
354        issuer_url: IssuerUrl,
355        provider_metadata: CoreProviderMetadata,
356        private_key: EncodingKey,
357        config: OnesOidcConfig,
358    ) -> Self {
359        OpenIdconnectClient {
360            client_id,
361            issuer_url,
362            issuer_jwks: None,
363            provider_metadata,
364            private_key,
365            config,
366        }
367    }
368
369    pub fn client_id(&self) -> &ClientId {
370        &self.client_id
371    }
372
373    pub fn issuer_url(&self) -> &IssuerUrl {
374        &self.issuer_url
375    }
376
377    pub fn provider_metadata(&self) -> &CoreProviderMetadata {
378        &self.provider_metadata
379    }
380
381    pub fn private_key(&self) -> &EncodingKey {
382        &self.private_key
383    }
384
385    pub fn device_jwt(&self) -> Result<String, DeviceError> {
386        make_device_jwt(&self.client_id, &self.provider_metadata, &self.private_key)
387    }
388
389    pub async fn make_ciba_request(
390        &self,
391        login_hint: &LoginHint,
392        scope: &str,
393        binding_message: &str,
394        resource: Option<String>,
395        qr_session_id: Option<String>,
396    ) -> Result<CibaResponse, OidcError> {
397        let device_jwt = self.device_jwt()?;
398        let config = CibaRequestConfig {
399            login_hint: login_hint.clone(),
400            scope: scope.to_string(),
401            binding_message: binding_message.to_string(),
402            resource,
403            qr_session_id,
404        };
405        make_ciba_request(
406            &self.client_id,
407            &self.provider_metadata,
408            &self.private_key,
409            &device_jwt,
410            &config,
411        )
412        .await
413    }
414
415    pub async fn check_ciba_status(
416        &self,
417        auth_request_id: &str,
418    ) -> Result<CibaStatusResponse, OidcError> {
419        let device_jwt = self.device_jwt()?;
420        check_ciba_status(
421            &self.provider_metadata,
422            auth_request_id,
423            &device_jwt,
424            &self.client_id,
425        )
426        .await
427    }
428
429    pub async fn make_qr_auth_session_idp(
430        &self,
431    ) -> Result<QrAuthSessionIdp, OidcError> {
432        let device_access_token_res = &self.device_access_token().await?;
433        let device_access_token = device_access_token_res.access_token().secret();
434        make_qr_auth_session_idp(&self.issuer_url, device_access_token).await
435    }
436
437    // Ones authRequestId is filled, switch to polling CIBA
438    pub async fn verify_qr_auth_session_idp(
439        &self,
440        session: QrStatusRequest,
441        // CIBA properties
442        scope: &str,
443        binding_message: &str,
444        resource: Option<String>,
445    ) -> Result<Option<QrAuthSessionIdp>, OidcError> {
446        let device_access_token_res = &self.device_access_token().await?;
447        let device_access_token = device_access_token_res.access_token().secret();
448        let result =
449            verify_qr_auth_session_idp(&self.issuer_url, session, device_access_token).await?;
450        if result.is_none() {
451            return Ok(None);
452        }
453
454        let result = result.ok_or(OidcError::InvalidQrSession)?;
455        if let (Some(login_hint_token), None) = (&result.login_hint_token, &result.auth_request_id) {
456            let login_hint = LoginHint {
457                kind: LoginHintKind::LoginHintToken,
458                value: login_hint_token.clone(),
459            };
460            let _ = self
461                .make_ciba_request(
462                    &login_hint,
463                    scope,
464                    binding_message,
465                    resource,
466                    Some(result.session_id.clone()),
467                )
468                .await?;
469        }
470
471        Ok(Some(result))
472    }
473
474    pub async fn token_introspection(
475        &self,
476        access_token: &AccessToken,
477    ) -> Result<ClientCredentialsIntrospection, OidcError> {
478        let device_jwt = self.device_jwt()?;
479        token_introspection(
480            &self.issuer_url,
481            access_token,
482            &device_jwt,
483            &self.client_id,
484        )
485        .await
486    }
487
488    pub async fn get_jwks(&mut self) -> Result<JwkSet, reqwest::Error> {
489        if let Some(jwks) = &self.issuer_jwks {
490            return Ok(jwks.clone());
491        }
492
493        let jwks = get_jwks(&self.provider_metadata).await?;
494        self.issuer_jwks = Some(jwks.clone());
495        Ok(jwks)
496    }
497
498    pub async fn request_device_access_token(
499        &self,
500    ) -> Result<
501        StandardTokenResponse<EmptyExtraTokenFields, CoreTokenType>,
502        DeviceError,
503    > {
504        let device_jwt = self.device_jwt()?;
505        request_device_access_token(&self.provider_metadata, &self.client_id, &device_jwt)
506            .await
507    }
508
509    pub async fn device_access_token(
510        &self,
511    ) -> Result<StandardTokenResponse<EmptyExtraTokenFields, CoreTokenType>, DeviceError> {
512        let device_jwt = self.device_jwt()?;
513        device_access_token(&self.provider_metadata, &self.client_id, &device_jwt).await
514    }
515
516    pub fn make_device_jwt(&self) -> Result<String, DeviceError> {
517        make_device_jwt(&self.client_id, &self.provider_metadata, &self.private_key)
518    }
519
520    pub async fn validate_jwt(&mut self, jwt: &str) -> Result<JwtPayload, OidcError> {
521        self.get_jwks().await?;
522        validate_jwt(jwt, &self.get_jwks().await?, &self.client_id)
523    }
524
525    pub async fn validate_token(
526        &mut self,
527        token: &String,
528        permitted_idp_scopes: Option<Vec<String>>,
529    ) -> Result<AuthenticationResult, OidcError> {
530        if is_jwt_token(token) {
531            let claims = self.validate_jwt(token).await?;
532
533            let aud = if let Some(aud_claim) = &claims.aud {
534                Some(Uuid::from_str(aud_claim.as_str())?)
535            } else {
536                None
537            };
538
539            let scope = claims.scope.clone();
540            let resource = claims.resource.clone();
541
542            if let (Some(scope_val), Some(resource_val), Some(permitted_scopes)) = (&scope, &resource, &permitted_idp_scopes) {
543                if resource_val == "idp-server" && permitted_scopes.contains(scope_val) {
544                    return Ok(AuthenticationResult {
545                        entity: AuthenticatedEntityKind::Device,
546                        iss: claims.iss,
547                        sub: claims.sub,
548                        aud,
549                        scope: Some(scope_val.clone()),
550                        username: claims.username,
551                        client_id: claims.client_id,
552                        method: AuthenticationMethod::IdpJwt,
553                        idp_role: claims.idp_role,
554                    });
555                }
556            }
557
558            Ok(AuthenticationResult {
559                entity: AuthenticatedEntityKind::User,
560                iss: claims.iss,
561                sub: claims.sub,
562                aud,
563                scope,
564                username: claims.username,
565                client_id: claims.client_id,
566                method: AuthenticationMethod::UserJwt,
567                idp_role: claims.idp_role,
568            })
569        } else {
570            // This may be a device, mobile, or user token
571            let access_token = AccessToken::new(token.to_string());
572            let introspection = self.token_introspection(&access_token).await
573                .map_err(|e| OidcError::TokenIntrospectionFailed(e.to_string()))?;
574                
575            if !introspection.active {
576                return Err(OidcError::TokenNotActive);
577            }
578
579            let iss = introspection.iss.ok_or(OidcError::TokenIntrospectionFailed(
580                "Missing issuer in token".to_string()
581            ))?;
582
583            let identify = identify_subject(self.issuer_url.as_str(), token).await
584                .map_err(|e| OidcError::TokenIdentificationFailed(e.to_string()))?;
585            let mut method = AuthenticationMethod::UserDevice;
586
587            if introspection.sub.is_some() && identify.subject_type == AuthenticatedEntityKind::User
588            {
589                method = AuthenticationMethod::UserIdp;
590            } else if identify.subject_type == AuthenticatedEntityKind::User {
591                // Mobile introspection does not provide a subject
592                method = AuthenticationMethod::UserDevice;
593            } else if identify.subject_type == AuthenticatedEntityKind::Device {
594                method = AuthenticationMethod::Device;
595            }
596
597            Ok(AuthenticationResult {
598                entity: identify.subject_type,
599                iss,
600                sub: identify.subject,
601                aud: None,
602                scope: introspection.scope,
603                username: identify.username,
604                client_id: Some(identify.client_id),
605                method,
606                idp_role: introspection.idp_role,
607            })
608        }
609    }
610}