Forráskód Böngészése

Changes pending swap to static login pages.

Kestrel 2 éve
szülő
commit
1118273564
13 módosított fájl, 694 hozzáadás és 22 törlés
  1. 1 0
      .vimrc
  2. 188 16
      Cargo.lock
  3. 6 1
      Cargo.toml
  4. 1 0
      src/login.rs
  5. 7 1
      src/main.rs
  6. 7 4
      src/schema.rs
  7. 56 0
      src/server.rs
  8. 140 0
      src/server/identity.rs
  9. 162 0
      src/server/oidc.rs
  10. 44 0
      src/server/oidc/api.rs
  11. 28 0
      srv/login.html
  12. 0 0
      srv/logout.html
  13. 54 0
      srv/style.css

+ 1 - 0
.vimrc

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

+ 188 - 16
Cargo.lock

@@ -8,7 +8,7 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331"
 dependencies = [
- "generic-array",
+ "generic-array 0.14.5",
 ]
 
 [[package]]
@@ -43,7 +43,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072"
 dependencies = [
  "cipher",
- "opaque-debug",
+ "opaque-debug 0.3.0",
 ]
 
 [[package]]
@@ -53,7 +53,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce"
 dependencies = [
  "cipher",
- "opaque-debug",
+ "opaque-debug 0.3.0",
 ]
 
 [[package]]
@@ -346,13 +346,25 @@ dependencies = [
  "digest 0.9.0",
 ]
 
+[[package]]
+name = "block-buffer"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
+dependencies = [
+ "block-padding",
+ "byte-tools",
+ "byteorder",
+ "generic-array 0.12.4",
+]
+
 [[package]]
 name = "block-buffer"
 version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
 dependencies = [
- "generic-array",
+ "generic-array 0.14.5",
 ]
 
 [[package]]
