Deserializing lifetime problems

I'm trying to implement OAuth2 in my Rocket.rs app, and I keep getting this error in my code.

`token` does not live long enough
borrowed value does not live long enough

Here's the code:

use rocket::serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use tokio::sync::RwLock;

use oxide_auth::endpoint::UniqueValue;
use reqwest::{header, Response};

// ...

pub async fn parse_token_response<'r>(
    response: Response,
) -> Result<TokenMap<'r>, serde_json::Error> {
    let tkn = response.text().await;
    let token = tkn.unwrap();
    let res: Result<TokenMap<'r>, serde_json::Error> = serde_json::from_str(&token);

    match res {
        Ok(val) => return Ok(val.clone()),
        Err(e) => return Err(e),
    };
}

Can someone please help me?

The error is saying that token goes out of scope when the function returns, but the return value borrows from it. This is a result of moving Response into the async function and then trying to return a value that borrows from it. One solution is to pass &'r Response to the function instead, so that the caller can keep it alive long enough for the returned borrow to remain valid.

Another option could be making TokenMap own its value instead of borrowing, if that is under your control.

1 Like

If I do &'r Response it gives me this error:

cannot move out of `*response` which is behind a shared reference
move occurs because `*response` has type `reqwest::Response`, which does not implement the `Copy` trait

Also, I don't know what you mean by "making TokemMap own its value". The error comes from trying to deserialize, so I don't think it has to do with that.

TokenMap<'_> says that it contains a borrow that must live for some lifetime. The particular lifetime you are using ends when the function ends.

It is unclear which line this error is referring to. I suspect it is the response.text() because that call requires consuming the Request. Please paste the full error messages to avoid any confusion.

Maybe that can be hacked around by using response.chunk() instead? Or remove the borrow from TokenMap (again, if that's an option ... I don't know the definition of this struct since it is missing from your code snippets). Serde is perfectly capable of deserializing into owned values.

Yeah, it was response.text(). I can try response.chunk() but I don't know. It's not the TokenMap creation that is the error, it's this line:

serde_json::from_str(&token /* <<< THIS */);

Why do you say that? The &token is perfectly reasonable as long as TokenMap owns its contents.

The problem is that the function does not own the data. Here's the whole file:

use rocket::serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use tokio::sync::RwLock;

use oxide_auth::endpoint::UniqueValue;
use reqwest::{header, Response};

#[derive(Clone)]
pub struct Client<'r> {
    config: Config<'r>,
    state: Arc<RwLock<State<'r>>>,
}

unsafe impl<'r> Send for Client<'r> {}
unsafe impl<'r> Sync for Client<'r> {}

#[derive(Clone, Copy)]
pub struct Config<'r> {
    pub protected_url: &'r str,
    pub token_url: &'r str,
    pub refresh_url: &'r str,
    pub client_id: &'r str,
    pub redirect_uri: &'r str,
    pub client_secret: Option<&'r str>,
}

unsafe impl<'r> Send for Config<'r> {}
unsafe impl<'r> Sync for Config<'r> {}

pub enum Error {
    AccessFailed,
    NoToken,
    AuthorizationFailed,
    RefreshFailed,
    Invalid(serde_json::Error),
    MissingToken,
    Response(String),
}

unsafe impl Send for Error {}
unsafe impl Sync for Error {}

#[derive(Debug, Default, Clone, Copy)]
pub struct State<'r> {
    pub token: Option<&'r str>,
    pub refresh: Option<&'r str>,
    pub until: Option<i64>,
}

unsafe impl<'r> Send for State<'r> {}
unsafe impl<'r> Sync for State<'r> {}

#[derive(Serialize, Deserialize, Clone, Copy)]
#[serde(crate = "rocket::serde")]
pub struct TokenMap<'r> {
    token_type: &'r str,

    scope: &'r str,

    #[serde(skip_serializing_if = "Option::is_none")]
    access_token: Option<&'r str>,

    #[serde(skip_serializing_if = "Option::is_none")]
    refresh_token: Option<&'r str>,

    #[serde(skip_serializing_if = "Option::is_none")]
    expires_in: Option<i64>,

    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<&'r str>,
}

impl<'r> Client<'r> {
    pub fn new(config: Config<'r>) -> Self {
        Client {
            config,
            state: Arc::new(RwLock::new(State::default())),
        }
    }

