letterboxd.rs 4.8 KB

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