mirror of
https://github.com/nullishamy/ferri.git
synced 2025-04-29 20:29:23 +00:00
feat: auth basics
This commit is contained in:
parent
005c13e1d4
commit
9c7c2858cc
11 changed files with 215 additions and 29 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2156,6 +2156,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"main",
|
"main",
|
||||||
|
"rand 0.8.5",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rocket",
|
"rocket",
|
||||||
"rocket_db_pools",
|
"rocket_db_pools",
|
||||||
|
|
|
@ -8,4 +8,5 @@ serde = "1.0.219"
|
||||||
rocket = { version = "0.5.1", features = ["json"] }
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite", "macros" ], default-features = false }
|
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite", "macros" ], default-features = false }
|
||||||
uuid = { version = "1.16.0", features = ["v4"] }
|
uuid = { version = "1.16.0", features = ["v4"] }
|
||||||
chrono = "0.4.40"
|
chrono = "0.4.40"
|
||||||
|
rand = "0.8"
|
|
@ -13,5 +13,5 @@ uuid = { workspace = true }
|
||||||
|
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
rsa = { version = "0.9.8", features = ["sha2"] }
|
rsa = { version = "0.9.8", features = ["sha2"] }
|
||||||
rand = "0.8"
|
rand = { workspace = true }
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
|
|
|
@ -1,2 +1,11 @@
|
||||||
pub mod ap;
|
pub mod ap;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
use rand::{Rng, distributions::Alphanumeric};
|
||||||
|
|
||||||
|
pub fn gen_token(len: usize) -> String {
|
||||||
|
rand::thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(len)
|
||||||
|
.map(char::from)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
|
@ -11,5 +11,6 @@ reqwest = { workspace = true }
|
||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
|
@ -1,14 +1,44 @@
|
||||||
use rocket::{form::Form, post, serde::json::Json};
|
use rocket::{form::Form, post, serde::json::Json};
|
||||||
|
|
||||||
|
use crate::Db;
|
||||||
use crate::types::oauth::{App, CredentialApplication};
|
use crate::types::oauth::{App, CredentialApplication};
|
||||||
|
use rocket_db_pools::Connection;
|
||||||
|
|
||||||
#[post("/apps", data = "<app>")]
|
#[post("/apps", data = "<app>")]
|
||||||
pub async fn new_app(app: Form<App>) -> Json<CredentialApplication> {
|
pub async fn new_app(app: Form<App>, mut db: Connection<Db>) -> Json<CredentialApplication> {
|
||||||
|
let secret = main::gen_token(15);
|
||||||
|
|
||||||
|
// Abort when we encounter a duplicate
|
||||||
|
let is_app_present = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO app (client_id, client_secret, scopes)
|
||||||
|
VALUES (?1, ?2, ?3)
|
||||||
|
"#,
|
||||||
|
app.client_name,
|
||||||
|
app.scopes,
|
||||||
|
secret
|
||||||
|
)
|
||||||
|
.execute(&mut **db)
|
||||||
|
.await
|
||||||
|
.is_err();
|
||||||
|
|
||||||
|
let mut app: App = app.clone();
|
||||||
|
|
||||||
|
if is_app_present {
|
||||||
|
let existing_app = sqlx::query!("SELECT * FROM app WHERE client_id = ?1", app.client_name)
|
||||||
|
.fetch_one(&mut **db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
app.client_name = existing_app.client_id;
|
||||||
|
app.scopes = existing_app.scopes;
|
||||||
|
}
|
||||||
|
|
||||||
Json(CredentialApplication {
|
Json(CredentialApplication {
|
||||||
name: app.client_name.clone(),
|
name: app.client_name.clone(),
|
||||||
scopes: app.scopes.clone(),
|
scopes: app.scopes.clone(),
|
||||||
redirect_uris: app.redirect_uris.clone(),
|
redirect_uris: app.redirect_uris.clone(),
|
||||||
client_id: format!("id-for-{}", app.client_name),
|
client_id: app.client_name.clone(),
|
||||||
client_secret: format!("secret-for-{}", app.client_name),
|
client_secret: secret,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{Db, endpoints::api::user::CredentialAcount};
|
use crate::{AuthenticatedUser, Db, endpoints::api::user::CredentialAcount};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
get,
|
get,
|
||||||
serde::{Deserialize, Serialize, json::Json},
|
serde::{Deserialize, Serialize, json::Json},
|
||||||
|
@ -32,7 +32,12 @@ pub struct TimelineStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/timelines/home?<limit>")]
|
#[get("/timelines/home?<limit>")]
|
||||||
pub async fn home(mut db: Connection<Db>, limit: i64) -> Json<Vec<TimelineStatus>> {
|
pub async fn home(
|
||||||
|
mut db: Connection<Db>,
|
||||||
|
limit: i64,
|
||||||
|
user: AuthenticatedUser,
|
||||||
|
) -> Json<Vec<TimelineStatus>> {
|
||||||
|
dbg!(user);
|
||||||
let posts = sqlx::query!(
|
let posts = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
SELECT p.id as "post_id", u.id as "user_id", p.content, p.uri as "post_uri",
|
SELECT p.id as "post_id", u.id as "user_id", p.content, p.uri as "post_uri",
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
|
use crate::Db;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
|
FromForm,
|
||||||
|
form::Form,
|
||||||
get, post,
|
get, post,
|
||||||
response::Redirect,
|
response::Redirect,
|
||||||
serde::{Deserialize, Serialize, json::Json},
|
serde::{Deserialize, Serialize, json::Json},
|
||||||
};
|
};
|
||||||
|
use rocket_db_pools::Connection;
|
||||||
|
|
||||||
#[get("/oauth/authorize?<client_id>&<scope>&<redirect_uri>&<response_type>")]
|
#[get("/oauth/authorize?<client_id>&<scope>&<redirect_uri>&<response_type>")]
|
||||||
pub async fn authorize(
|
pub async fn authorize(
|
||||||
|
@ -10,11 +14,45 @@ pub async fn authorize(
|
||||||
scope: &str,
|
scope: &str,
|
||||||
redirect_uri: &str,
|
redirect_uri: &str,
|
||||||
response_type: &str,
|
response_type: &str,
|
||||||
|
mut db: Connection<Db>,
|
||||||
) -> Redirect {
|
) -> Redirect {
|
||||||
Redirect::temporary(format!(
|
// For now, we will always authorize the request and assign it to an admin user
|
||||||
"{}?code=code-for-{}&state=state-for-{}",
|
let user_id = "9b9d497b-2731-435f-a929-e609ca69dac9";
|
||||||
redirect_uri, client_id, client_id
|
let code = main::gen_token(15);
|
||||||
))
|
|
||||||
|
// This will act as a token for the user, but we will in future say that it expires very shortly
|
||||||
|
// and can only be used for obtaining an access token etc
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO auth (token, user_id)
|
||||||
|
VALUES (?1, ?2)
|
||||||
|
"#,
|
||||||
|
code,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.execute(&mut **db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let id_token = main::gen_token(10);
|
||||||
|
|
||||||
|
// Add an oauth entry for the `code` which /oauth/token will rewrite
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO oauth (id_token, client_id, expires_in, scope, access_token)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||||
|
"#,
|
||||||
|
id_token,
|
||||||
|
client_id,
|
||||||
|
3600,
|
||||||
|
scope,
|
||||||
|
code
|
||||||
|
)
|
||||||
|
.execute(&mut **db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Redirect::temporary(format!("{}?code={}", redirect_uri, code))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
@ -27,13 +65,66 @@ pub struct Token {
|
||||||
pub id_token: String,
|
pub id_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/oauth/token")]
|
#[derive(Deserialize, Debug, FromForm)]
|
||||||
pub async fn new_token() -> Json<Token> {
|
#[serde(crate = "rocket::serde")]
|
||||||
|
struct NewTokenRequest {
|
||||||
|
client_id: String,
|
||||||
|
redirect_uri: String,
|
||||||
|
grant_type: String,
|
||||||
|
code: String,
|
||||||
|
client_secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/oauth/token", data = "<req>")]
|
||||||
|
pub async fn new_token(req: Form<NewTokenRequest>, mut db: Connection<Db>) -> Json<Token> {
|
||||||
|
let oauth = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT o.*, a.*
|
||||||
|
FROM oauth o
|
||||||
|
INNER JOIN auth a ON a.token = ?2
|
||||||
|
WHERE o.access_token = ?1
|
||||||
|
"#,
|
||||||
|
req.code,
|
||||||
|
req.code
|
||||||
|
)
|
||||||
|
.fetch_one(&mut **db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let access_token = main::gen_token(15);
|
||||||
|
|
||||||
|
// Important: setup 'auth' first
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO auth (token, user_id)
|
||||||
|
VALUES (?1, ?2)
|
||||||
|
"#,
|
||||||
|
access_token,
|
||||||
|
oauth.user_id
|
||||||
|
)
|
||||||
|
.execute(&mut **db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE oauth SET access_token = ?1 WHERE access_token = ?2",
|
||||||
|
access_token,
|
||||||
|
req.code
|
||||||
|
)
|
||||||
|
.execute(&mut **db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
sqlx::query!("DELETE FROM auth WHERE token = ?1", req.code)
|
||||||
|
.execute(&mut **db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
Json(Token {
|
Json(Token {
|
||||||
access_token: "9b9d497b-2731-435f-a929-e609ca69dac9".to_string(),
|
access_token: access_token.to_string(),
|
||||||
token_type: "Bearer".to_string(),
|
token_type: "Bearer".to_string(),
|
||||||
expires_in: 3600,
|
expires_in: oauth.expires_in,
|
||||||
scope: "read write follow push".to_string(),
|
scope: oauth.scope.to_string(),
|
||||||
id_token: "id-token".to_string(),
|
id_token: oauth.id_token,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,17 @@ use endpoints::{
|
||||||
api::{self, timeline},
|
api::{self, timeline},
|
||||||
custom, inbox, oauth, user, well_known,
|
custom, inbox, oauth, user, well_known,
|
||||||
};
|
};
|
||||||
|
|
||||||
use main::ap::http;
|
use main::ap::http;
|
||||||
use main::config::Config;
|
use main::config::Config;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
Build, Request, Rocket, build, get,
|
Build, Request, Rocket, build, get,
|
||||||
http::ContentType,
|
http::{ContentType, Status},
|
||||||
|
outcome::IntoOutcome,
|
||||||
request::{FromRequest, Outcome},
|
request::{FromRequest, Outcome},
|
||||||
routes,
|
routes,
|
||||||
};
|
};
|
||||||
use rocket_db_pools::{Database, sqlx};
|
use rocket_db_pools::{Connection, Database, sqlx};
|
||||||
|
|
||||||
mod cors;
|
mod cors;
|
||||||
mod endpoints;
|
mod endpoints;
|
||||||
|
@ -34,6 +36,7 @@ async fn activity_endpoint(activity: String) {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct AuthenticatedUser {
|
struct AuthenticatedUser {
|
||||||
username: String,
|
username: String,
|
||||||
|
token: String,
|
||||||
actor_id: String,
|
actor_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,15 +51,34 @@ enum LoginError {
|
||||||
impl<'a> FromRequest<'a> for AuthenticatedUser {
|
impl<'a> FromRequest<'a> for AuthenticatedUser {
|
||||||
type Error = LoginError;
|
type Error = LoginError;
|
||||||
async fn from_request(request: &'a Request<'_>) -> Outcome<AuthenticatedUser, LoginError> {
|
async fn from_request(request: &'a Request<'_>) -> Outcome<AuthenticatedUser, LoginError> {
|
||||||
let token = request.headers().get_one("Authorization").unwrap();
|
let token = request.headers().get_one("Authorization");
|
||||||
let token = token
|
|
||||||
.strip_prefix("Bearer")
|
if let Some(token) = token {
|
||||||
.map(|s| s.trim())
|
let token = token
|
||||||
.unwrap_or(token);
|
.strip_prefix("Bearer")
|
||||||
Outcome::Success(AuthenticatedUser {
|
.map(|s| s.trim())
|
||||||
username: token.to_string(),
|
.unwrap_or(token);
|
||||||
actor_id: format!("https://ferri.amy.mov/users/{}", token),
|
|
||||||
})
|
let mut conn = request.guard::<Connection<Db>>().await.unwrap();
|
||||||
|
let auth = sqlx::query!(r#"
|
||||||
|
SELECT *
|
||||||
|
FROM auth a
|
||||||
|
INNER JOIN user u ON a.user_id = u.id
|
||||||
|
WHERE token = ?1
|
||||||
|
"#, token)
|
||||||
|
.fetch_one(&mut **conn)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(auth) = auth {
|
||||||
|
return Outcome::Success(AuthenticatedUser {
|
||||||
|
token: auth.token,
|
||||||
|
username: auth.display_name,
|
||||||
|
actor_id: auth.actor_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Outcome::Forward(Status::Unauthorized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use rocket::{
|
||||||
serde::{Deserialize, Serialize},
|
serde::{Deserialize, Serialize},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, FromForm)]
|
#[derive(Serialize, Deserialize, Debug, FromForm, Clone)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub client_name: String,
|
pub client_name: String,
|
||||||
|
|
26
migrations/20250423182916_add_auth.sql
Normal file
26
migrations/20250423182916_add_auth.sql
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS auth
|
||||||
|
(
|
||||||
|
token TEXT PRIMARY KEY NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY(user_id) REFERENCES user(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS app
|
||||||
|
(
|
||||||
|
client_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
client_secret TEXT NOT NULL,
|
||||||
|
scopes TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS oauth
|
||||||
|
(
|
||||||
|
id_token TEXT PRIMARY KEY NOT NULL,
|
||||||
|
client_id TEXT NOT NULL,
|
||||||
|
expires_in INTEGER NOT NULL,
|
||||||
|
scope TEXT NOT NULL,
|
||||||
|
access_token TEXT NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY(access_token) REFERENCES auth(token),
|
||||||
|
FOREIGN KEY(client_id) REFERENCES app(client_id)
|
||||||
|
);
|
Loading…
Add table
Reference in a new issue