    pub async fn authorize<'a>(&self, code: &'a str) -> Result<(), Error> {
        let client = reqwest::Client::new();
        let mut state = self.state.write().await;

        let mut params = HashMap::new();
        params.insert("grant_type", "authorization_code");
        params.insert("code", code);
        params.insert("redirect_uri", &self.config.redirect_uri);

        let access_token_request = match &self.config.client_secret {
            Some(client_secret) => client
                .post(self.config.token_url)
                .form(&params)
                .basic_auth(&self.config.client_id, client_secret.get_unique())
                .build()
                .unwrap(),

            None => {
                params.insert("client_id", &self.config.client_id);
                client
                    .post(self.config.token_url)
                    .form(&params)
                    .build()
                    .unwrap()
            }
        };

        let token_response = client
            .execute(access_token_request)
            .await
            .map_err(|_| Error::AuthorizationFailed)?;

        let token_map: TokenMap<'r> = parse_token_response(token_response).await.unwrap();

        if let Some(err) = token_map.error {
            return Err(Error::Response(err.to_string()));
        }

        if let Some(token) = token_map.access_token {
            state.token = Some(token);
            state.refresh = token_map.refresh_token;
            state.until = token_map.expires_in;
            return Ok(());
        }

        Err(Error::MissingToken)
    }

    pub async fn retrieve_protected_page(&self) -> Result<String, Error> {
        let client = reqwest::Client::new();

        let state = self.state.read().await;
        let token = match state.token {
            Some(token) => token,
            None => return Err(Error::NoToken),
        };

        // Request the page with the oauth token
        let page_request = client
            .get(self.config.protected_url)
            .header(header::AUTHORIZATION, "Bearer ".to_string() + token)
            .build()
            .unwrap();

        let page_response = match client.execute(page_request).await {
            Ok(response) => response,
            Err(_) => return Err(Error::AccessFailed),
        };

        let protected_page = page_response.text().await.unwrap();

        Ok(protected_page)
    }

    pub async fn refresh(&self) -> Result<(), Error> {
        let client = reqwest::Client::new();

        let mut state = self.state.write().await;
        let refresh = match state.refresh {
            Some(ref refresh) => refresh.clone(),
            None => return Err(Error::NoToken),
        };

        let mut params = HashMap::new();
        params.insert("grant_type", "refresh_token");
        params.insert("refresh_token", &refresh);

        let access_token_request = match &self.config.client_secret {
            Some(client_secret) => client
                .post(self.config.refresh_url)
                .form(&params)
                .basic_auth(&self.config.client_id, client_secret.get_unique())
                .build()
                .unwrap(),
            None => {
                params.insert("client_id", &self.config.client_id);
                client
                    .post(self.config.refresh_url)
                    .form(&params)
                    .build()
                    .unwrap()
            }
        };

        let token_response = client
            .execute(access_token_request)
            .await
            .map_err(|_| Error::RefreshFailed)?;

        let token_map: TokenMap = parse_token_response(token_response).await.unwrap();

        if token_map.error.is_some() || !token_map.access_token.is_some() {
            return Err(Error::MissingToken);
        }

        let token = token_map.access_token.unwrap();
        state.token = Some(token);
        state.refresh = token_map.refresh_token.or(state.refresh.take());
        state.until = token_map.expires_in;
        Ok(())
    }

    pub async fn as_html(&self) -> String {
        format!("{}", self.state.read().await)
    }
}

pub async fn parse_token_response<'r>(
    response: Response,
) -> Result<TokenMap<'r>, serde_json::Error> {
    let tkn = response.text().await;
    let token = tkn.unwrap();
    let res: Result<TokenMap<'r>, serde_json::Error> = serde_json::from_str(&token /* <<< here is the error */);

    match res {
        Ok(val) => return Ok(val.clone()),
        Err(e) => return Err(e),
    };
}

impl From<serde_json::Error> for Error {
    fn from(err: serde_json::Error) -> Self {
        Error::Invalid(err)
    }
}

impl<'r> fmt::Display for State<'r> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str("Token {<br>")?;
        write!(f, "&nbsp;token: {:?},<br>", self.token)?;
        write!(f, "&nbsp;refresh: {:?},<br>", self.refresh)?;
        write!(f, "&nbsp;expires_in: {:?},<br>", self.until)?;
        f.write_str("}")
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Error::AuthorizationFailed => f.write_str("Could not fetch bearer token"),
            Error::NoToken => f.write_str("No token with which to access protected page"),
            Error::AccessFailed => {
                f.write_str("Access token failed to authorize for protected page")
            }
            Error::RefreshFailed => f.write_str("Could not refresh bearer token"),
            Error::Invalid(serde) => write!(f, "Bad json response: {}", serde),
            Error::MissingToken => write!(f, "No token nor error in server response"),
            Error::Response(err) => write!(f, "Server error while fetching token: {}", err),
        }
    }
}

Any particular reason that TokenMap holds &r str instead of String?

Because if I don't do that, Rocket yells at me for async methods and not being able to pass futures with threads and crap.

You are frustrated because you are using a screwdriver for hitting a nail.

Read about the Send trait in Rust.

1 Like

I just spent the last 10 minutes reverse engineering the dependencies you are using to find out why rocket would require anything of the sort. All I can say is that removing the Copy implementation and changing &'r str to String for both State and TokenMap to remove that lifetime annotation makes the compiler happy.

The unsafe impl in your code is concerning but it doesn't really have anything to do with using owned strings instead of borrowing things on the stack that disappear immediately.

It's not about the Send trait. It's that Serde JSON doesn't like deserializing a non-reference.

