1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
|
//!
//! This example showcases the Letterboxd OAuth2 process for requesting access
//! to the API features restricted by authentication. Letterboxd requires all
//! requests being signed as described in http://api-docs.letterboxd.com/#signing.
//! So this serves as an example how to implement a custom client, which signs
//! requests and appends the signature to the url query.
//!
//! Before running it, you'll need to get access to the API.
//!
//! In order to run the example call:
//!
//! ```sh
//! LETTERBOXD_CLIENT_ID=xxx LETTERBOXD_CLIENT_SECRET=yyy LETTERBOXD_USERNAME=www LETTERBOXD_PASSWORD=zzz cargo run --example letterboxd
//! ```
use hex::ToHex;
use hmac::{Hmac, Mac};
use oauth2::{
basic::BasicClient, AuthType, AuthUrl, ClientId, ClientSecret, HttpRequest, HttpResponse,
ResourceOwnerPassword, ResourceOwnerUsername, SyncHttpClient, TokenUrl,
};
use sha2::Sha256;
use url::Url;
use std::env;
use std::time;
fn main() -> Result<(), anyhow::Error> {
// a.k.a api key in Letterboxd API documentation
let letterboxd_client_id = ClientId::new(
env::var("LETTERBOXD_CLIENT_ID")
.expect("Missing the LETTERBOXD_CLIENT_ID environment variable."),
);
// a.k.a api secret in Letterboxd API documentation
let letterboxd_client_secret = ClientSecret::new(
env::var("LETTERBOXD_CLIENT_SECRET")
.expect("Missing the LETTERBOXD_CLIENT_SECRET environment variable."),
);
// Letterboxd uses the Resource Owner flow and does not have an auth url
let auth_url = AuthUrl::new("https://api.letterboxd.com/api/v0/auth/404".to_string())?;
let token_url = TokenUrl::new("https://api.letterboxd.com/api/v0/auth/token".to_string())?;
// Set up the config for the Letterboxd OAuth2 process.
let client = BasicClient::new(letterboxd_client_id.clone())
.set_client_secret(letterboxd_client_secret.clone())
.set_auth_uri(auth_url)
.set_token_uri(token_url);
// Resource Owner flow uses username and password for authentication
let letterboxd_username = ResourceOwnerUsername::new(
env::var("LETTERBOXD_USERNAME")
.expect("Missing the LETTERBOXD_USERNAME environment variable."),
);
let letterboxd_password = ResourceOwnerPassword::new(
env::var("LETTERBOXD_PASSWORD")
.expect("Missing the LETTERBOXD_PASSWORD environment variable."),
);
// All API requests must be signed as described at http://api-docs.letterboxd.com/#signing;
// for that, we use a custom http client.
let http_client = SigningHttpClient::new(letterboxd_client_id, letterboxd_client_secret);
let token_result = client
.set_auth_type(AuthType::RequestBody)
.exchange_password(&letterboxd_username, &letterboxd_password)
.request(&|request| http_client.execute(request))?;
println!("{token_result:?}");
Ok(())
}
/// Custom HTTP client which signs requests.
///
/// See http://api-docs.letterboxd.com/#signing.
#[derive(Debug, Clone)]
struct SigningHttpClient {
client_id: ClientId,
client_secret: ClientSecret,
inner: reqwest::blocking::Client,
}
impl SigningHttpClient {
fn new(client_id: ClientId, client_secret: ClientSecret) -> Self {
Self {
client_id,
client_secret,
inner: reqwest::blocking::ClientBuilder::new()
// Following redirects opens the client up to SSRF vulnerabilities.
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("Client should build"),
}
}
/// Signs the request before calling `oauth2::reqwest::http_client`.
fn execute(&self, mut request: HttpRequest) -> Result<HttpResponse, impl std::error::Error> {
let signed_url = self.sign_url(
Url::parse(&request.uri().to_string()).expect("http::Uri should be a valid url::Url"),
request.method(),
request.body(),
);
*request.uri_mut() = signed_url
.as_str()
.try_into()
.expect("url::Url should be a valid http::Uri");
self.inner.call(request)
}
/// Signs the request based on a random and unique nonce, timestamp, and
/// client id and secret.
///
/// The client id, nonce, timestamp and signature are added to the url's
/// query.
///
/// See http://api-docs.letterboxd.com/#signing.
fn sign_url(&self, mut url: Url, method: &http::method::Method, body: &[u8]) -> Url {
let nonce = uuid::Uuid::new_v4(); // use UUID as random and unique nonce
let timestamp = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)
.expect("SystemTime::duration_since failed")
.as_secs();
url.query_pairs_mut()
.append_pair("apikey", &self.client_id)
.append_pair("nonce", &format!("{}", nonce))
.append_pair("timestamp", &format!("{}", timestamp));
// create signature
let mut hmac = Hmac::<Sha256>::new_from_slice(self.client_secret.secret().as_bytes())
.expect("HMAC can take key of any size");
hmac.update(method.as_str().as_bytes());
hmac.update(&[b'\0']);
hmac.update(url.as_str().as_bytes());
hmac.update(&[b'\0']);
hmac.update(body);
let signature: String = hmac.finalize().into_bytes().encode_hex();
url.query_pairs_mut().append_pair("signature", &signature);
url
}
}
|