├── .gitignore ├── README.tpl ├── .travis.yml ├── Cargo.toml ├── LICENSE.md ├── README.md ├── src ├── redirector.rs ├── lib.rs └── verifier.rs └── examples └── server.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | [![Docs.rs](https://docs.rs/steam-auth/badge.svg)](https://docs.rs/steam-auth) 2 | {{badges}} 3 | 4 | # {{crate}} 5 | 6 | {{readme}} 7 | 8 | MIT Licensed. Pull requests and contributions welcome. 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | 7 | matrix: 8 | allow_failures: 9 | - rust: nightly 10 | fast_finish: true 11 | 12 | before_script: 13 | - rustup component add clippy rustfmt 14 | 15 | script: 16 | - cargo build --features reqwest-09x 17 | - cargo test --features reqwest-09x 18 | - cargo clippy 19 | - cargo fmt -- --check 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "steam-auth" 3 | authors = ["Matt Taylor"] 4 | version = "1.0.0" 5 | edition = "2018" 6 | readme = "README.md" 7 | license = "MIT" 8 | description = "Allows you to implement a 'login with steam' feature on your website." 9 | repository = "https://github.com/64/steam-auth" 10 | documentation = "https://docs.rs/steam-auth" 11 | keywords = ["steam", "login", "authentication", "authenticate", "openid"] 12 | categories = ["authentication", "asynchronous", "network-programming", "web-programming"] 13 | 14 | [features] 15 | reqwest-09x = ["reqwest", "futures"] 16 | 17 | [dependencies] 18 | reqwest = { version = "0.9", optional = true } 19 | futures = { version = "0.1", optional = true } 20 | url = "1.7.2" 21 | serde = "1.0.92" 22 | serde_derive = "1.0.92" 23 | serde_urlencoded = "0.5.5" 24 | failure = "0.1.5" 25 | http = "0.1.17" 26 | 27 | [dev-dependencies] 28 | simple-server = "0.4.0" 29 | 30 | [badges] 31 | travis-ci = { repository = "64/steam-auth" } 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2019 Matt Taylor 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Docs.rs](https://docs.rs/steam-auth/badge.svg)](https://docs.rs/steam-auth) 2 | [![Build Status](https://travis-ci.org/64/steam-auth.svg?branch=master)](https://travis-ci.org/64/steam-auth) 3 | 4 | # steam-auth 5 | 6 | Allows you to implement a 'login with steam' feature on your website. 7 | 8 | ### Usage 9 | 10 | The easiest way to use this crate is with the `reqwest-09x` feature which allows the library to 11 | make HTTP requests on your behalf. Otherwise, you will need to do that manually. 12 | 13 | Using the `reqwest-09x` feature: 14 | ```rust 15 | // First, create a redirector 16 | let redirector = Redirector::new("http://localhost:8080", "/callback").unwrap(); 17 | 18 | // When a user wants to log in with steam, (e.g when they land on the `/login` route), 19 | // redirect them to this URL: 20 | let redirect_url = redirector.url(); 21 | 22 | // Once they've finished authenticating, they will be returned to `/callback` with some data in 23 | // the query string that needs to be parsed and then verified by sending an HTTP request to the steam 24 | // servers. 25 | match Verifier::make_verify_request(&reqwest::Client::new(), querystring) { 26 | Ok(steam_id) => println!("Successfully logged in user with steam ID 64 {}", steam_id), 27 | Err(e) => eprintln!("There was an error authenticating: {}", e), 28 | } 29 | ``` 30 | 31 | There is an asynchronous variant: `Verifier::make_verify_request_async` which returns a 32 | future. You can also deserialize the data into a `SteamLoginData` struct and construct a 33 | `Verifier` from that if it is more convenient. 34 | 35 | If you don't want to depend on request, you'll need to send the HTTP request yourself. See the 36 | [example server](https://github.com/64/steam-auth/blob/master/examples/server.rs) and the 37 | `Verifier` documentation for more details on how this can be done. 38 | 39 | MIT Licensed. Pull requests and contributions welcome. 40 | -------------------------------------------------------------------------------- /src/redirector.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, STEAM_URL}; 2 | use url::Url; 3 | 4 | #[derive(Debug, Clone)] 5 | /// Stores the URL that users should be redirected to. 6 | pub struct Redirector { 7 | url: Url, 8 | } 9 | 10 | impl Redirector { 11 | /// # Example 12 | /// ``` 13 | /// # use steam_auth::Redirector; 14 | /// # fn main() { 15 | /// let redirector = Redirector::new("http://localhost:8080", "/callback"); 16 | /// # } 17 | /// ``` 18 | pub fn new, U: AsRef>(site_url: T, return_url: U) -> Result { 19 | let joined = Url::parse(site_url.as_ref()) 20 | .map_err(Error::BadUrl)? 21 | .join(return_url.as_ref()) 22 | .map_err(Error::BadUrl)?; 23 | 24 | let openid = SteamAuthRequest::new(site_url.as_ref(), joined.as_str()); 25 | 26 | let qs = serde_urlencoded::to_string(&openid).map_err(Error::ParseQueryString)?; 27 | 28 | // Shouldn't happen 29 | let mut url = Url::parse(STEAM_URL).map_err(Error::BadUrl)?; 30 | 31 | url.set_query(Some(&qs)); 32 | 33 | Ok(Self { url }) 34 | } 35 | 36 | /// Constructs a new HTTP response which redirects the user to the URL, starting the login 37 | /// process. 38 | pub fn create_response(&self) -> Result, Error> { 39 | http::Response::builder() 40 | .status(http::StatusCode::FOUND) 41 | .header("Location", self.url.as_str()) 42 | .body(()) 43 | .map_err(Error::BuildHttpStruct) 44 | } 45 | 46 | /// Gets the URL to which users should be redirected. 47 | pub fn url(&self) -> &Url { 48 | &self.url 49 | } 50 | } 51 | 52 | #[derive(Serialize)] 53 | struct SteamAuthRequest<'a> { 54 | #[serde(rename = "openid.ns")] 55 | ns: &'static str, 56 | #[serde(rename = "openid.identity")] 57 | identity: &'static str, 58 | #[serde(rename = "openid.claimed_id")] 59 | claimed_id: &'static str, 60 | #[serde(rename = "openid.mode")] 61 | mode: &'static str, 62 | #[serde(rename = "openid.return_to")] 63 | return_to: &'a str, 64 | #[serde(rename = "openid.realm")] 65 | realm: &'a str, 66 | } 67 | 68 | impl<'a> SteamAuthRequest<'a> { 69 | fn new(site_url: &'a str, return_to_joined: &'a str) -> Self { 70 | Self { 71 | ns: "http://specs.openid.net/auth/2.0", 72 | identity: "http://specs.openid.net/auth/2.0/identifier_select", 73 | claimed_id: "http://specs.openid.net/auth/2.0/identifier_select", 74 | mode: "checkid_setup", 75 | realm: site_url, 76 | return_to: return_to_joined, 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /examples/server.rs: -------------------------------------------------------------------------------- 1 | use simple_server::{Method, Server, StatusCode}; 2 | 3 | fn main() { 4 | let host = "127.0.0.1"; 5 | let port = "8080"; 6 | 7 | println!("Starting server on localhost:8080"); 8 | 9 | let redirector = steam_auth::Redirector::new("http://localhost:8080", "/callback").unwrap(); 10 | 11 | #[cfg(feature = "reqwest-09x")] 12 | let client = reqwest::Client::new(); 13 | 14 | let server = Server::new(move |request, mut response| { 15 | match (request.method(), request.uri().path()) { 16 | (&Method::GET, "/") => { 17 | Ok(response.body(" 18 | 19 | 20 | 21 | ".as_bytes().to_vec())?) 22 | } 23 | (&Method::GET, "/login") => { 24 | // Redirect user to redirect_url 25 | response.status(StatusCode::FOUND); 26 | response.header("Location", redirector.url().as_str()); 27 | Ok(response.body(Vec::new())?) 28 | } 29 | (&Method::GET, "/callback") => { 30 | // Parse query string data into auth_resp 31 | let qs = request.uri().query().unwrap(); 32 | 33 | // Check with the steam servers if the response was valid 34 | #[cfg(feature = "reqwest-09x")] 35 | match steam_auth::Verifier::make_verify_request(&client, qs) { 36 | Ok(id) => Ok(response.body(format!("

Success

Steam ID: {}

", id).as_bytes().to_vec())?), 37 | Err(e) => Ok(response.body(format!("

Error

Description: {}

", dbg!(e)).as_bytes().to_vec())?), 38 | } 39 | 40 | #[cfg(not(feature = "reqwest-09x"))] 41 | { 42 | // TODO: Example usage of the API without reqwest 43 | /* 44 | let (req, verifier) = Verifier::from_querystring(qs).unwrap(); 45 | // send off req, get back response 46 | match verifier.verify_response(response.body()) { 47 | Ok(steam_id) => (), // got steam id 48 | Err(e) => (), // Auth failure 49 | } 50 | */ 51 | unimplemented!(); 52 | } 53 | } 54 | (_, _) => { 55 | response.status(StatusCode::NOT_FOUND); 56 | Ok(response.body("

404

Not found!

".as_bytes().to_vec())?) 57 | } 58 | } 59 | }); 60 | 61 | server.listen(host, port); 62 | } 63 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Allows you to implement a 'login with steam' feature on your website. 2 | //! 3 | //! ## Usage 4 | //! 5 | //! The easiest way to use this crate is with the `reqwest-09x` feature which allows the library to 6 | //! make HTTP requests on your behalf. Otherwise, you will need to do that manually. 7 | //! 8 | //! Using the `reqwest-09x` feature: 9 | //! ```rust 10 | //! # use steam_auth::{Redirector, Verifier}; 11 | //! # fn main() { 12 | //! // First, create a redirector 13 | //! let redirector = Redirector::new("http://localhost:8080", "/callback").unwrap(); 14 | //! 15 | //! // When a user wants to log in with steam, (e.g when they land on the `/login` route), 16 | //! // redirect them to this URL: 17 | //! let redirect_url = redirector.url(); 18 | //! 19 | //! // Once they've finished authenticating, they will be returned to `/callback` with some data in 20 | //! // the query string that needs to be parsed and then verified by sending an HTTP request to the steam 21 | //! // servers. 22 | //! # let querystring = "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"; 23 | //! match Verifier::make_verify_request(&reqwest::Client::new(), querystring) { 24 | //! Ok(steam_id) => println!("Successfully logged in user with steam ID 64 {}", steam_id), 25 | //! Err(e) => eprintln!("There was an error authenticating: {}", e), 26 | //! } 27 | //! # } 28 | //! ``` 29 | //! 30 | //! There is an asynchronous variant: `Verifier::make_verify_request_async` which returns a 31 | //! future. You can also deserialize the data into a `SteamLoginData` struct and construct a 32 | //! `Verifier` from that if it is more convenient. 33 | //! 34 | //! If you don't want to depend on request, you'll need to send the HTTP request yourself. See the 35 | //! [example server](https://github.com/64/steam-auth/blob/master/examples/server.rs) and the 36 | //! `Verifier` documentation for more details on how this can be done. 37 | 38 | #[macro_use] 39 | extern crate serde_derive; 40 | #[macro_use] 41 | extern crate failure; 42 | 43 | mod redirector; 44 | mod verifier; 45 | 46 | pub use redirector::Redirector; 47 | pub use verifier::SteamLoginData; 48 | pub use verifier::Verifier; 49 | 50 | pub(crate) const STEAM_URL: &str = "https://steamcommunity.com/openid/login"; 51 | 52 | #[derive(Debug, Fail)] 53 | pub enum Error { 54 | #[fail(display = "bad site or return url: {}", _0)] 55 | /// The site or return URL was incorrect 56 | BadUrl(url::ParseError), 57 | #[fail(display = "failed to parse SteamAuthRequest (please file bug): {}", _0)] 58 | /// Internal error serializing the query string - should never happen. 59 | ParseQueryString(serde_urlencoded::ser::Error), 60 | #[fail(display = "authentication failed")] 61 | /// The authentication failed because the data provided to the callback was invalid 62 | AuthenticationFailed, 63 | #[fail(display = "failed to parse steam id")] 64 | /// There was an error parsing the Steam ID returned to the callback 65 | ParseSteamId, 66 | #[fail(display = "failed to build HTTP request or response: {}", _0)] 67 | BuildHttpStruct(http::Error), 68 | #[fail(display = "error serializing url encoded data: {}", _0)] 69 | Serialize(serde_urlencoded::ser::Error), 70 | #[fail(display = "error deserializing url encoded data: {}", _0)] 71 | Deserialize(serde_urlencoded::de::Error), 72 | #[fail(display = "reqwest error: {}", _0)] 73 | #[cfg(feature = "reqwest-09x")] 74 | /// There was an error during the verify request 75 | Reqwest(reqwest::Error), 76 | } 77 | 78 | #[cfg(feature = "reqwest-0_9")] 79 | pub fn verify_response_async( 80 | client: &reqwest::r#async::Client, 81 | mut form: SteamAuthResponse, 82 | ) -> impl futures::Future { 83 | client 84 | .post(STEAM_URL) 85 | .form(&form) 86 | .send() 87 | .map_err(Error::Reqwest) 88 | .and_then(|res| res.into_body().concat2().map_err(Error::Reqwest)) 89 | .and_then(move |body| { 90 | let s = std::str::from_utf8(&body) 91 | .map_err(|_| Error::AuthenticationFailed)? 92 | .to_owned(); 93 | 94 | parse_verify_response(&form.claimed_id, s) 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /src/verifier.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, STEAM_URL}; 2 | 3 | #[cfg(feature = "reqwest-09x")] 4 | use 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 | /// ``` 27 | pub struct Verifier { 28 | claimed_id: u64, 29 | } 30 | 31 | impl 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: S) -> Result<(http::Request>, 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>, 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::().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>(self, response_body: S) -> Result { 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>( 96 | client: &reqwest::Client, 97 | querystring: S, 98 | ) -> Result { 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>( 120 | client: &reqwest::r#async::Client, 121 | querystring: S, 122 | ) -> impl Future { 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)] 150 | pub 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, 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, 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 | } 174 | --------------------------------------------------------------------------------