Ver código fonte

Merge remote-tracking branch 'refs/remotes/origin/main'

Kestrel 1 ano atrás
pai
commit
01a4263821
18 arquivos alterados com 1187 adições e 306 exclusões
  1. 1 0
      .vimrc
  2. 255 219
      Cargo.lock
  3. 6 0
      Cargo.toml
  4. 35 17
      src/cert.rs
  5. 61 29
      src/cli.rs
  6. 23 0
      src/config.rs
  7. 1 0
      src/login.rs
  8. 13 5
      src/main.rs
  9. 88 33
      src/schema.rs
  10. 60 3
      src/server.rs
  11. 157 0
      src/server/oidc.rs
  12. 44 0
      src/server/oidc/api.rs
  13. 241 0
      src/server/session.rs
  14. 50 0
      src/user.rs
  15. 58 0
      src/user_management.rs
  16. 0 0
      static/logout.html
  17. 62 0
      static/style.css
  18. 32 0
      tmpl/id_v1_login.tmpl

+ 1 - 0
.vimrc

@@ -0,0 +1 @@
+set wildignore+=target

Diferenças do arquivo suprimidas por serem muito extensas
+ 255 - 219
Cargo.lock


+ 6 - 0
Cargo.toml

@@ -16,12 +16,18 @@ serde_bytes = { version = "0.11.6" }
 serde_json = "1.0"
 sha2 = { version = "0.10.2" }
 base64 = { version = "0.13.0" }
+log = "0.4"
+stderrlog = "0.5"
+handlebars = { version = "4.3", features = ["dir_source"] }
+lazy_static = "1.4.0"
 
 # Data storage dependencies
 microrm = { version = "0.3.9" }
 
 # Public API dependencies
 tide = { version = "0.16.0" }
+anyhow = { version = "1.0" }
 
 # CLI dependencies
 clap = { version = "3.1.15", features = ["derive"] }
+rpassword = "6.0"

+ 35 - 17
src/cert.rs

@@ -1,27 +1,37 @@
+use crate::schema;
 use ring::signature::Ed25519KeyPair;
 use sha2::Digest;
-use crate::schema;
+use std::collections::HashMap;
 use microrm::prelude::*;
 
-pub struct CertStore { }
+pub struct CertStore<'a> {
+    db: &'a microrm::DB,
+    qi: microrm::QueryInterface<'a>,
+    keys: HashMap<String, Ed25519KeyPair>,
+}
 
-impl CertStore {
-    pub fn new() -> Self {
-        Self { }
+impl<'a> CertStore<'a> {
+    pub fn new(db: &'a microrm::DB) -> Self {
+        Self {
+            db,
+            qi: db.query_interface(),
+            keys: HashMap::new(),
+        }
     }
 
-    fn realm_id(&self, qi: &microrm::QueryInterface, realm_name: &str) -> Option<schema::RealmID> {
-        qi.get().by(schema::Realm::Shortname, realm_name).one().ok()?.map(|v| v.id())
+    fn realm_id(&self, realm_name: &str) -> Option<schema::RealmID> {
+        self.qi.get().by(schema::Realm::Shortname, realm_name).one().expect("couldn't query db")
+            .map(|x| x.id())
     }
 
-    pub fn generate_in(&self, qi: &microrm::QueryInterface, realm_name: &str) -> Result<String, &'static str> {
+    pub fn generate_in(&self, realm_name: &str) -> Result<String, &'static str> {
         let mut rng = ring::rand::SystemRandom::new();
         let sign_generated = Ed25519KeyPair::generate_pkcs8(&mut rng);
 
-        let realm_id = self.realm_id(qi, realm_name).ok_or("Failed to find realm")?;
+        let realm_id = self.realm_id(realm_name).ok_or("Failed to find realm")?;
 
         if let Err(_) = sign_generated {
-            return Err("Failed to generate key")
+            return Err("Failed to generate key");
         }
         let sign_generated = sign_generated.unwrap();
 
@@ -32,7 +42,13 @@ impl CertStore {
         let mut key_id = base64::encode(key_hasher.finalize());
         key_id.truncate(16);
 
-        qi.add(&schema::Key { realm: realm_id, key_id: key_id.clone(), keydata }).expect("Couldn't add key");
+        self.qi
+            .add(&schema::Key {
+                realm: realm_id,
+                key_id: key_id.clone(),
+                keydata,
+            })
+            .expect("Couldn't add key");
 
         Ok(key_id)
     }
@@ -40,25 +56,27 @@ impl CertStore {
 
 pub fn inspect(db: &microrm::DB, realm_name: &str) {
     let qi = db.query_interface();
-
-    let realm = qi.get().by(schema::Realm::Shortname, realm_name).one().expect("couldn't load realm");
+    let cs = CertStore::new(db);
+    println!("Certstore loaded.");
+    let realm = qi.get().by(schema::Realm::Shortname, realm_name).one().expect("couldn't query db");
     if realm.is_none() {
         println!("No such realm {}", realm_name);
-        return
+        return;
     }
     let realm = realm.unwrap();
 
     println!("Retrieving keys for {} realm...", realm_name);
-    let keys = qi.get().by(schema::Key::Realm, &realm.id()).all().expect("couldn't load keys");
+    let keys = qi.get().by(schema::Key::Realm, &realm.id()).all()
+        .expect("Can get keys");
     for key in keys {
         println!("- [{:20}]", key.key_id);
     }
 }
 
 pub fn generate(db: &microrm::DB, realm_name: &str) {
-    let cs = CertStore::new();
     let qi = db.query_interface();
-    if let Err(e) = cs.generate_in(&qi, realm_name) {
+    let cs = CertStore::new(db);
+    if let Err(e) = cs.generate_in(realm_name) {
         println!("Failed to generate key: {}", e);
     }
 }

+ 61 - 29
src/cli.rs

@@ -1,5 +1,5 @@
 use clap::{Parser,Subcommand};
-use crate::{schema,cert};
+use crate::{schema,cert,user_management,server};
 
 #[derive(Debug, Parser)]
 #[clap(author, version, about, long_about = None)]
@@ -14,14 +14,14 @@ struct RootArgs {
     command: Command,
 }
 
-#[derive(Debug,Subcommand)]
+#[derive(Debug, Subcommand)]
 enum Command {
     Init,
     Cert(CertArgs),
     Group(GroupArgs),
     Server(ServerArgs),
     Token(TokenArgs),
-    User(UserArgs),
+    User(UserArgs)
 }
 
 impl RootArgs {
@@ -30,44 +30,58 @@ impl RootArgs {
             return self.init().await;
         }
 
-        let storage = microrm::DB::new(schema::schema(), &self.db, microrm::CreateMode::MustExist);
+        let storage = microrm::DB::new(
+            schema::schema(),
+            &self.db,
+            microrm::CreateMode::MustExist,
+        );
 
         if let Err(e) = storage {
             println!("Error occured while loading database: {}", e);
-            return
+            return;
         }
         let storage = storage.unwrap();
 
         match &self.command {
             Command::Init => unreachable!(),
-            Command::Cert(v) => v.run(&self, &storage).await,
-            Command::Group(v) => v.run(&self, &storage).await,
-            Command::Server(v) => v.run(&self, &storage).await,
-            Command::Token(v) => v.run(&self, &storage).await,
-            Command::User(v) => v.run(&self, &storage).await,
+            Command::Cert(v) => v.run(&self, storage).await,
+            Command::Group(v) => v.run(&self, storage).await,
+            Command::Server(v) => v.run(&self, storage).await,
+            Command::Token(v) => v.run(&self, storage).await,
+            Command::User(v) => v.run(&self, storage).await,
         }
     }
 
     async fn init(&self) {
         // first check to see if the database is already vaguely set up
-        let maybedb = microrm::DB::new(schema::schema(), &self.db, microrm::CreateMode::MustExist);
+        let maybedb = microrm::DB::new(
+            schema::schema(),
+            &self.db,
+            microrm::CreateMode::MustExist,
+        );
 
         if maybedb.is_ok() {
             println!("Database already initialized, not overwriting!");
-            return
+            return;
         }
 
         println!("Initializing!");
 
-        let db = microrm::DB::new(schema::schema(), &self.db, microrm::CreateMode::AllowNewDatabase).expect("Unable to initialize database!");
+        let db = microrm::DB::new(
+            schema::schema(),
+            &self.db,
+            microrm::CreateMode::AllowNewDatabase,
+        )
+        .expect("Unable to initialize database!");
 
         // create primary realm
-        let qi = db.query_interface();
-        qi.add(&schema::Realm { shortname: "primary".to_string() }).expect("couldn't add primary realm");
+        db.query_interface().add(&schema::Realm {
+            shortname: "primary".to_string(),
+        }).expect("couldn't add realm");
     }
 }
 
-#[derive(Debug,Subcommand)]
+#[derive(Debug, Subcommand)]
 enum CertCommand {
     Inspect,
     Generate,
@@ -76,17 +90,17 @@ enum CertCommand {
 #[derive(Debug, Parser)]
 struct CertArgs {
     #[clap(subcommand)]
-    command: CertCommand
+    command: CertCommand,
 }
 
 impl CertArgs {
-    async fn run(&self, root: &RootArgs, si: &microrm::DB) {
+    async fn run(&self, root: &RootArgs, db: microrm::DB) {
         match &self.command {
             CertCommand::Inspect => {
-                cert::inspect(si, &root.realm);
+                cert::inspect(&db, &root.realm);
             }
             CertCommand::Generate => {
-                cert::generate(si, &root.realm);
+                cert::generate(&db, &root.realm);
             }
         }
     }
@@ -103,7 +117,7 @@ struct GroupArgs {
 }
 
 impl GroupArgs {
-    async fn run(&self, root: &RootArgs, si: &microrm::DB) {
+    async fn run(&self, root: &RootArgs, db: microrm::DB) {
     }
 }
 
@@ -114,8 +128,8 @@ struct ServerArgs {
 }
 
 impl ServerArgs {
-    async fn run(&self, root: &RootArgs, db: &microrm::DB) {
-        crate::server::launch(db, self.port.unwrap_or(2000)).await
+    async fn run(&self, root: &RootArgs, db: microrm::DB) {
+        server::run_server(db, self.port.unwrap_or(2114)).await
     }
 }
 
@@ -133,26 +147,44 @@ struct TokenArgs {
 }
 
 impl TokenArgs {
-    async fn run(&self, root: &RootArgs, _db: &microrm::DB) {
+    async fn run(&self, root: &RootArgs, _db: microrm::DB) {
         
     }
 }
 
+#[derive(Debug, Parser)]
+struct CreateUserArgs {
+    username: String
+}
+
+#[derive(Debug, Parser)]
+struct AuthUserArgs {
+    username: String,
+
+    #[clap(short='p', long, parse(from_occurrences))]
+    change_password: usize,
+}
+
 #[derive(Debug, Subcommand)]
 enum UserCommand {
-    Create { username: String },
-    List { filter: Option<String> },
+    List,
+    Create(CreateUserArgs),
+    Auth(AuthUserArgs),
 }
 
 #[derive(Debug, Parser)]
 struct UserArgs {
     #[clap(subcommand)]
-    command: UserCommand,
+    command: UserCommand
 }
 
 impl UserArgs {
-    async fn run(&self, root: &RootArgs, _db: &microrm::DB) {
-        
+    async fn run(&self, root: &RootArgs, db: microrm::DB) {
+        match &self.command {
+            UserCommand::List => { user_management::list(&root.realm, db) }
+            UserCommand::Create(args) => { user_management::create(&root.realm, db, args.username.as_str()) }
+            UserCommand::Auth(args) => { user_management::change_auth(&root.realm, db, args.username.as_str(), args.change_password > 0) }
+        }
     }
 }
 

+ 23 - 0
src/config.rs

@@ -0,0 +1,23 @@
+#![allow(dead_code)]
+
+use serde::Deserialize;
+
+#[derive(Deserialize)]
+pub enum Flow {
+    Challenge(crate::schema::AuthChallengeType),
+    OneOf(Vec<Flow>),
+    AllOf(Vec<Flow>)
+}
+
+#[derive(Deserialize)]
+pub struct AuthConfig {
+    pbkdf2_rounds: usize,
+
+    login_flow: Flow,
+}
+
+#[derive(Deserialize)]
+pub struct GlobalConfig {
+    auth: AuthConfig,
+
+}

+ 1 - 0
src/login.rs

@@ -0,0 +1 @@
+

+ 13 - 5
src/main.rs

@@ -1,11 +1,19 @@
-pub use microrm::DB;
-
-mod schema;
-mod cli;
 mod cert;
-mod user;
+mod cli;
+mod login;
+mod schema;
 mod server;
+mod user;
+mod user_management;
+mod config;
+
 
 fn main() {
+    stderrlog::new()
+        .verbosity(5)
+        .module(module_path!())
+        .init()
+        .unwrap();
+
     cli::invoked();
 }

+ 88 - 33
src/schema.rs

@@ -1,39 +1,73 @@
-use serde::{Serialize,Deserialize};
-pub use microrm::{Entity, Schema};
+pub use microrm::{Schema, Entity, Modelable};
+use serde::{Deserialize, Serialize};
 
-/// Top-level partitioning object
-#[derive(Entity,Serialize,Deserialize)]
+#[derive(Entity, Serialize, Deserialize)]
+pub struct Session {
+    pub key: String,
+    // TODO: add expiry here
+}
+
+microrm::make_index!(!SessionKeyIndex, Session::Key);
+
+#[derive(Entity, Serialize, Deserialize)]
+pub struct SessionAuthentication {
+    #[microrm_foreign]
+    pub session: SessionID,
+    #[microrm_foreign]
+    pub realm: RealmID,
+    #[microrm_foreign]
+    pub user: UserID,
+
+    pub challenges_left: Vec<AuthChallengeType>,
+}
+
+// **** oauth types ****
+#[derive(Entity, Serialize, Deserialize)]
 pub struct Realm {
-    pub shortname: String
+    pub shortname: String,
 }
 
-/// Cryptographic signing key
-#[derive(Entity,Serialize,Deserialize)]
+#[derive(Entity, Serialize, Deserialize)]
 pub struct Key {
+    #[microrm_foreign]
     pub realm: RealmID,
     pub key_id: String,
     #[serde(with = "serde_bytes")]
-    pub keydata: Vec<u8>
+    pub keydata: Vec<u8>,
 }
 
 /// End-user representation object
-#[derive(Entity,Serialize,Deserialize)]
+#[derive(Entity, Serialize, Deserialize)]
 pub struct User {
+    #[microrm_foreign]
     pub realm: RealmID,
     pub username: String,
 }
 
-/// End-user authentication
-#[derive(Entity,Serialize,Deserialize)]
-pub struct Password {
+#[derive(Clone, Copy, Debug, PartialEq, Modelable, Serialize, Deserialize)]
+pub enum AuthChallengeType {
+    Username,
+    Password,
+    TOTP,
+    Grid,
+    WebAuthn,
+}
+
+#[derive(Entity, Serialize, Deserialize)]
+pub struct AuthChallenge {
+    #[microrm_foreign]
     pub user: UserID,
-    pub salt: String,
-    pub hash: String
+    pub challenge_type: AuthChallengeType,
+    #[serde(with = "serde_bytes")]
+    pub public: Vec<u8>,
+    #[serde(with = "serde_bytes")]
+    pub secret: Vec<u8>,
 }
 
 /// User semantic grouping
-#[derive(Entity,Serialize,Deserialize)]
+#[derive(Entity, Serialize, Deserialize)]
 pub struct Group {
+    #[microrm_foreign]
     pub realm: RealmID,
     pub shortname: String,
 }
@@ -46,59 +80,80 @@ pub struct GroupMembership {
 }
 
 /// OAuth2 client representation
-#[derive(Entity,Serialize,Deserialize)]
+#[derive(Entity, Serialize, Deserialize)]
 pub struct Client {
+    #[microrm_foreign]
     pub realm: RealmID,
     pub shortname: String,
     pub secret: String,
 }
 
+microrm::make_index!(
+    !ClientNameIndex,
+    Client::Realm,
+    Client::Shortname
+);
+
+#[derive(Entity, Serialize, Deserialize)]
+pub struct ClientRedirect {
+    #[microrm_foreign]
+    pub client: ClientID,
+    pub redirect: String,
+}
+
 /// Requested group of permissions
-#[derive(Entity,Serialize,Deserialize)]
+#[derive(Entity, Serialize, Deserialize)]
 pub struct Scope {
+    #[microrm_foreign]
     pub realm: RealmID,
     pub shortname: String,
 }
 
 /// Specific atomic permission
-#[derive(Entity,Serialize,Deserialize)]
+#[derive(Entity, Serialize, Deserialize)]
 pub struct Role {
+    #[microrm_foreign]
     pub realm: RealmID,
     pub shortname: String,
 }
 
 /// Role membership in scope
 #[derive(Entity,Serialize,Deserialize)]
-pub struct ScopeRoles {
+pub struct ScopeRole {
     pub scope: ScopeID,
     pub role: RoleID,
 }
 
 /// Assigned permissions in group
 #[derive(Entity,Serialize,Deserialize)]
-pub struct GroupRoles {
+pub struct GroupRole {
     pub scope: ScopeID,
     pub role: RoleID,
 }
 
-
 #[derive(Entity,Serialize,Deserialize)]
-pub struct RevokedTokens {
+pub struct RevokedToken {
     pub user: UserID,
     pub nonce: String,
 }
 
 pub fn schema() -> Schema {
     Schema::new()
-        .add::<Realm>()
-        .add::<Key>()
-        .add::<User>()
-        .add::<Password>()
-        .add::<Group>()
-        .add::<GroupMembership>()
-        .add::<Client>()
-        .add::<Scope>()
-        .add::<Role>()
-        .add::<ScopeRoles>()
-        .add::<GroupRoles>()
+        .entity::<Session>()
+        .index::<SessionKeyIndex>()
+        .entity::<SessionAuthentication>()
+        // oauth types
+        .entity::<Realm>()
+        .entity::<Key>()
+        .entity::<User>()
+        .entity::<AuthChallenge>()
+        .entity::<Group>()
+        .entity::<Client>()
+        .index::<ClientNameIndex>()
+        .entity::<ClientRedirect>()
+        .entity::<Scope>()
+        .entity::<Role>()
+        .entity::<ScopeRole>()
+        .entity::<GroupRole>()
+        .entity::<RevokedToken>()
 }

+ 60 - 3
src/server.rs

@@ -1,5 +1,62 @@
-pub async fn launch(db: &microrm::DB, port: u16) {
-    let mut srv = tide::new();
+use crate::schema;
+use microrm::prelude::*;
 
-    srv.listen(("127.0.0.1", port)).await.expect("couldn't bind to port");
+mod session;
+mod oidc;
+
+pub struct ServerCoreState {
+    pool: microrm::DBPool<'static>,
+    templates: std::sync::RwLock<handlebars::Handlebars<'static>>,
+}
+
+#[derive(Clone)]
+struct ServerState {
+    core: &'static ServerCoreState,
+}
+
+fn is_auth_valid<T>(core: &'static ServerCoreState, of: &tide::Request<T>) -> Option<bool> {
+    let cookie = of.cookie("vogt_session")?;
+    let session_id = cookie.value();
+
+    Some(
+        core.pool
+            .query_interface()
+            .get().by(schema::Session::Key, &session_id).one().expect("couldn't query db")
+            .is_some(),
+    )
+}
+
+pub async fn run_server(db: microrm::DB, port: u16) {
+    let db_box = Box::new(db);
+    let db: &'static mut microrm::DB = Box::leak(db_box);
+    let pool = microrm::DBPool::new(db);
+
+    let core_state = Box::leak(Box::new(ServerCoreState { pool, templates: std::sync::RwLock::new(handlebars::Handlebars::new()) }));
+    let state = ServerState { core: core_state };
+
+    // XXX: for development only
+    core_state.templates.write().unwrap().set_dev_mode(true);
+
+    core_state.templates.write().unwrap().register_templates_directory("tmpl", "tmpl").expect("Couldn't open templates directory?");
+
+    let mut app = tide::with_state(state);
+
+    app.with(tide::log::LogMiddleware::new());
+
+    /*app.at("/:realm/login").get(login); // serve_file("srv/login.html").expect("Can't serve login.html");
+    app.at("/:realm/do_login").post(do_login);
+    app.at("/:realm/logout")
+        .serve_file("srv/logout.html")
+        .expect("Can't serve logout.html");*/
+
+    app.at("/static")
+        .serve_dir("static/")
+        .expect("Can't serve static files");
+
+    app.at("/:realm/v1/session/")
+        .nest(session::session_v1_server(core_state));
+    app.at("/:realm/v1/oidc")
+        .nest(oidc::oidc_v1_server(core_state));
+
+    app.listen(("127.0.0.1", port)).await.expect("Can listen");
 }

+ 157 - 0
src/server/oidc.rs

@@ -0,0 +1,157 @@
+use crate::schema;
+use serde::{Deserialize, Serialize};
+use microrm::prelude::*;
+
+#[derive(Clone)]
+pub struct ServerState {
+    core: &'static super::ServerCoreState,
+}
+
+type Request = tide::Request<ServerState>;
+
+#[derive(serde::Serialize)]
+pub enum OIDCErrorType {
+    InvalidRequest,
+    UnauthorizedClient,
+    AccessDenied,
+    UnsupportedResponseType,
+    InvalidScope,
+    ServerError,
+    TemporarilyUnavailable,
+}
+
+pub struct OIDCError<'a>(OIDCErrorType, String, Option<&'a str>);
+
+impl<'a> OIDCError<'a> {
+    fn to_response(self) -> tide::Response {
+        #[derive(Serialize)]
+        struct ErrorOut<'a> {
+            error: OIDCErrorType,
+            error_description: String,
+            state: Option<&'a str>,
+        }
+
+        let eo = ErrorOut {
+            error: self.0,
+            error_description: self.1,
+            state: self.2,
+        };
+
+        tide::Response::builder(400)
+            .body(serde_json::to_vec(&eo).unwrap())
+            .build()
+    }
+}
+
+#[derive(Deserialize)]
+struct AuthorizeQueryParams {
+    response_type: String,
+    client_id: String,
+    redirect_uri: String,
+    scope: Option<String>,
+}
+
+fn do_code_authorize(
+    request: Request,
+    qp: AuthorizeQueryParams,
+    state: Option<&str>,
+    client: microrm::WithID<schema::Client>,
+) -> Result<tide::Response, OIDCError> {
+    todo!()
+}
+
+fn do_token_authorize(
+    request: Request,
+    qp: AuthorizeQueryParams,
+    state: Option<&str>,
+    client: microrm::WithID<schema::Client>,
+) -> Result<tide::Response, OIDCError> {
+    todo!()
+}
+
+fn do_authorize(request: Request, state: Option<&str>) -> Result<tide::Response, OIDCError> {
+    let qp: AuthorizeQueryParams = request
+        .query()
+        .map_err(|x| OIDCError(OIDCErrorType::InvalidRequest, x.to_string(), state))?;
+
+    // verify the realm and client_id and redirect_uri
+    let qi = request.state().core.pool.query_interface();
+    let realm = qi.get().by(
+        schema::Realm::Shortname,
+        &request.param("realm").unwrap(),
+    ).one().expect("couldn't query db");
+    if realm.is_none() {
+        return Err(OIDCError(
+            OIDCErrorType::InvalidRequest,
+            "No such realm!".to_string(),
+            state,
+        ));
+    }
+    let realm = realm.unwrap();
+
+    let client = qi.get().by(schema::Client::Realm, &realm.id()).by(schema::Client::Shortname, &qp.client_id).one().expect("couldn't query db");
+    if client.is_none() {
+        return Err(OIDCError(
+            OIDCErrorType::UnauthorizedClient,
+            "Client does not exist".to_string(),
+            state,
+        ));
+    }
+
+    if qp.response_type == "code" {
+        do_code_authorize(request, qp, state, client.unwrap())
+    } else if qp.response_type == "token" {
+        do_token_authorize(request, qp, state, client.unwrap())
+    } else {
+        return Err(OIDCError(
+            OIDCErrorType::UnsupportedResponseType,
+            "Only code and token are understood.".to_string(),
+            state,
+        ));
+    }
+}
+
+async fn authorize(request: Request) -> tide::Result<tide::Response> {
+    #[derive(Deserialize)]
+    struct State {
+        state: Option<String>,
+    }
+    let state: Option<String> = request.query::<State>().ok().map(|x| x.state).flatten();
+
+    let result = do_authorize(request, state.as_ref().map(|x| x.as_str()));
+
+    if let Err(e) = result {
+        todo!()
+    }
+
+    todo!()
+}
+
+#[derive(Deserialize)]
+struct TokenRequestBody {
+    grant_type: String,
+    refresh_token: Option<String>,
+    scope: Option<String>,
+    redirect_uri: Option<String>,
+    code: Option<String>,
+    client_id: Option<String>,
+
+    // direct grant
+    username: Option<String>,
+    password: Option<String>,
+}
+
+async fn token(mut request: Request) -> tide::Result<tide::Response> {
+    let body: Result<TokenRequestBody, _> = request.body_form().await;
+    // let body : TokenRequestBody = request.body_form();
+    todo!()
+}
+
+pub fn oidc_v1_server(core: &'static super::ServerCoreState) -> tide::Server<ServerState> {
+    let mut srv = tide::with_state(ServerState { core });
+
+    srv.at("authorize").get(authorize).post(authorize);
+    srv.at("token").post(token);
+
+    srv
+}

+ 44 - 0
src/server/oidc/api.rs

@@ -0,0 +1,44 @@
+pub use serde::{Serialize,Deserialize};
+
+#[derive(Deserialize)]
+pub struct AuthorizationRequestQuery {
+    response_type: String,
+    client_id: String,
+    redirect_uri: String,
+    scope: Option<String>,
+    state: Option<String>
+}
+
+#[derive(Serialize)]
+pub struct AuthorizationResponse {
+    response_type: String,
+    state: Option<String>,
+    code: Option<String>,
+}
+
+#[derive(Serialize)]
+pub enum AuthorizationResponseErrorType {
+    InvalidRequest,
+    UnauthorizedClient,
+    AccessDenied,
+    UnsupportedResponseType,
+    InvalidScope,
+    ServerError,
+    TemporarilyUnavailable
+}
+
+#[derive(Serialize)]
+pub struct AuthorizationResponseError {
+    state: Option<String>,
+    error: AuthorizationResponseErrorType
+}
+
+#[derive(Deserialize)]
+pub struct TokenRequestParameters {
+    
+}
+
+#[derive(Serialize)]
+pub struct TokenResponse {
+    
+}

+ 241 - 0
src/server/session.rs

@@ -0,0 +1,241 @@
+use crate::schema;
+use serde::Deserialize;
+use tide::http::Cookie;
+use microrm::prelude::*;
+
+#[derive(Clone)]
+pub struct ServerState {
+    core: &'static super::ServerCoreState,
+    realm_cache: std::sync::Arc<std::sync::RwLock<std::collections::HashMap<String, schema::RealmID>>>
+}
+
+type Request = tide::Request<ServerState>;
+
+impl ServerState {
+    pub fn get_realm(&self, req: &Request) -> Option<schema::RealmID> {
+        let realm_str = req.param("realm").expect("get_realm called with no :realm param");
+        let cache = self.realm_cache.read().unwrap();
+        let cache_lookup = cache.get(realm_str);
+
+        // expected case
+        if cache_lookup.is_some() { return cache_lookup.map(|x| *x) }
+        drop(cache);
+
+        // unexpected case, but maybe we haven't filled that cache entry yet
+        
+        let qi = self.core.pool.query_interface();
+        let realm = qi.get().by(schema::Realm::Shortname, realm_str).one().expect("couldn't query db");
+
+        if let Some(with_id) = realm {
+            let mut cache = self.realm_cache.write().unwrap();
+            cache.insert(realm_str.to_owned(), with_id.id());
+            return Some(with_id.id())
+        }
+
+        // other expected case, is bogus realm
+        return None
+    }
+
+    fn build_session(
+        qi: &microrm::QueryInterface
+    ) -> tide::Result<(schema::SessionID, Option<tide::http::Cookie<'static>>)> {
+        let rng = ring::rand::SystemRandom::new();
+        let session_id: [u8; 32] = ring::rand::generate(&rng)
+            .map_err(|_| tide::Error::from_str(500, "Failed to generate session ID"))?
+            .expose();
+        let session_id = base64::encode_config(session_id, base64::URL_SAFE_NO_PAD);
+
+        let maybe_id = qi.add(&schema::Session { key: session_id.clone() });
+        Ok((maybe_id.ok().ok_or(tide::Error::from_str(
+            500,
+            "Failed to store session in database",
+        ))?, Some(Cookie::new("vogt_session", session_id))))
+    }
+
+    pub fn get_or_build_session(&self, req: &Request) -> tide::Result<(schema::SessionID, Option<tide::http::Cookie<'static>>)> {
+        let qi = self.core.pool.query_interface();
+        if let Some(sid) = req.cookie("vogt_session") {
+            let existing = qi.get().by(schema::Session::Key, sid.value()).one().expect("couldn't query db");
+
+            if existing.is_some() {
+                return Ok((existing.unwrap().id(), None))
+            }
+        }
+        Self::build_session(qi)
+    }
+
+    pub fn get_auth_for_session(&self, realm: schema::RealmID, session: schema::SessionID) -> Option<microrm::WithID<schema::SessionAuthentication>> {
+        let qi = self.core.pool.query_interface();
+
+        use schema::SessionAuthentication as SAC;
+        qi.get().by(SAC::Realm, &realm).by(SAC::Session, &session).one().expect("couldn't query db")
+        // qi.get_one_by_multi(&[&SAC::Realm, &SAC::Session], &microrm::value_list!(realm, session))
+    }
+
+    pub fn destroy_auth(&self, realm: schema::RealmID, session: schema::SessionID) {
+        let qi = self.core.pool.query_interface();
+        
+        use schema::SessionAuthentication as SAC;
+        qi.delete().by(SAC::Realm, &realm).by(SAC::Session, &session).exec().expect("couldn't query db")
+    }
+}
+
+impl ServerState {
+    fn render_login_from_auth(&self, mut response: tide::Response, auth: Option<schema::SessionAuthentication>, error_msg: Option<String>) -> tide::Response {
+        let to_present: Option<schema::AuthChallengeType> = match auth {
+            None => Some(schema::AuthChallengeType::Username),
+            Some(auth) => auth.challenges_left.first().copied()
+        };
+
+        if to_present.is_none() {
+            response.set_status(302);
+            tide::Redirect::new("/").into()
+        }
+        else {
+            self.render_login_page(response, to_present.unwrap(), error_msg)
+        }
+    }
+
+    fn render_login_page(&self, mut response: tide::Response, to_present: schema::AuthChallengeType, error_msg: Option<String>) -> tide::Response {
+        let tmpl = self.core.templates.read().unwrap();
+
+        let do_challenge = |ty,ch| {
+            tmpl.render("id_v1_login", &serde_json::json!(
+                {
+                    "challenge":
+                        format!(r#"
+                            <input type="hidden" name="challenge_type" value="{:?}" />
+                            <div class="challenge-type">{}</div>
+                            <div class="challenge-content">{}</div>
+                            "#,
+                            to_present, ty, ch),
+                    "error_msg": error_msg.iter().collect::<Vec<_>>()
+                }
+            )).unwrap()
+        };
+
+        response.set_content_type("text/html");
+
+        match to_present {
+            schema::AuthChallengeType::Username => {
+                response.set_body(do_challenge("Username",
+                    r#"<input name="challenge" type="text" autofocus />"#));
+            },
+            schema::AuthChallengeType::Password => {
+                response.set_body(do_challenge("Password",
+                    r#"<input name="challenge" type="password" autofocus />"#));
+            },
+            _ => todo!()
+        }
+
+        response
+    }
+}
+
+async fn v1_login(req: tide::Request<ServerState>) -> tide::Result<tide::Response> {
+    let mut response = tide::Response::builder(200).build();
+
+    let realm = req.state().get_realm(&req).ok_or(tide::Error::from_str(404, "No such realm"))?;
+    let (session_id, cookie) = req.state().get_or_build_session(&req)?;
+    cookie.map(|c| response.insert_cookie(c));
+
+    let auth = req.state().get_auth_for_session(realm, session_id);
+
+    Ok(req.state().render_login_from_auth(response, auth.map(|a| a.wrapped()), None))
+}
+
+async fn v1_login_response(mut req: tide::Request<ServerState>) -> tide::Result<tide::Response> {
+    let mut response = tide::Response::builder(200).build();
+
+    let realm = req.state().get_realm(&req).ok_or(tide::Error::from_str(404, "No such realm"))?;
+    let (session_id, cookie) = req.state().get_or_build_session(&req)?;
+    cookie.map(|c| response.insert_cookie(c));
+
+    let mut auth = req.state().get_auth_for_session(realm, session_id);
+
+    #[derive(Deserialize)]
+    struct ResponseBody {
+        challenge_type: String,
+        challenge: String
+    }
+
+    let body : ResponseBody = req.body_form().await?;
+
+    use schema::AuthChallengeType as ChallengeType;
+
+    let challenge: schema::AuthChallengeType = match body.challenge_type.as_str() {
+        "Username" => ChallengeType::Username,
+        "Password" => ChallengeType::Password,
+        _ => Err(tide::Error::from_str(400, "Unknown challenge type"))?
+    };
+
+    let mut error = None;
+
+    // check that the response matches what we're expecting next
+    let to_be_presented: Option<schema::AuthChallengeType> = match &auth {
+        None => Some(schema::AuthChallengeType::Username),
+        Some(auth) => auth.challenges_left.first().copied()
+    };
+
+    if to_be_presented != Some(challenge) {
+        Err(tide::Error::from_str(400, "Incorrect challenge type"))?
+    }
+
+    match challenge {
+        ChallengeType::Username => {
+            let qi = req.state().core.pool.query_interface();
+            req.state().destroy_auth(realm, session_id);
+
+            let user = qi.get().by(schema::User::Realm, &realm).by(schema::User::Username, &body.challenge).one().expect("couldn't query db");
+            if user.is_none() {
+                error = Some(format!("No such user {}", body.challenge));
+            }
+            else {
+                let user = user.unwrap();
+
+                let sa = schema::SessionAuthentication { session: session_id, realm: realm, user: user.id(), challenges_left: vec![schema::AuthChallengeType::Password] };
+                let id = qi.add(&sa).unwrap();
+                auth = Some(microrm::WithID::new(sa, id));
+            }
+        },
+        ChallengeType::Password => {
+            if auth.is_none() {
+                error = Some(format!("Please restart login process."));
+            }
+            else {
+                let qi = req.state().core.pool.query_interface();
+
+                use schema::AuthChallenge;
+                let challenge = qi.get().by(AuthChallenge::User, &auth.as_ref().unwrap().user).by(AuthChallenge::ChallengeType, &schema::AuthChallengeType::Password).one().expect("couldn't query db");
+
+                if challenge.is_none() {
+                    error = Some(format!("User lacks a password. Please contact an administrator."));
+                }
+                else {
+                    use ring::pbkdf2;
+
+                    let challenge = challenge.unwrap();
+
+                    let verification = pbkdf2::verify(pbkdf2::PBKDF2_HMAC_SHA256, std::num::NonZeroU32::new(20000).unwrap(), challenge.public.as_slice(), body.challenge.as_bytes(), challenge.secret.as_slice());
+
+                    if verification.is_ok() {
+                        auth.as_mut().unwrap().challenges_left.remove(0);
+                    }
+                }
+            }
+        },
+        _ => todo!()
+    };
+
+    Ok(req.state().render_login_from_auth(response, auth.map(|a| a.wrapped()), error))
+}
+
+pub fn session_v1_server(core: &'static super::ServerCoreState) -> tide::Server<ServerState> {
+    let mut srv = tide::with_state(ServerState { core, realm_cache: std::sync::Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())) });
+
+    srv.with(tide::log::LogMiddleware::new());
+
+    srv.at("login").get(v1_login).post(v1_login_response);
+
+    srv
+}

+ 50 - 0
src/user.rs

@@ -0,0 +1,50 @@
+use crate::schema;
+use microrm::prelude::*;
+
+pub struct User {
+    id: schema::UserID,
+    model: Option<schema::User>
+}
+
+const PBKDF2_ROUNDS: Option<std::num::NonZeroU32> = std::num::NonZeroU32::new(20000);
+
+impl User {
+    pub fn from_model(model: microrm::WithID<schema::User>) -> Self {
+        Self { id: model.id(), model: Some(model.wrapped()) }
+    }
+
+    /// returns Some(true) if challenge passed, Some(false) if challenge failed, and None if
+    /// challenge not found
+    pub fn verify_challenge(&self, qi: &microrm::QueryInterface, which: schema::AuthChallengeType, response: &[u8]) -> Option<bool> {
+        let challenge = qi.get().by(schema::AuthChallenge::User, &self.id).by(schema::AuthChallenge::ChallengeType, &which).one().expect("couldn't query db")?;
+
+        match which {
+            schema::AuthChallengeType::Password => self.verify_password_challenge(challenge.wrapped(), response),
+            schema::AuthChallengeType::TOTP => self.verify_totp_challenge(challenge.wrapped(), response),
+            _ => todo!()
+        }
+    }
+
+    pub fn set_new_password(&self, qi: &microrm::QueryInterface, password: &[u8]) {
+        qi.delete().by(schema::AuthChallenge::User, &self.id).by(schema::AuthChallenge::ChallengeType, &schema::AuthChallengeType::Password).exec().expect("couldn't query db");
+
+        let rng = ring::rand::SystemRandom::new();
+        let salt: [u8; 16] = ring::rand::generate(&rng).expect("Couldn't generate random salt?").expose();
+
+        let mut generated = [0u8; ring::digest::SHA256_OUTPUT_LEN];
+
+        ring::pbkdf2::derive(ring::pbkdf2::PBKDF2_HMAC_SHA256, PBKDF2_ROUNDS.unwrap(), &salt, password, &mut generated);
+
+        qi.add(&schema::AuthChallenge { user: self.id, challenge_type: schema::AuthChallengeType::Password, public: salt.into(), secret: generated.into()}).expect("couldn't set password");
+    }
+
+    fn verify_password_challenge(&self, challenge: schema::AuthChallenge, response: &[u8]) -> Option<bool> {
+        use ring::pbkdf2;
+
+        Some(pbkdf2::verify(pbkdf2::PBKDF2_HMAC_SHA256, PBKDF2_ROUNDS.unwrap(), challenge.public.as_slice(), response, challenge.secret.as_slice()).is_ok())
+    }
+
+    fn verify_totp_challenge(&self, challenge: schema::AuthChallenge, response: &[u8]) -> Option<bool> {
+        todo!()
+    }
+}

+ 58 - 0
src/user_management.rs

@@ -0,0 +1,58 @@
+use crate::{schema,user};
+use microrm::prelude::*;
+
+pub fn list(realm: &str, db: microrm::DB) {
+    // get realm ID
+    let qi = db.query_interface();
+
+    let realm_id = qi.get().by(schema::Realm::Shortname, realm).one().expect("couldn't query db").expect("No such realm").id();
+
+    let users = qi.get().by(schema::User::Realm, &realm_id).all().expect("couldn't query db");
+
+    println!("User list ({} users):", users.len());
+
+    for user in &users {
+        println!("- {:20}", user.username);
+        let auth_challenges = qi.get().by(schema::AuthChallenge::User, &user.id()).all().expect("Can't get authentication challenges?");
+        for ch in &auth_challenges {
+            println!("    - Has {:?} authentication challenge", ch.challenge_type);
+        }
+    }
+}
+
+pub fn create(realm: &str, db: microrm::DB, username: &str) {
+    // get realm ID
+    let qi = db.query_interface();
+
+    let realm_id = qi.get().by(schema::Realm::Shortname, realm).one().expect("couldn't query db").expect("No such realm").id();
+
+    // check that the user doesn't exist already
+    let existing_user = qi.get().by(schema::User::Realm, &realm_id).by(schema::User::Username, &username).one().expect("couldn't query db");
+
+    if existing_user.is_some() {
+        log::error!("Can't create user {} in {} realm as a user with that username already exists", username, realm);
+        return;
+    }
+
+    qi.add(&schema::User { realm: realm_id, username: username.to_owned() }).expect("couldn't add user");
+}
+
+pub fn change_auth(realm: &str, db: microrm::DB, username: &str, change_password: bool) {
+    // get realm ID
+    let qi = db.query_interface();
+
+    let realm_id = qi.get().by(schema::Realm::Shortname, realm).one().expect("couldn't query db").expect("No such realm").id();
+    // check that the user exists
+    let existing_user = qi.get().by(schema::User::Realm, &realm_id).by(schema::User::Username, &username).one().expect("couldn't query db");
+    if existing_user.is_none() {
+        log::error!("User {} does not exist in the {} realm!", username, realm);
+        return;
+    }
+
+    let user = user::User::from_model(existing_user.unwrap());
+
+    if change_password {
+        let raw_pass = rpassword::prompt_password("Enter new user password: ").unwrap();
+        user.set_new_password(&qi, raw_pass.as_bytes());
+    }
+}

+ 0 - 0
static/logout.html


+ 62 - 0
static/style.css

@@ -0,0 +1,62 @@
+body {
+    font-family: sans-serif;
+}
+
+div.login-box {
+    margin-left: auto;
+    margin-right: auto;
+    display: block;
+    width: 30em;
+    background: #fff;
+    border: 1px #888 solid;
+    min-height: 20em;
+
+    padding-left: 2em;
+    padding-right: 2em;
+}
+
+div.login-box h1 {
+    font-weight: normal;
+    text-align: center;
+    vertical-align: middle;
+}
+
+div.login-content {
+    display: table;
+    height: 15em;
+    width: 100%;
+}
+
+div.login-box div.spacer {
+    display: table-row;
+    min-height: 3em;
+}
+
+div.login-box div.login-challenge {
+    width: 100%;
+    display: table-row;
+}
+
+div.login-challenge div.challenge-type {
+    padding-right: 1em;
+    display: inline-block;
+}
+
+div.login-challenge div.challenge-content {
+    margin-left: auto;
+    margin-right: auto;
+    display: inline-block;
+}
+
+div.error-msg {
+    background: #fcc;
+    border: #888 1px solid;
+    border-radius: 5pt;
+    padding-left: 1em;
+    padding-right: 1em;
+}
+
+div.footer {
+    text-align: center;
+    font-size: .75em;
+}

+ 32 - 0
tmpl/id_v1_login.tmpl

@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>Login</title>
+
+        <link rel="stylesheet" text="text/css" href="/static/style.css">
+    </head>
+    <body>
+        <div>
+            <div class="login-box">
+                <h1>Login</h1>
+                
+                <form action="login" method="POST">
+                    <div class="login-content">
+                        <div class="spacer">&nbsp;
+                            {{ #each error_msg }}
+                                <div class="error-msg">{{ this }}</div>
+                            {{ /each }}
+                        </div>
+                        <div class="login-challenge">
+                            {{{ challenge }}}
+                        </div>
+                        <div class="spacer">&nbsp;</div>
+                    </div>
+                </form>
+            </div>
+            <div class="footer">
+                Copyright &copy; Kestrel 2022. Released under the terms of the 4-clause BSD license.
+            </div>
+        </div>
+    </body>
+</html>

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff