Implement JWT refreshing.
This commit is contained in:
parent
01e90ea0a1
commit
5986a3611a
|
@ -3518,6 +3518,7 @@ dependencies = [
|
|||
"rocket",
|
||||
"rocket_cors",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"substring",
|
||||
"tenebrous-rpc",
|
||||
"thiserror",
|
||||
|
|
|
@ -14,6 +14,7 @@ substring = "1.4"
|
|||
jsonwebtoken = "7.2"
|
||||
chrono = "0.4"
|
||||
serde = {version = "1.0", features = ["derive"] }
|
||||
serde_json = {version = "1.0" }
|
||||
tenebrous-rpc = { path = "../rpc" }
|
||||
juniper = { git = "https://github.com/graphql-rust/juniper", branch = "master" }
|
||||
juniper_rocket_async = { git = "https://github.com/graphql-rust/juniper", branch = "master" }
|
||||
|
|
|
@ -2,8 +2,8 @@ use crate::config::Config;
|
|||
use crate::errors::ApiError;
|
||||
use chrono::{Duration, Utc};
|
||||
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||
use rocket::request::{self, FromRequest, Request};
|
||||
use rocket::response::status::Custom;
|
||||
use rocket::{http::SameSite, request::local_cache};
|
||||
use rocket::{
|
||||
http::Status,
|
||||
serde::{json::Json, Deserialize, Serialize},
|
||||
|
@ -12,6 +12,10 @@ use rocket::{
|
|||
http::{Cookie, CookieJar},
|
||||
outcome::Outcome,
|
||||
};
|
||||
use rocket::{
|
||||
outcome::IntoOutcome,
|
||||
request::{self, FromRequest, Request},
|
||||
};
|
||||
use rocket::{routes, Route, State};
|
||||
use substring::Substring;
|
||||
|
||||
|
@ -56,7 +60,7 @@ impl<'r> FromRequest<'r> for User {
|
|||
}
|
||||
|
||||
pub(crate) fn routes() -> Vec<Route> {
|
||||
routes![login]
|
||||
routes![login, refresh_token]
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
@ -75,7 +79,7 @@ fn create_token<'a>(
|
|||
username: &str,
|
||||
expiration: Duration,
|
||||
secret: &str,
|
||||
) -> Result<String, Custom<String>> {
|
||||
) -> Result<String, ApiError> {
|
||||
let expiration = Utc::now()
|
||||
.checked_add_signed(expiration)
|
||||
.expect("clock went awry")
|
||||
|
@ -90,8 +94,7 @@ fn create_token<'a>(
|
|||
&Header::default(),
|
||||
&claims,
|
||||
&EncodingKey::from_secret(secret.as_bytes()),
|
||||
)
|
||||
.map_err(|e| Custom(Status::InternalServerError, e.to_string()))?;
|
||||
)?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
@ -101,15 +104,51 @@ struct LoginResponse {
|
|||
jwt_token: String,
|
||||
}
|
||||
|
||||
/// A strongly-typed representation of the refresh token, used with a
|
||||
/// FromRequest trait to decode it from the cookie.
|
||||
struct RefreshToken(String);
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for RefreshToken {
|
||||
type Error = ();
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let token: Option<RefreshToken> = request
|
||||
.cookies()
|
||||
.get_private("refresh_token")
|
||||
.and_then(|cookie| cookie.value().parse::<String>().ok())
|
||||
.map(|t| RefreshToken(t));
|
||||
|
||||
token.or_forward(())
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::post("/login", data = "<request>")]
|
||||
async fn login(
|
||||
request: Json<LoginRequest<'_>>,
|
||||
config: &State<Config>,
|
||||
cookies: &CookieJar<'_>,
|
||||
) -> Result<Json<LoginResponse>, Custom<String>> {
|
||||
) -> Result<Json<LoginResponse>, ApiError> {
|
||||
let token = create_token(request.username, Duration::minutes(1), &config.jwt_secret)?;
|
||||
let refresh_token = create_token(request.username, Duration::weeks(1), &config.jwt_secret)?;
|
||||
|
||||
cookies.add_private(Cookie::new("refresh_token", refresh_token));
|
||||
let mut cookie = Cookie::new("refresh_token", refresh_token);
|
||||
cookie.set_same_site(SameSite::None);
|
||||
cookies.add_private(cookie);
|
||||
|
||||
Ok(Json(LoginResponse { jwt_token: token }))
|
||||
}
|
||||
|
||||
#[rocket::post("/refresh")]
|
||||
async fn refresh_token(
|
||||
config: &State<Config>,
|
||||
refresh_token: Option<RefreshToken>,
|
||||
) -> Result<Json<LoginResponse>, ApiError> {
|
||||
let refresh_token = refresh_token.ok_or(ApiError::RefreshTokenMissing)?;
|
||||
let refresh_token = decode_token(&refresh_token.0, config)?;
|
||||
|
||||
//TODO check if token is valid? maybe decode takes care of it.
|
||||
let token = create_token(&refresh_token.sub, Duration::minutes(1), &config.jwt_secret)?;
|
||||
|
||||
Ok(Json(LoginResponse { jwt_token: token }))
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
use rocket::http::ContentType;
|
||||
use rocket::response::{self, Responder, Response};
|
||||
use rocket::{http::Status, request::Request};
|
||||
use std::io::Cursor;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
@ -11,6 +15,34 @@ pub enum ApiError {
|
|||
#[error("authentication token missing from request")]
|
||||
AuthenticationRequired,
|
||||
|
||||
#[error("refresh token missing from request")]
|
||||
RefreshTokenMissing,
|
||||
|
||||
#[error("error decoding token: {0}")]
|
||||
TokenDecodingError(#[from] jsonwebtoken::errors::Error),
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> Responder<'r, 'static> for ApiError {
|
||||
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
|
||||
let status = match self {
|
||||
Self::UserDoesNotExist(_) => Status::Forbidden,
|
||||
Self::AuthenticationRequired => Status::Forbidden,
|
||||
Self::RefreshTokenMissing => Status::Forbidden,
|
||||
Self::AuthenticationDenied(_) => Status::Forbidden,
|
||||
Self::TokenDecodingError(_) => Status::InternalServerError,
|
||||
};
|
||||
|
||||
//TODO certain errors might be too sensitive; need to filter them here.
|
||||
let body = serde_json::json!({
|
||||
"message": self.to_string()
|
||||
})
|
||||
.to_string();
|
||||
|
||||
Response::build()
|
||||
.header(ContentType::JsonApi)
|
||||
.status(status)
|
||||
.sized_body(body.len(), Cursor::new(body))
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,17 +3,29 @@ use serde::{Deserialize, Serialize};
|
|||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{console, Request, RequestInit, RequestMode, Response};
|
||||
use web_sys::{console, Request, RequestCredentials, RequestInit, RequestMode, Response};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct LoginResponse {
|
||||
jwt_token: String,
|
||||
}
|
||||
|
||||
async fn make_request(request: Request) -> Result<JsValue, UiError> {
|
||||
let window = web_sys::window().unwrap();
|
||||
|
||||
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||
let resp: Response = resp_value.dyn_into().unwrap();
|
||||
|
||||
let json = JsFuture::from(resp.json()?).await?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
pub async fn fetch_jwt() -> Result<String, UiError> {
|
||||
let mut opts = RequestInit::new();
|
||||
opts.method("POST");
|
||||
opts.mode(RequestMode::Cors);
|
||||
opts.credentials(RequestCredentials::Include);
|
||||
|
||||
opts.body(Some(&JsValue::from_str(
|
||||
r#"
|
||||
{ "username": "@projectmoon:agnos.is", "password": "lolol" }
|
||||
|
@ -26,21 +38,29 @@ pub async fn fetch_jwt() -> Result<String, UiError> {
|
|||
request.headers().set("Content-Type", "application/json")?;
|
||||
request.headers().set("Accept", "application/json")?;
|
||||
|
||||
let window = web_sys::window().unwrap();
|
||||
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||
//TODO don't unwrap the response. OR... change it so we have a standard response.
|
||||
let response = make_request(request).await?;
|
||||
let response: LoginResponse = response.into_serde().unwrap();
|
||||
|
||||
// `resp_value` is a `Response` object.
|
||||
assert!(resp_value.is_instance_of::<Response>());
|
||||
let resp: Response = resp_value.dyn_into().unwrap();
|
||||
|
||||
// Convert this other `Promise` into a rust `Future`.
|
||||
let json = JsFuture::from(resp.json()?).await?;
|
||||
|
||||
console::log_1(&json);
|
||||
|
||||
// Use serde to parse the JSON into a struct.
|
||||
let login_response: LoginResponse = json.into_serde().unwrap();
|
||||
|
||||
// Send the `Branch` struct back to JS as an `Object`.
|
||||
Ok(login_response.jwt_token)
|
||||
Ok(response.jwt_token)
|
||||
}
|
||||
|
||||
pub async fn refresh_jwt() -> Result<String, UiError> {
|
||||
let mut opts = RequestInit::new();
|
||||
opts.method("POST");
|
||||
opts.mode(RequestMode::Cors);
|
||||
opts.credentials(RequestCredentials::Include);
|
||||
|
||||
let url = format!("http://localhost:10000/refresh");
|
||||
|
||||
let request = Request::new_with_str_and_init(&url, &opts)?;
|
||||
request.headers().set("Content-Type", "application/json")?;
|
||||
request.headers().set("Accept", "application/json")?;
|
||||
|
||||
//TODO don't unwrap the response. OR... change it so we have a standard response.
|
||||
let response = make_request(request).await?;
|
||||
console::log_1(&response);
|
||||
let response: LoginResponse = response.into_serde().unwrap();
|
||||
|
||||
Ok(response.jwt_token)
|
||||
}
|
||||
|
|
|
@ -50,6 +50,12 @@ async fn do_jwt_stuff(dispatch: &WebUiDispatcher) -> Result<(), UiError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn do_refresh_jwt(dispatch: &WebUiDispatcher) -> Result<(), UiError> {
|
||||
let jwt = api::auth::refresh_jwt().await?;
|
||||
dispatch.send(Action::UpdateJwt(jwt));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Component for YewduxRoomList {
|
||||
type Message = ();
|
||||
type Properties = WebUiDispatcher;
|
||||
|
@ -84,6 +90,7 @@ impl Component for YewduxRoomList {
|
|||
fn view(&self) -> Html {
|
||||
let dispatch = Arc::new(self.dispatch.clone());
|
||||
let dispatch2 = dispatch.clone();
|
||||
let dispatch3 = dispatch.clone();
|
||||
|
||||
let the_future = self.link.callback(move |_| {
|
||||
let dispatch = dispatch.clone();
|
||||
|
@ -105,10 +112,18 @@ impl Component for YewduxRoomList {
|
|||
});
|
||||
});
|
||||
|
||||
let refresh_jwt = self.link.callback(move |_| {
|
||||
let dispatch = dispatch3.clone();
|
||||
spawn_local(async move {
|
||||
do_refresh_jwt(&*dispatch).await;
|
||||
});
|
||||
});
|
||||
|
||||
html! {
|
||||
<div>
|
||||
<button onclick=the_future>{ "Add Room" }</button>
|
||||
<button onclick=jwt_update>{ "Fetch JWT" }</button>
|
||||
<button onclick=refresh_jwt>{ "Refresh JWT" }</button>
|
||||
<div> { "Current JWT: " } { self.dispatch.state().jwt_token.as_ref().unwrap_or(&"[not set]".to_string()) }</div>
|
||||
<ul>
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue