use super::device::{get_jwks, make_device_jwt, make_device_jwt_ciba, request_device_access_token};
use super::oidc_types::JwtPayload;
use super::oidc_types::{
AuthenticationMethod, AuthenticationResult, CibaResponse, CibaStatusResponse,
ClientCredentialsIntrospection, SubjectIdentity,
};
use super::{AuthenticatedEntityKind, LoginHint};
use crate::errors::{DeviceError, OidcError, OidcRequirementsError};
use crate::oidc_backend::QrStatusRequestFrontend;
use crate::oidc_types::{LoginHintKind, OidcErrorResponse, QrAuthSessionIdp};
use jsonwebtoken::jwk::JwkSet;
use jsonwebtoken::EncodingKey;
use log::debug;
use openidconnect::core::{CoreProviderMetadata, CoreTokenType};
use openidconnect::{
AccessToken, ClientId, EmptyExtraTokenFields, IssuerUrl, OAuth2TokenResponse, StandardTokenResponse
};
use reqwest::Client;
use std::str::FromStr;
use uuid::Uuid;
pub fn is_jwt_token(token: &String) -> bool {
token.contains(".")
}
async fn token_introspection(
base_url: &str,
access_token: &AccessToken,
device_jwt: &String,
device_client_id: &String,
) -> Result<ClientCredentialsIntrospection, OidcError> {
let client = Client::new();
let params = [
("token", access_token.secret()),
("client_id", &device_client_id.to_string()),
(
"client_assertion_type",
&"urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string(),
),
("client_assertion", device_jwt),
];
let response = client
.post(format!("{}/token/introspection", base_url))
.form(¶ms)
.header("content-type", "application/x-www-form-urlencoded")
.timeout(std::time::Duration::from_secs(3))
.send()
.await?
.error_for_status()?
.json::<ClientCredentialsIntrospection>()
.await?;
Ok(response)
}
async fn identify_subject(
base_url: &str,
access_token: &String,
) -> Result<SubjectIdentity, OidcError> {
let client = Client::new();
let response = client
.get(format!("{}/identify", base_url))
.header("Authorization", format!("Bearer {}", access_token))
.timeout(std::time::Duration::from_secs(3))
.send()
.await?
.error_for_status()?
.json::<SubjectIdentity>()
.await?;
Ok(response)
}
async fn ciba_request(
base_url: &str,
device_client_id: &ClientId,
device_jwt: &str,
device_jwt_ciba: &str,
) -> Result<CibaResponse, OidcError> {
let client = Client::new();
let params = [
("client_id", device_client_id.as_str()),
(
"client_assertion_type",
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
),
("client_assertion", device_jwt),
("request", device_jwt_ciba),
];
let response = client
.post(format!("{}/backchannel", base_url))
.form(¶ms)
.header("content-type", "application/x-www-form-urlencoded")
.timeout(std::time::Duration::from_secs(3))
.send()
.await?
.error_for_status()?
.json::<CibaResponse>()
.await?;
Ok(response)
}
async fn make_ciba_request(
device_client_id: &ClientId,
provider_metadata: &CoreProviderMetadata,
private_key: &EncodingKey,
login_hint: &LoginHint,
scope: &str,
binding_message: &str,
device_jwt: &str,
resource: Option<String>,
qr_session_id: Option<String>,
) -> Result<CibaResponse, OidcError> {
let signed_request = make_device_jwt_ciba(
device_client_id,
provider_metadata,
login_hint,
scope,
binding_message,
resource,
private_key,
qr_session_id,
);
let ciba_response = ciba_request(
provider_metadata.issuer().as_str(),
device_client_id,
&device_jwt,
&signed_request,
)
.await?;
Ok(ciba_response)
}
async fn check_ciba_status(
provider_metadata: &CoreProviderMetadata,
auth_request_id: &str,
device_jwt: &str,
device_client_id: &ClientId,
) -> Result<CibaStatusResponse, OidcError> {
let client = Client::new();
let token_endpoint = provider_metadata
.token_endpoint()
.ok_or(OidcRequirementsError::MissingTokenEndpoint)?;
let params = [
("grant_type", "urn:openid:params:grant-type:ciba"),
("auth_req_id", auth_request_id),
("client_id", device_client_id.as_str()),
(
"client_assertion_type",
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
),
("client_assertion", device_jwt),
];
let response = client
.post(token_endpoint.as_str())
.form(¶ms)
.header("content-type", "application/x-www-form-urlencoded")
.timeout(std::time::Duration::from_secs(3))
.send()
.await?;
if response.status().is_success() {
let status_response = response.json::<CibaStatusResponse>().await?;
Ok(status_response)
} else if response.status() == reqwest::StatusCode::BAD_REQUEST {
let response_json = response.json::<OidcErrorResponse>().await?;
if response_json.error == "authorization_pending" {
Err(OidcError::CibaAuthenticationPending)
} else {
Err(OidcError::CibaStatusBadRequest(response_json.error_description))
}
} else {
let error_text = response.text().await?;
Err(OidcError::CibaStatusCheckFailed(error_text))
}
}
async fn make_qr_auth_session_idp(
base_url: &str,
device_access_token: &str,
) -> Result<QrAuthSessionIdp, OidcError> {
let client = Client::new();
let response = client
.post(format!("{}/token/oidc/qr/make", base_url))
.bearer_auth(device_access_token)
.timeout(std::time::Duration::from_secs(3))
.send()
.await?
.error_for_status()?
.json::<QrAuthSessionIdp>()
.await?;
Ok(response)
}
async fn verify_qr_auth_session_idp(
base_url: &str,
session: QrStatusRequestFrontend,
device_access_token: &str,
) -> Result<Option<QrAuthSessionIdp>, OidcError> {
let client = Client::new();
let response = client
.post(format!("{}/token/oidc/qr/status", base_url))
.bearer_auth(device_access_token)
.json(&session)
.timeout(std::time::Duration::from_secs(3))
.send()
.await?
.error_for_status()?
.json::<Option<QrAuthSessionIdp>>()
.await?;
Ok(response)
}
pub fn validate_jwt(
jwt: &str,
issuer_jwks: &JwkSet,
_client_id: &ClientId,
) -> Result<JwtPayload, OidcError> {
let decoding_key = jsonwebtoken::DecodingKey::from_jwk(&issuer_jwks.keys[0].clone()).unwrap();
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);
validation.validate_aud = false;
debug!("Validating JWT: {}", jwt);
let token_data = jsonwebtoken::decode::<JwtPayload>(&jwt, &decoding_key, &validation)?;
Ok(token_data.claims)
}
#[derive(Clone)]
pub struct OpenIdconnectClient {
pub client_id: ClientId,
pub issuer_url: IssuerUrl,
pub issuer_jwks: Option<JwkSet>,
pub provider_metadata: CoreProviderMetadata,
pub private_key: EncodingKey,
}
impl OpenIdconnectClient {
pub fn new(
client_id: ClientId,
issuer_url: IssuerUrl,
provider_metadata: CoreProviderMetadata,
private_key: EncodingKey,
) -> Self {
OpenIdconnectClient {
client_id,
issuer_url,
issuer_jwks: None,
provider_metadata,
private_key,
}
}
pub fn client_id(&self) -> &ClientId {
&self.client_id
}
pub fn issuer_url(&self) -> &IssuerUrl {
&self.issuer_url
}
pub fn provider_metadata(&self) -> &CoreProviderMetadata {
&self.provider_metadata
}
pub fn private_key(&self) -> &EncodingKey {
&self.private_key
}
pub fn device_jwt(&self) -> String {
make_device_jwt(&self.client_id, &self.provider_metadata, &self.private_key)
}
pub async fn make_ciba_request(
&self,
login_hint: &LoginHint,
scope: &str,
binding_message: &str,
resource: Option<String>,
qr_session_id: Option<String>,
) -> Result<CibaResponse, OidcError> {
make_ciba_request(
&self.client_id,
&self.provider_metadata,
&self.private_key,
login_hint,
scope,
binding_message,
&self.device_jwt(),
resource,
qr_session_id,
)
.await
}
pub async fn check_ciba_status(
&self,
auth_request_id: &str,
) -> Result<CibaStatusResponse, OidcError> {
check_ciba_status(
&self.provider_metadata,
auth_request_id,
&self.device_jwt(),
&self.client_id,
)
.await
}
pub async fn make_qr_auth_session_idp(
&self,
) -> Result<QrAuthSessionIdp, OidcError> {
let device_access_token_res = &self.device_access_token().await?;
let device_access_token = device_access_token_res.access_token().secret();
make_qr_auth_session_idp(&self.issuer_url, device_access_token).await
}
pub async fn verify_qr_auth_session_idp(
&self,
session: QrStatusRequestFrontend,
device_access_token: &str,
scope: &str,
binding_message: &str,
resource: Option<String>,
) -> Result<Option<QrAuthSessionIdp>, OidcError> {
let result =
verify_qr_auth_session_idp(&self.issuer_url, session, device_access_token).await?;
if result.is_none() {
return Ok(None);
}
let result = result.unwrap();
if result.login_hint_token.is_some() && result.auth_request_id.is_none() {
let login_hint = LoginHint {
kind: LoginHintKind::LoginHintToken,
value: result.login_hint_token.clone().unwrap(),
};
let _ = self
.make_ciba_request(
&login_hint,
scope,
binding_message,
resource,
Some(result.session_id.clone()),
)
.await?;
}
Ok(Some(result))
}
pub async fn token_introspection(
&self,
access_token: &AccessToken,
) -> Result<ClientCredentialsIntrospection, OidcError> {
token_introspection(
&self.issuer_url,
access_token,
&self.device_jwt(),
&self.client_id,
)
.await
}
pub async fn get_jwks(&mut self) -> Result<JwkSet, reqwest::Error> {
if self.issuer_jwks.is_some() {
return Ok(self.issuer_jwks.clone().unwrap());
}
let jwks = get_jwks(&self.provider_metadata).await?;
self.issuer_jwks = Some(jwks.clone());
Ok(jwks)
}
pub async fn request_device_access_token(
&self,
) -> Result<
StandardTokenResponse<EmptyExtraTokenFields, CoreTokenType>,
DeviceError,
> {
request_device_access_token(&self.provider_metadata, &self.client_id, &self.device_jwt())
.await
}
pub async fn device_access_token(
&self,
) -> Result<StandardTokenResponse<EmptyExtraTokenFields, CoreTokenType>, DeviceError> {
self.request_device_access_token().await
}
pub fn make_device_jwt(&self) -> String {
make_device_jwt(&self.client_id, &self.provider_metadata, &self.private_key)
}
pub async fn validate_jwt(&mut self, jwt: &String) -> Result<JwtPayload, OidcError> {
self.get_jwks().await?;
validate_jwt(jwt, &self.get_jwks().await?, &self.client_id)
}
pub async fn validate_token(
&mut self,
token: &String,
) -> Result<AuthenticationResult, OidcError> {
if is_jwt_token(token) {
let claims = self.validate_jwt(token).await?;
let aud = if claims.aud.is_some() {
Some(Uuid::from_str(claims.aud.unwrap().as_str()).unwrap())
} else {
None
};
let scope = if claims.scope.is_some() {
Some(claims.scope.unwrap())
} else {
None
};
let resource = if claims.resource.is_some() {
Some(claims.resource.unwrap())
} else {
None
};
if scope.is_some() && resource.is_some() {
let resource = resource.unwrap();
let scope = scope.clone().unwrap();
if resource == "idp-server" && scope == "idp-documents" {
return Ok(AuthenticationResult {
entity: AuthenticatedEntityKind::Device,
iss: claims.iss,
sub: claims.sub,
aud,
scope: Some(scope),
username: claims.username,
client_id: claims.client_id,
method: AuthenticationMethod::IdpJwt,
idp_role: claims.idp_role,
});
}
}
Ok(AuthenticationResult {
entity: AuthenticatedEntityKind::User,
iss: claims.iss,
sub: claims.sub,
aud,
scope,
username: claims.username,
client_id: claims.client_id,
method: AuthenticationMethod::UserJwt,
idp_role: claims.idp_role,
})
} else {
let access_token = AccessToken::new(token.to_string());
let introspection = self.token_introspection(&access_token).await;
if introspection.is_err() {
return Err(OidcError::TokenIntrospectionFailed(introspection.err().unwrap().to_string()));
}
let introspection = introspection.unwrap();
if introspection.active == false {
return Err(OidcError::TokenNotActive);
}
let identify = identify_subject(self.issuer_url.as_str(), token).await;
if identify.is_err() {
return Err(OidcError::TokenIdentificationFailed(identify.err().unwrap().to_string()));
}
let identify = identify.unwrap();
let mut method = AuthenticationMethod::UserDevice;
if introspection.sub.is_some() && identify.subject_type == AuthenticatedEntityKind::User
{
method = AuthenticationMethod::UserIdp;
} else if identify.subject_type == AuthenticatedEntityKind::User {
method = AuthenticationMethod::UserDevice;
} else if identify.subject_type == AuthenticatedEntityKind::Device {
method = AuthenticationMethod::Device;
}
Ok(AuthenticationResult {
entity: identify.subject_type,
iss: introspection.iss,
sub: identify.subject,
aud: None,
scope: introspection.scope,
username: identify.username,
client_id: Some(identify.client_id),
method,
idp_role: introspection.idp_role,
})
}
}
}