I am writing a fullstack application and I have decided to use svelte as the frontend with a Rocket rust backend:
struct QuickbooksAuth;
#[get("/login/quickbooks")]
fn quickbooks_login(oauth2: OAuth2<QuickbooksAuth>, cookies: &CookieJar<'_>) -> Redirect {
oauth2
.get_redirect(cookies, &["com.intuit.quickbooks.accounting"])
.unwrap()
}
#[get("/auth/quickbooks")]
fn quickbooks_callback(token: TokenResponse<QuickbooksAuth>, cookies: &CookieJar<'_>) -> Redirect {
cookies.add_private(
// Todo encrypt this before saving to a cookie
Cookie::build(("qb_access_token", token.access_token().to_string()))
.same_site(rocket::http::SameSite::Lax)
.build(),
);
Redirect::to("http://localhost:5173/") // The home page of the frontend
}
struct QBAccessToken(String);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for QBAccessToken {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
request
.cookies()
.get_private("qb_access_token")
.map(|f| QBAccessToken(f.value().to_owned()))
.or_forward(rocket::http::Status::Unauthorized)
}
}
// The endpoints that use the access token
#[get("/?<claim_number>&<get_qb>&<get_sb>")]
async fn get_claim(
claim_number: String,
get_qb: bool,
get_sb: bool,
qb: &State<Quickbooks>,
sp: &State<ClaimHandler>,
access_token: QBAccessToken, // Need this for api calls
) -> Result<HAInvoice, ServiceBooksError> {
...
}
and the svelte frontend has a login button that links to the login endpoint:
<a
class="titlebar-button"
id="titlebar-qb-login"
style="right:0px"
href="http://127.0.0.1:7777/login/quickbooks"
> <img src="some_logo.svg">
</a>
and then tries to use that when fetching the api later:
const url = new URL("http://127.0.0.1:7777/claim");
url.searchParams.append("claim_number", claimNumber);
url.searchParams.append("get_qb", getQb);
url.searchParams.append("get_sb", getSb);
try {
loading = true;
const response = await fetch(url);
const data = await response.text();
claim = data;
} catch (error) {
notifications.danger(error.message, 2000);
} finally {
loading = false;
}
That way the access token is only used by the backend and the frontend isn't exposed to it, which I thought would be more secure. When I go to the http://localhost:7777/login/quickbooks endpoint manually it goes through and redirects to the frontend without a problem, but any subsequent api calls are blocked for being unauthorized, and when trying the link from the frontend it throws a bad request error on the auth/quickbooks endpoint for the state having a mismatch:
Matched: (quickbooks_callback) GET /auth/quickbooks
ERROR rocket_oauth2 > The OAuth2 state cookie was missing. It may have been blocked by the client?
WARN servicebooks_server::_ > Request guard `TokenResponse < QuickbooksAuth >` failed: Error { kind: ExchangeFailure, source: Some("The OAuth2 state returned from the server did match the stored state.") }.
INFO rocket::server::_ > Outcome: Error(400 Bad Request)
WARN rocket::server::_ > No 400 catcher registered. Using Rocket default.
I've already disabled anything blocking cookies in my browser and I've tried clearing the cache and restarting everything I can think of. I suspect this is because the cookies from the /login endpoint are not being retained and passed along. How can I ensure the OAuth state and cookies are maintained throughout the authorization flow and API calls from my Svelte frontend? Do I need to manually save and send the cookie with each request? Or, is this not how OAuth should be used? if so, is there a simple way to convert what I have now to something that is more in line with what's intended?
Edit: I am using the rocket_oauth2 crate to handle the oauth credentials: https://docs.rs/rocket_oauth2/latest/rocket_oauth2/