steam_auth/
verifier.rs

1use crate::{Error, STEAM_URL};
2
3#[cfg(feature = "reqwest-09x")]
4use futures::{
5    future::{self, Either},
6    Future, Stream,
7};
8
9#[derive(Debug, Clone)]
10/// Verifies the login details returned after users have gone through the 'sign in with Steam' page
11/// # Example
12/// ```
13/// # use steam_auth::Verifier;
14/// # struct Response; impl Response { fn new() -> Self { Self } fn body(&self) -> &'static
15/// # str { "foo" } }
16/// # fn main() {
17/// # let qs = "openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.mode=id_res&openid.op_endpoint=https%3A%2F%2Fsteamcommunity.com%2Fopenid%2Flogin&openid.claimed_id=https%3A%2F%2Fsteamcommunity.com%2Fopenid%2Fid%2F92345666790633291&openid.identity=https%3A%2F%2Fsteamcommunity.com%2Fopenid%2Fid%2F12333456789000000&openid.return_to=http%3A%2F%2Flocalhost%3A8080%2Fcallback&openid.response_nonce=2019-06-15T00%3A36%3A00Z7nVIS5lDAcZe%2FT0gT4%2BQNQyexyA%3D&openid.assoc_handle=1234567890&openid.signed=signed%2Cop_endpoint%2Cclaimed_id%2Cidentity%2Creturn_to%2Cresponse_nonce%2Cassoc_handle&openid.sig=BK0zC%2F%2FKzERs7N%2BNlDO0aL06%2BBA%3D";
18/// let (req, verifier) = Verifier::from_querystring(qs).unwrap();
19/// // send off req, get back response
20/// # let response = Response;
21/// match verifier.verify_response(response.body()) {
22///     Ok(steam_id) => (), // got steam id
23///     Err(e) => (), // Auth failure
24/// }
25/// # }
26/// ```
27pub struct Verifier {
28    claimed_id: u64,
29}
30
31impl Verifier {
32    /// Constructs a Verifier and a HTTP request from a query string. You must use the method,
33    /// headers, URI and body from the returned `http::Request` struct.
34    pub fn from_querystring<S: AsRef<str>>(s: S) -> Result<(http::Request<Vec<u8>>, Self), Error> {
35        let form = serde_urlencoded::from_str(s.as_ref()).map_err(Error::Deserialize)?;
36
37        Self::from_parsed(form)
38    }
39
40    /// Constructs a Verifier and a HTTP request directly from the data deserialized from the query
41    /// string. This may be useful if you are using a web framework which offers the ability to
42    /// deserialize data during route matching. You must use the method, headers, URI and body from
43    /// the returned `http::Request` struct.
44    pub fn from_parsed(
45        mut login_data: SteamLoginData,
46    ) -> Result<(http::Request<Vec<u8>>, Self), Error> {
47        login_data.mode = "check_authentication".to_owned();
48
49        let verifier = {
50            let url = url::Url::parse(&login_data.claimed_id).map_err(|_| Error::ParseSteamId)?;
51            let mut segments = url.path_segments().ok_or(Error::ParseSteamId)?;
52            let id_segment = segments.next_back().ok_or(Error::ParseSteamId)?;
53
54            let claimed_id = id_segment.parse::<u64>().map_err(|_| Error::ParseSteamId)?;
55
56            Self { claimed_id }
57        };
58
59        let form_data = serde_urlencoded::to_string(login_data)
60            .map_err(Error::Serialize)?
61            .into_bytes();
62
63        let req = http::Request::builder()
64            .method(http::Method::POST)
65            .uri(STEAM_URL)
66            .header("Content-Type", "application/x-www-form-urlencoded")
67            .body(form_data)
68            .map_err(Error::BuildHttpStruct)?;
69
70        Ok((req, verifier))
71    }
72
73    /// Verifies the response from the steam servers.
74    pub fn verify_response<S: Into<String>>(self, response_body: S) -> Result<u64, Error> {
75        let is_valid = response_body
76            .into()
77            .split('\n')
78            .filter_map(|line| {
79                // Allow values to contain colons, but not keys
80                let mut pair = line.splitn(2, ':');
81                Some((pair.next()?, pair.next()?))
82            })
83            .any(|(k, v)| k == "is_valid" && v == "true");
84
85        if is_valid {
86            Ok(self.claimed_id)
87        } else {
88            Err(Error::AuthenticationFailed)
89        }
90    }
91
92    #[cfg(feature = "reqwest-09x")]
93    /// Constructs and sends a synchronous verification request. Requires the `reqwest-09x`
94    /// feature.
95    pub fn make_verify_request<S: AsRef<str>>(
96        client: &reqwest::Client,
97        querystring: S,
98    ) -> Result<u64, Error> {
99        let (req, verifier) = Self::from_querystring(querystring)?;
100
101        let (parts, body) = req.into_parts();
102
103        client
104            .post(&parts.uri.to_string())
105            .header("Content-Type", "application/x-www-form-urlencoded")
106            .body(body)
107            .send()
108            .map_err(Error::Reqwest)
109            .and_then(|mut response| {
110                let text = response.text().map_err(Error::Reqwest)?;
111
112                verifier.verify_response(text)
113            })
114    }
115
116    #[cfg(feature = "reqwest-09x")]
117    /// Constructs and sends an asynchronous verification request. Requires the `reqwest-09x`
118    /// feature.
119    pub fn make_verify_request_async<S: AsRef<str>>(
120        client: &reqwest::r#async::Client,
121        querystring: S,
122    ) -> impl Future<Item = u64, Error = Error> {
123        let (req, verifier) = match Self::from_querystring(querystring) {
124            Ok(rv) => rv,
125            Err(e) => return Either::A(future::err(e)),
126        };
127
128        let (parts, body) = req.into_parts();
129
130        Either::B(
131            client
132                .post(&parts.uri.to_string())
133                .header("Content-Type", "application/x-www-form-urlencoded")
134                .body(body)
135                .send()
136                .map_err(Error::Reqwest)
137                .and_then(|res| res.into_body().concat2().map_err(Error::Reqwest))
138                .and_then(move |body| {
139                    let s = std::str::from_utf8(&body)
140                        .map_err(|_| Error::AuthenticationFailed)?
141                        .to_owned();
142
143                    verifier.verify_response(s)
144                }),
145        )
146    }
147}
148
149#[derive(Clone, Deserialize, Serialize, Debug)]
150pub struct SteamLoginData {
151    #[serde(rename = "openid.ns")]
152    ns: String,
153    #[serde(rename = "openid.mode")]
154    mode: String,
155    #[serde(rename = "openid.op_endpoint")]
156    op_endpoint: String,
157    #[serde(rename = "openid.claimed_id")]
158    claimed_id: String,
159    #[serde(rename = "openid.identity")]
160    identity: Option<String>,
161    #[serde(rename = "openid.return_to")]
162    return_to: String,
163    #[serde(rename = "openid.response_nonce")]
164    response_nonce: String,
165    #[serde(rename = "openid.invalidate_handle")]
166    invalidate_handle: Option<String>,
167    #[serde(rename = "openid.assoc_handle")]
168    assoc_handle: String,
169    #[serde(rename = "openid.signed")]
170    signed: String,
171    #[serde(rename = "openid.sig")]
172    sig: String,
173}