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#[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
34async 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 match res.text().await {
58 Ok(body) => {
59 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, error: "request_error".to_string(),
95 error_description: e.to_string(),
96 })
97 }
98 }
99}
100
101pub fn is_jwt_token(token: &str) -> bool {
106 token.contains(".")
107}
108
109async 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(¶ms)
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
142async 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
163async 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(¶ms)
187 .header("content-type", "application/x-www-form-urlencoded")
188 .send()
189 .await;
190
191 handle_oidc_response::<CibaResponse>(response).await
192}
193
194async 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
226async 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(¶ms)
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
300pub 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 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 pub async fn verify_qr_auth_session_idp(
439 &self,
440 session: QrStatusRequest,
441 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 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 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}