@@ -361,7 +373,16 @@ version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
 dependencies = [
- "generic-array",
+ "generic-array 0.14.5",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
+dependencies = [
+ "byte-tools",
 ]
 
 [[package]]
@@ -384,6 +405,18 @@ version = "3.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
 
+[[package]]
+name = "byte-tools"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
 [[package]]
 name = "cache-padded"
 version = "1.2.0"
@@ -428,7 +461,7 @@ version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801"
 dependencies = [
- "generic-array",
+ "generic-array 0.14.5",
 ]
 
 [[package]]
@@ -545,7 +578,7 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
 dependencies = [
- "generic-array",
+ "generic-array 0.14.5",
  "typenum",
 ]
 
@@ -555,7 +588,7 @@ version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
 dependencies = [
- "generic-array",
+ "generic-array 0.14.5",
  "subtle",
 ]
 
@@ -565,7 +598,7 @@ version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a"
 dependencies = [
- "generic-array",
+ "generic-array 0.14.5",
  "subtle",
 ]
 
@@ -598,13 +631,22 @@ dependencies = [
  "num_cpus",
 ]
 
+[[package]]
+name = "digest"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
+dependencies = [
+ "generic-array 0.12.4",
+]
+
 [[package]]
 name = "digest"
 version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
 dependencies = [
- "generic-array",
+ "generic-array 0.14.5",
 ]
 
 [[package]]
@@ -638,6 +680,12 @@ version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71"
 
+[[package]]
+name = "fake-simd"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
+
 [[package]]
 name = "fastrand"
 version = "1.7.0"
@@ -740,6 +788,15 @@ dependencies = [
  "slab",
 ]
 
+[[package]]
+name = "generic-array"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
+dependencies = [
+ "typenum",
+]
+
 [[package]]
 name = "generic-array"
 version = "0.14.5"
@@ -778,7 +835,7 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375"
 dependencies = [
- "opaque-debug",
+ "opaque-debug 0.3.0",
  "polyval",
 ]
 
@@ -794,6 +851,20 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "handlebars"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d113a9853e5accd30f43003560b5563ffbb007e3f325e8b103fa0d0029c6e6df"
+dependencies = [
+ "log",
+ "pest",
+ "pest_derive",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.11.2"
@@ -969,6 +1040,12 @@ dependencies = [
  "value-bag",
 ]
 
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
 [[package]]
 name = "matches"
 version = "0.1.9"
@@ -983,9 +1060,10 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 
 [[package]]
 name = "microrm"
-version = "0.1.2"
+version = "0.2.2"
 dependencies = [
  "base64 0.13.0",
+ "lazy_static",
  "microrm-macros",
  "serde",
  "serde_bytes",
@@ -996,7 +1074,7 @@ dependencies = [
 
 [[package]]
 name = "microrm-macros"
-version = "0.1.2"
+version = "0.2.0"
 dependencies = [
  "convert_case",
  "proc-macro2",
@@ -1039,6 +1117,12 @@ version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
 
+[[package]]
+name = "opaque-debug"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
+
 [[package]]
 name = "opaque-debug"
 version = "0.3.0"
@@ -1063,6 +1147,49 @@ version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
 
+[[package]]
+name = "pest"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
+dependencies = [
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
+dependencies = [
+ "maplit",
+ "pest",
+ "sha-1",
+]
+
 [[package]]
 name = "pin-project"
 version = "1.0.10"
@@ -1127,7 +1254,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd"
 dependencies = [
  "cpuid-bool",
- "opaque-debug",
+ "opaque-debug 0.3.0",
  "universal-hash",
 ]
 
@@ -1379,6 +1506,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "sha-1"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
+dependencies = [
+ "block-buffer 0.7.3",
+ "digest 0.8.1",
+ "fake-simd",
+ "opaque-debug 0.2.3",
+]
+
 [[package]]
 name = "sha1"
 version = "0.6.1"
@@ -1404,7 +1543,7 @@ dependencies = [
  "cfg-if 1.0.0",
  "cpufeatures",
  "digest 0.9.0",
- "opaque-debug",
+ "opaque-debug 0.3.0",
 ]
 
 [[package]]
@@ -1507,6 +1646,19 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "stderrlog"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45a53e2eff3e94a019afa6265e8ee04cb05b9d33fe9f5078b14e4e391d155a38"
+dependencies = [
+ "atty",
+ "chrono",
+ "log",
+ "termcolor",
+ "thread_local",
+]
+
 [[package]]
 name = "stdweb"
 version = "0.4.20"
@@ -1623,6 +1775,15 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "thread_local"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
+dependencies = [
+ "lazy_static",
+]
+
 [[package]]
 name = "tide"
 version = "0.16.0"
@@ -1716,6 +1877,12 @@ version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
 
+[[package]]
+name = "ucd-trie"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
+
 [[package]]
 name = "unicode-bidi"
 version = "0.3.8"
@@ -1743,7 +1910,7 @@ version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05"
 dependencies = [
- "generic-array",
+ "generic-array 0.14.5",
  "subtle",
 ]
 
@@ -1790,15 +1957,20 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 name = "vogt"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "async-std",
  "base64 0.13.0",
  "clap",
+ "handlebars",
+ "lazy_static",
+ "log",
  "microrm",
  "ring",
  "serde",
  "serde_bytes",
  "serde_json",
  "sha2 0.10.2",
+ "stderrlog",
  "tide",
 ]
 

+ 6 - 1
Cargo.toml

@@ -14,12 +14,17 @@ 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 = "4.3"
+lazy_static = "1.4.0"
 
 # Data storage dependencies
-microrm = { path = "../microrm/microrm", version = "0.1.2" }
+microrm = { path = "../microrm/microrm", version = "0.2.1" }
 
 # Public API dependencies
 tide = { version = "0.16.0" }
+anyhow = { version = "1.0" }
 
 # CLI dependencies
 clap = { version = "3.1.15", features = ["derive"] }

+ 1 - 0
src/login.rs

@@ -0,0 +1 @@
+

+ 7 - 1
src/main.rs

@@ -1,9 +1,15 @@
 mod cert;
 mod cli;
+mod login;
 mod schema;
 mod server;
-mod login;
 
 fn main() {
+    stderrlog::new()
+        .verbosity(5)
+        .module(module_path!())
+        .init()
+        .unwrap();
+
     cli::invoked();
 }

+ 7 - 4
src/schema.rs

@@ -14,9 +14,11 @@ pub struct SessionAuthentication {
     #[microrm_foreign]
     pub session: SessionID,
     #[microrm_foreign]
+    pub realm: RealmID,
+    #[microrm_foreign]
     pub user: UserID,
 
-    challenges_left: Vec<AuthChallengeType>
+    pub challenges_left: Vec<AuthChallengeType>,
 }
 
 // **** oauth types ****
@@ -41,12 +43,13 @@ pub struct User {
     pub username: String,
 }
 
-#[derive(Modelable, Serialize, Deserialize)]
+#[derive(Clone, Copy, Debug, Modelable, Serialize, Deserialize)]
 pub enum AuthChallengeType {
+    Username,
     Password,
     TOTP,
     Grid,
-    WebAuthn
+    WebAuthn,
 }
 
 #[derive(Entity, Serialize, Deserialize)]
@@ -55,7 +58,7 @@ pub struct AuthChallenge {
     pub user: UserID,
     pub challenge_type: AuthChallengeType,
     pub public: String,
-    pub secret: String
+    pub secret: String,
 }
 
 #[derive(Entity, Serialize, Deserialize)]

+ 56 - 0
src/server.rs

@@ -0,0 +1,56 @@
+use crate::schema;
+
+mod identity;
+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_one_by(crate::schema::SessionColumns::Key, session_id)
+            .is_some(),
+    )
+}
+
+pub async fn run_server(db: microrm::DB) {
+    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 };
+
+    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("srv/")
+        .expect("Can't serve static files");
+
+    app.at("/:realm/v1/id/")
+        .nest(identity::id_v1_server(core_state));
+    app.at("/:realm/v1/oidc")
+        .nest(oidc::oidc_v1_server(core_state));
+
+    app.listen("127.0.0.1:2114").await.expect("Can listen");
+}

+ 140 - 0
src/server/identity.rs

@@ -0,0 +1,140 @@
+use crate::schema;
+use tide::http::Cookie;
+
+#[derive(Clone)]
+pub struct ServerState {
+    core: &'static super::ServerCoreState,
+}
+
+type Request = tide::Request<ServerState>;
+
+async fn v1_check(req: Request) -> tide::Result<tide::Response> {
+    let validity = super::is_auth_valid(req.state().core, &req);
+    if let Some(true) = validity {
+        return Ok(tide::Response::builder(200).build());
+    } else if let Some(false) = validity {
+        return Ok(tide::Response::builder(403).build());
+    } else {
+        return Ok(tide::Response::builder(401).build());
+    }
+}
+
+fn build_session(
+    qi: &microrm::QueryInterface,
+    response: &mut tide::Response,
+) -> tide::Result<schema::SessionID> {
+    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);
+
+    response.insert_cookie(Cookie::new("vogt_session", session_id.clone()));
+
+    let maybe_id = qi.add(&crate::schema::Session { key: session_id });
+    Ok(maybe_id.ok_or(tide::Error::from_str(
+        500,
+        "Failed to store session in database",
+    ))?)
+}
+
+fn render_login_page(state: &ServerState, mut response: tide::Response, to_present: schema::AuthChallengeType) -> tide::Response {
+    let tmpl = state.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, serde_json::to_string(&to_present).unwrap(),ty,ch)
+            }
+        )).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(mut req: tide::Request<ServerState>) -> tide::Result<tide::Response> {
+    let mut response = tide::Response::builder(200).build();
+    let qi = req.state().core.pool.query_interface();
+
+    let sid: schema::SessionID = match req.cookie("vogt_session") {
+        Some(sid) => {
+            let existing = qi.get_one_by(schema::SessionColumns::Key, sid.value());
+
+            if let Some(id) = existing {
+                id.id()
+            } else {
+                build_session(&qi, &mut response)?
+            }
+        }
+        None => build_session(&qi, &mut response)?,
+    };
+
+    let realm_name = req
+        .param("realm")
+        .expect("Failed to parse realm out of path?");
+
+    let realm_id = qi
+        .get_one_by(schema::RealmColumns::Shortname, realm_name)
+        .ok_or(tide::Error::from_str(404, "No such realm"))?;
+
+    let realm_auth = qi.get_one_by_multi(
+        &[
+            schema::SessionAuthenticationColumns::Session,
+            schema::SessionAuthenticationColumns::Realm,
+        ],
+        &microrm::value_list!(&sid, &realm_id.id()),
+    );
+
+    let to_present: Option<schema::AuthChallengeType> = match realm_auth {
+        None => Some(schema::AuthChallengeType::Username),
+        Some(auth) => auth.challenges_left.first().map(|x| *x),
+    };
+
+    if to_present.is_some() {
+        Ok(render_login_page(req.state(), response, to_present.unwrap()))
+    }
+    else {
+        // already logged in...
+        todo!()
+    }
+}
+
+async fn v1_login_response(mut req: tide::Request<ServerState>) -> tide::Result<tide::Response> {
+    todo!()
+}
+
+pub fn id_v1_server(core: &'static super::ServerCoreState) -> tide::Server<ServerState> {
+    let mut srv = tide::with_state(ServerState { core });
+
+    srv.with(tide::log::LogMiddleware::new());
+
+    srv.at("login").get(v1_login).post(v1_login_response);
+    srv.at("check").get(v1_check).head(v1_check);
+
+    // load and register templates
+    core.templates.write().unwrap()
+        .register_template_file("id_v1_login", "srv/login.html")
+        .expect("Couldn't register template!");
+
+    srv
+}

+ 162 - 0
src/server/oidc.rs

@@ -0,0 +1,162 @@
+use crate::schema;
+use serde::{Deserialize, Serialize};
+
+#[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_one_by(
+        schema::RealmColumns::Shortname,
+        request.param("realm").unwrap(),
+    );
+    if realm.is_none() {
+        return Err(OIDCError(
+            OIDCErrorType::InvalidRequest,
+            "No such realm!".to_string(),
+            state,
+        ));
+    }
+    let realm = realm.unwrap();
+
+    let client = qi.get_one_by_multi(
+        &[
+            schema::ClientColumns::Realm,
+            schema::ClientColumns::Shortname,
+        ],
+        &microrm::value_list!(&realm.id(), &qp.client_id),
+    );
+    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 {
+    
+}

+ 28 - 0
srv/login.html

@@ -0,0 +1,28 @@
+<!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;</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>

+ 0 - 0
srv/logout.html


+ 54 - 0
srv/style.css

@@ -0,0 +1,54 @@
+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.footer {
+    text-align: center;
+    font-size: .75em;
+}