civic_sip/
lib.rs

1extern crate chrono;
2extern crate frank_jwt as jwt;
3extern crate openssl;
4extern crate reqwest;
5extern crate uuid;
6
7pub mod crypto;
8pub mod error;
9
10use chrono::Utc;
11use error::CivicError;
12use hmac::{Hmac, Mac};
13use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE};
14use reqwest::StatusCode;
15use serde::Deserialize;
16use serde_json::{json, Value as JsonValue};
17use sha2::Sha256;
18use uuid::Uuid;
19
20// Create alias for HMAC-SHA256
21type HmacSha256 = Hmac<Sha256>;
22
23const CIVIC_SIP_API_URL: &str = "https://api.civic.com/sip";
24const CIVIC_SIP_API_PUB: &str = "049a45998638cfb3c4b211d72030d9ae8329a242db63bfb0076a54e7647370a8ac5708b57af6065805d5a6be72332620932dbb35e8d318fce18e7c980a0eb26aa1";
25const CIVIC_JWT_EXPIRATION: i64 = 180; // 3 min
26
27#[derive(Deserialize)]
28struct Payload {
29    data: String,
30}
31
32/// CIVIC Application configuration
33pub struct CivicSipConfig {
34    pub app_id: &'static str,
35    pub app_secret: &'static str,
36    pub private_key: &'static str,
37    pub proxy: Option<&'static str>,
38}
39
40pub enum CivicEnv {
41    Prod,
42    Dev,
43}
44
45pub struct CivicSip {
46    config: CivicSipConfig,
47    env: &'static str,
48}
49
50impl CivicSip {
51    pub fn new(config: CivicSipConfig, civic_env: Option<CivicEnv>) -> CivicSip {
52        let env = match civic_env {
53            None => "prod",
54            Some(civic_env_enum) => match civic_env_enum {
55                CivicEnv::Prod => "prod",
56                CivicEnv::Dev => "dev",
57            },
58        };
59        CivicSip { config, env }
60    }
61
62    /// Exchange the authorization code wrapped in a JWT token for the requested user data
63    ///
64    /// # Arguments
65    /// * `jwt_token` - A string containing the authorization code (AC)
66    ///
67    pub fn exchange_code(&self, jwt_token: &str) -> Result<JsonValue, CivicError> {
68        let body: String = json!({
69            "authToken": jwt_token,
70            "processPayload": true,
71        })
72        .to_string();
73        let auth_header: String = self.make_authorization_header(&body);
74
75        let client = match self.config.proxy {
76            Some(proxy) => reqwest::Client::builder()
77                .proxy(reqwest::Proxy::all(proxy)?)
78                .build()?,
79            _ => reqwest::Client::new(),
80        };
81
82        let mut response = client
83            .post(format!("{}/{}/scopeRequest/authCode", CIVIC_SIP_API_URL, &self.env).as_str())
84            .header(AUTHORIZATION, auth_header)
85            .header(CONTENT_LENGTH, body.len())
86            .header(CONTENT_TYPE, "application/json")
87            .header(ACCEPT, "Accept")
88            .body(body)
89            .send()?;
90
91        match response.status() {
92            StatusCode::OK => self.process_payload(response.json()?),
93
94            StatusCode::BAD_REQUEST => Err(CivicError {
95                code: 100_400,
96                message: String::from("Exchange code failed!"),
97            }),
98            StatusCode::UNAUTHORIZED => Err(CivicError {
99                code: 100_401,
100                message: String::from("Exchange code failed!"),
101            }),
102            StatusCode::FORBIDDEN => Err(CivicError {
103                code: 100_403,
104                message: String::from("Exchange code failed!"),
105            }),
106            StatusCode::NOT_FOUND => Err(CivicError {
107                code: 100_404,
108                message: String::from("Exchange code failed!"),
109            }),
110            StatusCode::INTERNAL_SERVER_ERROR => Err(CivicError {
111                code: 100_500,
112                message: String::from("Exchange code failed!"),
113            }),
114
115            status => Err(CivicError {
116                code: 1,
117                message: format!("Backend status code not supported: {:?}", status),
118            }),
119        }
120    }
121
122    /// Create the value of Authorization header for the call of CIVIC API
123    /// The token format: Civic requestToken.extToken
124    /// where requestToken certifies the service path, method
125    /// and audience, and extToken certifies the request body.
126    ///
127    /// The token is signed by the application private_key and secret.
128    ///
129    fn make_authorization_header(&self, body: &str) -> String {
130        let payload = json!({
131            "jti": Uuid::new_v4(),
132            "iat": Utc::now().timestamp_millis() / 1000,
133            "exp": (Utc::now().timestamp_millis() + CIVIC_JWT_EXPIRATION) / 1000,
134            "iss": self.config.app_id,
135            "aud": CIVIC_SIP_API_URL,
136            "sub": self.config.app_id,
137            "data": {
138                "method": "POST",
139                "path": "scopeRequest/authCode",
140            },
141        });
142        let header = json!({
143            "alg": "ES256",
144            "typ": "JWT"
145        });
146
147        let private_key: Vec<u8> = crypto::get_private_key_pem(&self.config.private_key);
148        let jwt_token =
149            match frank_jwt::encode(header, &private_key, &payload, frank_jwt::Algorithm::ES256) {
150                Ok(token) => token,
151                Err(error) => {
152                    panic!("There was a problem during JWT ENCODE: {:?}", error);
153                }
154            };
155
156        // Create CIVIC extension in base64 using hmac with the application secret on the JWT AC
157        let mut mac = HmacSha256::new_varkey(self.config.app_secret.as_bytes()).unwrap();
158        mac.input(body.as_bytes());
159
160        return format!(
161            "Civic {}.{}",
162            jwt_token,
163            base64::encode(&mac.result().code().to_owned())
164        );
165    }
166
167    /// Process CIVIC response
168    /// decrypt data using app secret and return result as JsonValue
169    fn process_payload(&self, payload: Payload) -> Result<JsonValue, CivicError> {
170        match crypto::decode(&payload.data, CIVIC_SIP_API_PUB) {
171            Err(error) => Err(error),
172            Ok((_, jwt_payload)) => crypto::decrypt(
173                jwt_payload["data"].as_str().unwrap(),
174                &self.config.app_secret,
175            ),
176        }
177    }
178}