Use of temporary scope-bound loans in structs that aren't views into pre-existing data is a design error. In your case Config should use String or Box<str>.

For context, here's the file that caused this whole thing:

use super::generic::{Client, ClientConfig, ClientError};

use rocket::fairing::{Fairing, Info, Kind, Result};
use rocket::http::Status;
use rocket::response::{content::RawHtml, status::Custom, Redirect};
use rocket::{get, post, routes, Build, Rocket, State};
use std::sync::Arc;

pub use super::generic::consent_page_html;
pub struct ClientFairing;

#[rocket::async_trait]
impl Fairing for ClientFairing {
    fn info(&self) -> Info {
        Info {
            name: "Simple oauth client implementation",
            kind: Kind::Ignite,
        }
    }

    async fn on_ignite(&self, rocket: Rocket<Build>) -> Result {
        let config = ClientConfig {
            client_id: "LocalClient".into(),
            protected_url: "https://[redacted]/api/v1/oauth/".into(),
            token_url: "https://[redacted]/api/v1/oauth/token".into(),
            refresh_url: "https://[redacted]/api/v1/oauth/refresh".into(),
            redirect_uri: "https://[redacted]/api/v1/oauth/clientside/endpoint"
                .into(),
            client_secret: None,
        };

        Ok(rocket.manage(Arc::new(Client::new(config))).mount(
            "/api/v1/oauth/clientside",
            routes![oauth_endpoint, client_view, client_debug, refresh],
        ))
    }
}

#[get("/endpoint?<code>&<error>")]
pub async fn oauth_endpoint<'r>(
    code: Option<String>,
    error: Option<String>,
    state: &State<Client<'r>>,
) -> Result<Redirect, Custom<String>> {
    if let Some(error) = error {
        return Err(Custom(
            Status::InternalServerError,
            format!("Error during owner authorization: {:?}", error),
        ));
    }

    let code = code.ok_or_else(|| {
        Custom(
            Status::BadRequest,
            "Endpoint hit without an authorization code".into(),
        )
    })?;

    state.authorize(&code).await.map_err(internal_error)?;

    Ok(Redirect::found("/api/v1/oauth/clientside"))
}

#[get("/")]
pub async fn client_view<'r>(state: &State<Client<'r>>) -> Result<RawHtml<String>, Custom<String>> {
    let protected_page = state
        .retrieve_protected_page()
        .await
        .map_err(internal_error)?;

    let display_page = format!(
        "<html><style>
            aside{{overflow: auto; word-break: keep-all; white-space: nowrap}}
            main{{text-align: center}}
            main>aside,main>article{{margin: auto; text-align: left; border: 1px solid black; width: 50%}}
        </style>
        <main>
        Used token <aside style>{}</aside> to access
        <a href=\"https://[redacted]/api/v1/oauth/\">https://[redacted]/api/v1/oauth/</a>.
        Its contents are:
        <article>{:?}</article>
        <form action=\"/clientside/refresh\" method=\"post\"><button>Refresh token</button></form>
        </main></html>", state.as_html().await, protected_page);

    Ok(RawHtml(display_page))
}

#[post("/refresh")]
pub async fn refresh<'r>(state: &State<Client<'r>>) -> Result<Redirect, Custom<String>> {
    state
        .refresh()
        .await
        .map_err(internal_error)
        .map(|()| Redirect::found("/api/v1/oauth/clientside"))
}

#[get("/debug")]
pub async fn client_debug<'r>(state: &State<Arc<Client<'r>>>) -> RawHtml<String> {
    RawHtml(state.as_html().await)
}

pub fn internal_error(err: ClientError) -> Custom<String> {
    Custom(Status::InternalServerError, err.to_string())
}

That's not where the error is from.

It does, though: from_str in serde_json - Rust (docs.rs)

User owns its contents. It does not have to borrow from j.

1 Like
let tkn = response.text().await; // <<< Result<String, ...>
let token = tkn.unwrap(); // <<< String
let res: Result<TokenMap<'r>, serde_json::Error> = serde_json::from_str(&token /* Needs &str, but lifetimes */);

You seem to overuse references and lifetimes without understanding what they're for.

Lifetime marks where the data is borrowing from. If you have a function in the form:

fn foo<'r>(/* no lifetimes here */) -> Data<'r>

it is illogical in Rust. There is no lifetime that can satisfy this except 'static, where 'static means it's either a constant compiled into the executable or leaked memory.

Your TokenMap<'r> borrows from a local variable, and local variables are destroyed before end of the function, so the temporary not-stored-anywhere-else data that TokenMap<'r> points to is gone before you return it.

4 Likes

I don't understand.

Short version is: don't put & in a struct.

It's an advanced feature that does something else, not what you assume it does. Structs with a lifetime like struct Foo<'a> are a special case, that is rarely useful, and is difficult to use properly. Without good understanding of ownership and borrowing, it will make your life hard.

It's a common novice mistake to use references with intention to avoid copying data, but instead they avoid owning the data, and make programs impossible to compile.

2 Likes