Skip to main content

fizzy_sdk/
auth.rs

1//! How credentials go on a request. Fizzy takes a bearer token (`Authorization: Bearer`)
2//! for API access tokens, or a session cookie (`Cookie: session_token=`) for a session
3//! signed in through a magic link. There is no OAuth and no refresh: a 401 is final.
4
5use async_trait::async_trait;
6use bytes::Bytes;
7use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode};
8
9use crate::error::Error;
10use crate::http::header::{AUTHORIZATION, COOKIE};
11use crate::http::{HeaderValue, Request};
12use crate::types::SensitiveString;
13
14/// The cookie a signed-in session is carried in.
15pub const SESSION_COOKIE: &str = "session_token";
16
17/// What Rack's cookie parser decodes: a `%XX` escape, and `+` as a space. A token is sent
18/// with everything outside the unreserved set escaped, so a `+` or a `/` in a signed
19/// token reaches Rails as itself — the way Rails' own `cookies[]=` writes it out.
20const COOKIE_VALUE: &AsciiSet = &NON_ALPHANUMERIC
21    .remove(b'-')
22    .remove(b'_')
23    .remove(b'.')
24    .remove(b'~');
25
26/// A `Cookie` header value carrying one cookie, escaped for Rack, marked sensitive so the
27/// `http` crate's `Debug` prints it as such.
28pub(crate) fn cookie_header(name: &str, value: &str) -> Result<HeaderValue, Error> {
29    let encoded = utf8_percent_encode(value, COOKIE_VALUE);
30    let mut header = HeaderValue::from_str(&format!("{name}={encoded}"))
31        .map_err(|_| Error::auth(format!("{name} is not a valid cookie value")))?;
32    header.set_sensitive(true);
33    Ok(header)
34}
35
36/// Supplies the token each request goes out with.
37#[async_trait]
38pub trait TokenProvider: Send + Sync {
39    /// The token to send now.
40    async fn access_token(&self) -> Result<String, Error>;
41}
42
43/// A fixed token, from an environment variable say. It prints as `[REDACTED]`, so a `{:?}`
44/// of the provider — or of anything holding one — cannot put the token in a log.
45#[derive(Debug, Clone)]
46pub struct StaticTokenProvider {
47    /// The token.
48    pub token: SensitiveString,
49}
50
51impl StaticTokenProvider {
52    /// Wraps a token.
53    pub fn new(token: impl Into<SensitiveString>) -> StaticTokenProvider {
54        StaticTokenProvider {
55            token: token.into(),
56        }
57    }
58}
59
60#[async_trait]
61impl TokenProvider for StaticTokenProvider {
62    async fn access_token(&self) -> Result<String, Error> {
63        if self.token.is_empty() {
64            Err(Error::auth("no token configured"))
65        } else {
66            Ok(self.token.expose().to_string())
67        }
68    }
69}
70
71/// Puts credentials on a request. [`BearerAuth`] and [`CookieAuth`] are the two Fizzy
72/// takes; anything else can plug in here.
73#[async_trait]
74pub trait AuthStrategy: Send + Sync {
75    /// Adds whatever the request needs to be recognized.
76    async fn authenticate(&self, request: &mut Request<Bytes>) -> Result<(), Error>;
77}
78
79/// `Authorization: Bearer <token>`, for API access tokens.
80pub struct BearerAuth<P: TokenProvider> {
81    provider: P,
82}
83
84impl<P: TokenProvider> BearerAuth<P> {
85    /// Bearer auth over a provider.
86    pub fn new(provider: P) -> BearerAuth<P> {
87        BearerAuth { provider }
88    }
89}
90
91#[async_trait]
92impl<P: TokenProvider> AuthStrategy for BearerAuth<P> {
93    async fn authenticate(&self, request: &mut Request<Bytes>) -> Result<(), Error> {
94        let token = self.provider.access_token().await?;
95        let mut value = HeaderValue::from_str(&format!("Bearer {token}"))
96            .map_err(|_| Error::auth("access token is not a valid header value"))?;
97        value.set_sensitive(true);
98        request.headers_mut().insert(AUTHORIZATION, value);
99        Ok(())
100    }
101}
102
103/// `Cookie: session_token=<token>`, for a session signed in through a magic link.
104pub struct CookieAuth<P: TokenProvider> {
105    provider: P,
106}
107
108impl<P: TokenProvider> CookieAuth<P> {
109    /// Cookie auth over a provider.
110    pub fn new(provider: P) -> CookieAuth<P> {
111        CookieAuth { provider }
112    }
113}
114
115#[async_trait]
116impl<P: TokenProvider> AuthStrategy for CookieAuth<P> {
117    async fn authenticate(&self, request: &mut Request<Bytes>) -> Result<(), Error> {
118        let token = self.provider.access_token().await?;
119        request
120            .headers_mut()
121            .insert(COOKIE, cookie_header(SESSION_COOKIE, &token)?);
122        Ok(())
123    }
124}
125
126/// No credentials at all, for the calls that come before there are any: creating a session
127/// and redeeming its magic link.
128#[derive(Debug, Clone, Copy, Default)]
129pub struct NoAuth;
130
131#[async_trait]
132impl AuthStrategy for NoAuth {
133    async fn authenticate(&self, _request: &mut Request<Bytes>) -> Result<(), Error> {
134        Ok(())
135    }
136}