sqrl_protocol/
server_response.rs

1//! Code for a server to respond to client requests
2
3use super::{
4    encode_newline_data, get_or_error, parse_newline_data, ProtocolVersion, PROTOCOL_VERSIONS,
5};
6use crate::{error::SqrlError, Result};
7use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
8use std::{collections::HashMap, fmt, result, str::FromStr};
9
10// The keys used to encode a server response
11const PROTOCOL_VERSION_KEY: &str = "ver";
12const NUT_KEY: &str = "nut";
13const TIF_KEY: &str = "tif";
14const QUERY_URL_KEY: &str = "qry";
15const SUCCESS_URL_KEY: &str = "url";
16const CANCEL_URL_KEY: &str = "can";
17const SECRET_INDEX_KEY: &str = "sin";
18const SERVER_UNLOCK_KEY_KEY: &str = "suk";
19const ASK_KEY: &str = "ask";
20
21/// An object representing a response from the server
22#[derive(Debug, PartialEq)]
23pub struct ServerResponse {
24    /// The SQRL protocol versions supported by the server (ver)
25    pub protocol_version: ProtocolVersion,
26    /// The nut to be used for signing the next request (nut)
27    pub nut: String,
28    /// A collection of transaction indication flags (tif)
29    pub transaction_indication_flags: Vec<TIFValue>,
30    /// The server object to query in the next request (qry)
31    pub query_url: String,
32    /// If CPS set, the url to redirect the client's browser to after
33    /// successful authentication (url)
34    pub success_url: Option<String>,
35    /// If CPS set, a url to use to cancel a user's authentication (can)
36    pub cancel_url: Option<String>,
37    /// The secret index used for requesting a client to return an indexed
38    /// secret (sin)
39    pub secret_index: Option<String>,
40    /// The server unlock key requested by the client (suk)
41    pub server_unlock_key: Option<String>,
42    /// A way for the server to request that the client display a prompt to the
43    /// client user and return the selection (ask)
44    pub ask: Option<String>,
45}
46
47impl ServerResponse {
48    /// Create a new server response object from the nut and tif values
49    pub fn new(
50        nut: String,
51        transaction_indication_flags: Vec<TIFValue>,
52        query_url: String,
53    ) -> ServerResponse {
54        ServerResponse {
55            protocol_version: ProtocolVersion::new(PROTOCOL_VERSIONS).unwrap(),
56            nut,
57            transaction_indication_flags,
58            query_url,
59            success_url: None,
60            cancel_url: None,
61            secret_index: None,
62            server_unlock_key: None,
63            ask: None,
64        }
65    }
66
67    /// Decode a server response from a base64-encoded value
68    pub fn from_base64(base64_string: &str) -> Result<Self> {
69        // Decode the response
70        let server_data = String::from_utf8(BASE64_URL_SAFE_NO_PAD.decode(base64_string)?)?;
71        Self::from_str(&server_data)
72    }
73
74    /// Return the base64-encoded value of the server response
75    pub fn to_base64(&self) -> String {
76        BASE64_URL_SAFE_NO_PAD.encode(self.to_string().as_bytes())
77    }
78}
79
80impl fmt::Display for ServerResponse {
81    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
82        let mut map = HashMap::<&str, &str>::new();
83        let protocol = self.protocol_version.to_string();
84        map.insert(PROTOCOL_VERSION_KEY, &protocol);
85        map.insert(NUT_KEY, &self.nut);
86
87        let mut tif: u16 = 0;
88        for t in &self.transaction_indication_flags {
89            tif |= *t as u16;
90        }
91
92        let tif_string = tif.to_string();
93        map.insert(TIF_KEY, &tif_string);
94        map.insert(QUERY_URL_KEY, &self.query_url);
95
96        if let Some(url) = &self.success_url {
97            map.insert(SUCCESS_URL_KEY, url);
98        }
99        if let Some(can) = &self.cancel_url {
100            map.insert(CANCEL_URL_KEY, can);
101        }
102        if let Some(sin) = &self.secret_index {
103            map.insert(SECRET_INDEX_KEY, sin);
104        }
105        if let Some(suk) = &self.server_unlock_key {
106            map.insert(SERVER_UNLOCK_KEY_KEY, suk);
107        }
108        if let Some(ask) = &self.ask {
109            map.insert(ASK_KEY, ask);
110        }
111
112        write!(f, "{}", &encode_newline_data(&map))
113    }
114}
115
116impl FromStr for ServerResponse {
117    type Err = SqrlError;
118
119    fn from_str(s: &str) -> result::Result<Self, Self::Err> {
120        let data = parse_newline_data(s)?;
121
122        // Validate the protocol version is supported
123        let ver_string = get_or_error(
124            &data,
125            PROTOCOL_VERSION_KEY,
126            "No version number in server response",
127        )?;
128        let protocol_version = ProtocolVersion::new(&ver_string)?;
129        let nut = get_or_error(&data, NUT_KEY, "No nut in server response")?;
130        let tif_string = get_or_error(&data, TIF_KEY, "No status code (tif) in server response")?;
131        let transaction_indication_flags = TIFValue::parse_str(&tif_string)?;
132
133        let query_url = get_or_error(
134            &data,
135            QUERY_URL_KEY,
136            "No query url (qry) in server response",
137        )?;
138
139        // The rest of these are optional
140        let success_url = data.get(SUCCESS_URL_KEY).map(|x| x.to_string());
141        let cancel_url = data.get(CANCEL_URL_KEY).map(|x| x.to_string());
142        let secret_index = data.get(SECRET_INDEX_KEY).map(|x| x.to_string());
143        let server_unlock_key = data.get(SERVER_UNLOCK_KEY_KEY).map(|x| x.to_string());
144        let ask = data.get(ASK_KEY).map(|x| x.to_string());
145
146        Ok(ServerResponse {
147            protocol_version,
148            nut,
149            transaction_indication_flags,
150            query_url,
151            success_url,
152            cancel_url,
153            secret_index,
154            server_unlock_key,
155            ask,
156        })
157    }
158}
159
160/// Transaction information flags
161#[derive(Clone, Copy, Debug, PartialEq)]
162pub enum TIFValue {
163    /// A response indicating the current identity (idk) matches the known
164    /// server identity
165    CurrentIdMatch = 0x1,
166    /// A response indicating the previous identity (pidk) matches the known
167    /// server identity
168    PreviousIdMatch = 0x2,
169    /// A response indicating the client ip address matches the first ip
170    /// address to query the server
171    IpsMatch = 0x4,
172    /// Response that indicates SQRL is disabled for this user
173    SqrlDisabled = 0x8,
174    /// Response that indicates the server does not support the previous request
175    FunctionNotSupported = 0x10,
176    /// Response that indicates the server experienced a transient error
177    /// and the request should be retried
178    TransientError = 0x20,
179    /// Response that indicates the client command failed
180    CommandFailed = 0x40,
181    /// Response that indicates that the client query was incorrect
182    ClientFailure = 0x80,
183    /// Response that indicates that the identities used in the client query do not
184    /// match the server's
185    BadId = 0x100,
186    /// Response that indicates the client identity used has been superseded
187    IdentitySuperseded = 0x200,
188}
189
190impl TIFValue {
191    /// Parse the TIF values based on a string
192    pub fn parse_str(value: &str) -> Result<Vec<Self>> {
193        match value.parse::<u16>() {
194            Ok(x) => Ok(Self::from_u16(x)),
195            Err(_) => Err(SqrlError::new(format!(
196                "Unable to parse server response status code (tif): {}",
197                value
198            ))),
199        }
200    }
201
202    /// Parse the TIF values based on a u16
203    pub fn from_u16(value: u16) -> Vec<Self> {
204        let mut ret = Vec::new();
205
206        if value & TIFValue::CurrentIdMatch as u16 > 0 {
207            ret.push(TIFValue::CurrentIdMatch);
208        }
209        if value & TIFValue::PreviousIdMatch as u16 > 0 {
210            ret.push(TIFValue::PreviousIdMatch);
211        }
212        if value & TIFValue::IpsMatch as u16 > 0 {
213            ret.push(TIFValue::IpsMatch);
214        }
215        if value & TIFValue::SqrlDisabled as u16 > 0 {
216            ret.push(TIFValue::SqrlDisabled);
217        }
218        if value & TIFValue::FunctionNotSupported as u16 > 0 {
219            ret.push(TIFValue::FunctionNotSupported);
220        }
221        if value & TIFValue::TransientError as u16 > 0 {
222            ret.push(TIFValue::TransientError);
223        }
224        if value & TIFValue::CommandFailed as u16 > 0 {
225            ret.push(TIFValue::CommandFailed);
226        }
227        if value & TIFValue::ClientFailure as u16 > 0 {
228            ret.push(TIFValue::ClientFailure);
229        }
230        if value & TIFValue::BadId as u16 > 0 {
231            ret.push(TIFValue::BadId);
232        }
233        if value & TIFValue::IdentitySuperseded as u16 > 0 {
234            ret.push(TIFValue::IdentitySuperseded);
235        }
236
237        ret
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use rand::{distr::Alphanumeric, rng, Rng};
245
246    const TEST_SERVER_RESPONSE: &str = "dmVyPTENCm51dD0xV005bGZGMVNULXoNCnRpZj01DQpxcnk9L2NsaS5zcXJsP251dD0xV005bGZGMVNULXoNCnN1az1CTUZEbTdiUGxzUW9qdUpzb0RUdmxTMU1jbndnU2N2a3RGODR2TGpzY0drDQo";
247
248    #[test]
249    fn server_response_validate_example() {
250        let response = ServerResponse::from_base64(TEST_SERVER_RESPONSE).unwrap();
251        assert_eq!(response.protocol_version.to_string(), "1");
252        assert_eq!(response.nut, "1WM9lfF1ST-z");
253        assert_eq!(response.query_url, "/cli.sqrl?nut=1WM9lfF1ST-z");
254        assert_eq!(
255            response.server_unlock_key.unwrap(),
256            "BMFDm7bPlsQojuJsoDTvlS1McnwgScvktF84vLjscGk"
257        )
258    }
259
260    #[test]
261    fn server_response_encode_decode() {
262        let nut: String = rng()
263            .sample_iter(&Alphanumeric)
264            .take(30)
265            .map(char::from)
266            .collect();
267        let qry: String = rng()
268            .sample_iter(&Alphanumeric)
269            .take(30)
270            .map(char::from)
271            .collect();
272        let tif: u16 = rng().random_range(0..1023);
273
274        let initial_response = ServerResponse::new(nut, TIFValue::from_u16(tif), qry);
275        let decoded_response = ServerResponse::from_base64(&initial_response.to_base64()).unwrap();
276
277        assert_eq!(initial_response, decoded_response);
278    }
279
280    #[test]
281    fn tif_value_from_string() {
282        let resp = TIFValue::parse_str("674").unwrap();
283        assert_eq!(4, resp.len());
284        assert!(resp.contains(&TIFValue::PreviousIdMatch));
285        assert!(resp.contains(&TIFValue::TransientError));
286        assert!(resp.contains(&TIFValue::ClientFailure));
287        assert!(resp.contains(&TIFValue::IdentitySuperseded));
288    }
289
290    #[test]
291    fn tif_value_from_u16() {
292        let resp = TIFValue::from_u16(73);
293        assert_eq!(3, resp.len());
294        assert!(resp.contains(&TIFValue::CurrentIdMatch));
295        assert!(resp.contains(&TIFValue::SqrlDisabled));
296        assert!(resp.contains(&TIFValue::CommandFailed));
297    }
298}