aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbenj <benj@rse8.com>2022-12-12 17:06:57 -0800
committerbenj <benj@rse8.com>2022-12-12 17:06:57 -0800
commit0920c4d4f30a3345870d385d5c6f3e0919228b56 (patch)
treef54668d91db469b7304758893a51b590c8f9b0de
parent3a4de13528fc85dcbe6bc9055d97ba5cc87f5712 (diff)
downloadsecdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar
secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.gz
secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.bz2
secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.lz
secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.xz
secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.tar.zst
secdiam-0920c4d4f30a3345870d385d5c6f3e0919228b56.zip
(oauth2 + email added): a mess that may or may not really work and needs to be refactored...
Diffstat (limited to '')
-rw-r--r--Cargo.lock405
-rw-r--r--crates/iam/Cargo.toml5
-rw-r--r--crates/iam/src/api.rs71
-rw-r--r--crates/iam/src/command.rs52
-rw-r--r--crates/iam/src/main.rs108
-rw-r--r--crates/iam/src/util.rs4
-rw-r--r--crates/secd/Cargo.toml4
-rw-r--r--crates/secd/src/client/mod.rs233
-rw-r--r--crates/secd/src/client/sqldb.rs324
-rw-r--r--crates/secd/src/client/types.rs3
-rw-r--r--crates/secd/src/command/admin.rs57
-rw-r--r--crates/secd/src/command/authn.rs230
-rw-r--r--crates/secd/src/command/mod.rs66
-rw-r--r--crates/secd/src/lib.rs390
-rw-r--r--crates/secd/src/util/mod.rs158
-rw-r--r--crates/secd/store/pg/migrations/20221116062550_bootstrap.sql83
-rw-r--r--crates/secd/store/pg/sql/find_email_validation.sql17
-rw-r--r--crates/secd/store/pg/sql/find_identity.sql14
-rw-r--r--crates/secd/store/pg/sql/find_identity_by_code.sql4
-rw-r--r--crates/secd/store/pg/sql/read_oauth_provider.sql12
-rw-r--r--crates/secd/store/pg/sql/read_oauth_validation.sql23
-rw-r--r--crates/secd/store/pg/sql/read_validation_type.sql7
-rw-r--r--crates/secd/store/pg/sql/write_email.sql5
-rw-r--r--crates/secd/store/pg/sql/write_email_validation.sql44
-rw-r--r--crates/secd/store/pg/sql/write_identity.sql4
-rw-r--r--crates/secd/store/pg/sql/write_oauth_provider.sql25
-rw-r--r--crates/secd/store/pg/sql/write_oauth_validation.sql45
-rw-r--r--crates/secd/store/pg/sql/write_session.sql7
-rw-r--r--crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql87
-rw-r--r--crates/secd/store/sqlite/sql/find_email_validation.sql18
-rw-r--r--crates/secd/store/sqlite/sql/find_identity.sql14
-rw-r--r--crates/secd/store/sqlite/sql/find_identity_by_code.sql12
-rw-r--r--crates/secd/store/sqlite/sql/read_email_raw_id.sql2
-rw-r--r--crates/secd/store/sqlite/sql/read_identity_raw_id.sql2
-rw-r--r--crates/secd/store/sqlite/sql/read_oauth_provider.sql12
-rw-r--r--crates/secd/store/sqlite/sql/read_oauth_validation.sql23
-rw-r--r--crates/secd/store/sqlite/sql/read_validation_type.sql7
-rw-r--r--crates/secd/store/sqlite/sql/write_email.sql7
-rw-r--r--crates/secd/store/sqlite/sql/write_email_validation.sql44
-rw-r--r--crates/secd/store/sqlite/sql/write_identity.sql12
-rw-r--r--crates/secd/store/sqlite/sql/write_oauth_provider.sql23
-rw-r--r--crates/secd/store/sqlite/sql/write_oauth_validation.sql45
-rw-r--r--crates/secd/store/sqlite/sql/write_session.sql7
-rw-r--r--justfile5
44 files changed, 2243 insertions, 477 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e44d0d3..98ec89b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -14,12 +14,27 @@ dependencies = [
]
[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "anyhow"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
[[package]]
+name = "ascii"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
+
+[[package]]
name = "async-attributes"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -272,6 +287,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
+name = "chunked_transfer"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
+
+[[package]]
name = "clap"
version = "4.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -479,6 +500,28 @@ dependencies = [
]
[[package]]
+name = "encoding_rs"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -527,6 +570,12 @@ dependencies = [
]
[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -585,7 +634,7 @@ checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5"
dependencies = [
"futures-core",
"lock_api",
- "parking_lot",
+ "parking_lot 0.11.2",
]
[[package]]
@@ -683,6 +732,25 @@ dependencies = [
]
[[package]]
+name = "h2"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -761,6 +829,83 @@ dependencies = [
]
[[package]]
+name = "http"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "hyper"
+version = "0.14.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
name = "iam"
version = "0.1.0"
dependencies = [
@@ -768,6 +913,7 @@ dependencies = [
"async-std",
"clap",
"colored",
+ "env_logger",
"home",
"log",
"rand",
@@ -777,7 +923,11 @@ dependencies = [
"strum",
"strum_macros",
"thiserror",
+ "tiny_http",
+ "tokio",
"toml",
+ "url",
+ "urlencoding",
"uuid",
]
@@ -821,6 +971,12 @@ dependencies = [
]
[[package]]
+name = "ipnet"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e"
+
+[[package]]
name = "is-terminal"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -930,12 +1086,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
+name = "mio"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -973,6 +1147,16 @@ dependencies = [
]
[[package]]
+name = "num_cpus"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+]
+
+[[package]]
name = "once_cell"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1043,7 +1227,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
- "parking_lot_core",
+ "parking_lot_core 0.8.5",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core 0.9.5",
]
[[package]]
@@ -1061,6 +1255,19 @@ dependencies = [
]
[[package]]
+name = "parking_lot_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
name = "paste"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1223,6 +1430,23 @@ dependencies = [
]
[[package]]
+name = "regex"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1232,6 +1456,43 @@ dependencies = [
]
[[package]]
+name = "reqwest"
+version = "0.11.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1286,15 +1547,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
name = "secd"
version = "0.1.0"
dependencies = [
+ "anyhow",
"async-std",
"async-trait",
"base64",
+ "clap",
"derive_more",
"email_address",
"lazy_static",
"log",
"openssl",
"rand",
+ "reqwest",
"serde",
"serde_json",
"sqlx",
@@ -1302,6 +1566,7 @@ dependencies = [
"strum_macros",
"thiserror",
"time",
+ "url",
"uuid",
]
@@ -1366,6 +1631,18 @@ dependencies = [
]
[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
name = "sha1"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1668,6 +1945,18 @@ dependencies = [
]
[[package]]
+name = "tiny_http"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
+dependencies = [
+ "ascii",
+ "chunked_transfer",
+ "httpdate",
+ "log",
+]
+
+[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1683,6 +1972,61 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
+name = "tokio"
+version = "1.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "parking_lot 0.12.1",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
name = "toml"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1692,6 +2036,38 @@ dependencies = [
]
[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1742,6 +2118,12 @@ dependencies = [
]
[[package]]
+name = "urlencoding"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
+
+[[package]]
name = "uuid"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1780,6 +2162,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2011,3 +2403,12 @@ name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
+
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
diff --git a/crates/iam/Cargo.toml b/crates/iam/Cargo.toml
index fd9006d..ba642c3 100644
--- a/crates/iam/Cargo.toml
+++ b/crates/iam/Cargo.toml
@@ -8,6 +8,7 @@ anyhow = "1.0"
async-std = { version = "1.12.0", features = [ "attributes" ] }
clap = { version = "4.0.29", features = ["derive"] }
colored = "2.0.0"
+env_logger = "0.9"
home = "0.5.4"
log = "0.4"
rand = "0.8"
@@ -16,6 +17,10 @@ serde = "1"
serde_json = { version = "1.0", features = ["raw_value"] }
strum = "0.24.1"
strum_macros = "0.24"
+tiny_http = "0.12"
+tokio = { version = "1.23.0", features = ["full"] }
toml = "0.5.9"
thiserror = "1.0"
+url = "2.3.1"
+urlencoding = "2.1.2"
uuid = { version = "1.2", features = ["v4", "serde"]} \ No newline at end of file
diff --git a/crates/iam/src/api.rs b/crates/iam/src/api.rs
index 5b72d93..8b46d08 100644
--- a/crates/iam/src/api.rs
+++ b/crates/iam/src/api.rs
@@ -1,22 +1,26 @@
use crate::ISSUE_TRACKER_LOC;
use clap::{Parser, Subcommand, ValueEnum};
use colored::*;
+use secd::{IdentityId, OauthProviderName};
use serde::{Deserialize, Serialize};
use thiserror;
+use url::Url;
use uuid::Uuid;
#[derive(Debug, thiserror::Error)]
pub enum CliError {
+ #[error("{} {}", "Failed to initialize an iam store.".red(), format!("An invariant was likely broken and should be reported as a bug here: {}", ISSUE_TRACKER_LOC))]
+ AdminInitializationError,
+ #[error("{} {}", "Failed to recieve incoming request.".red(), .0.white())]
+ DevOauthServer(String),
+ #[error("{} {} {}", "An unknown error occurred.".red(), format!("An invariant was likely broken and should be reported as a bug here: {}", ISSUE_TRACKER_LOC), .0.yellow())]
+ InternalError(String),
+ #[error("{} {}", "The provided validation id and code is invalid or has expired.".red(), "You may recieve at most one session with a valid code, after which a new validation is required.")]
+ InvalidCode,
#[error("{}", "iam failed to read a valid configuration profile. Initialize an iam store with `iam admin init`".red())]
InvalidProfile,
#[error("{} {}", "Failed to initialize secd: ".red(), .0.yellow())]
SecdInitializationFailure(String),
- #[error("{} {}", "Fail to initialize an iam store.".red(), format!("An invariant was likely broken and should be reported as a bug here: {}", ISSUE_TRACKER_LOC))]
- AdminInitializationError,
- #[error("{} {}", "The provided validation id and code is invalid or has expired.".red(), "You may recieve at most one session with a valid code, after which a new validation is required.")]
- InvalidCode,
- #[error("{} {}", "An unknown error occurred.".red(), format!("An invariant was likely broken and should be reported as a bug here: {}", ISSUE_TRACKER_LOC))]
- Unknown,
}
#[derive(Parser)]
@@ -178,11 +182,11 @@ pub enum AdminObject {
public_key: Option<String>,
},
/// A selected Oauth2.0 provider capable of authenticating identities
- OauthProvider {
- provider: OauthProvider,
+ Oauth2Provider {
+ provider: OauthProviderName,
client_id: String,
secret: String,
- redirect_uri: String,
+ redirect_url: Url,
},
/// A selected provider capable of sending SMS
SmsProvider {
@@ -320,7 +324,17 @@ pub enum CreateObject {
}
#[derive(Subcommand)]
-pub enum DevObject {}
+pub enum DevObject {
+ #[command(
+ about = "Create a temporary server to easily receive oauth validation during development.",
+ long_about = "Oauth2\n\nCreate a temporary server to easily receive oauth validation during development."
+ )]
+ Oauth2Server {
+ /// The port on which the server should listen. You must specify this exact port with your oauth provider. Defaults to 1337
+ #[arg(long, short)]
+ port: Option<u16>,
+ },
+}
#[derive(Subcommand)]
pub enum ValidationMethod {
@@ -335,9 +349,11 @@ pub enum ValidationMethod {
Kerberos,
/// An oauth2 provider to authenticate (and authorize) an identity
Oauth2 {
- provider: OauthProvider,
+ provider: OauthProviderName,
/// An optional scope to use for authorization
scope: Option<String>,
+ /// An optional existing identity to link to this validation request
+ identity: Option<IdentityId>,
},
/// A phone which an identity may authenticate via SMS or voice
Phone {
@@ -349,28 +365,6 @@ pub enum ValidationMethod {
Saml,
}
-#[derive(Clone, ValueEnum)]
-pub enum OauthProvider {
- Amazon,
- Apple,
- Dropbox,
- Facebook,
- Github,
- Gitlab,
- Google,
- Instagram,
- LinkedIn,
- Microsoft,
- Paypal,
- Reddit,
- Spotify,
- Strava,
- Stripe,
- Twitch,
- Twitter,
- WeChat,
-}
-
#[derive(Subcommand)]
pub enum GetObject {
ApiKey {
@@ -507,3 +501,14 @@ pub struct ConfigProfile {
pub email_template_login: Option<String>,
pub email_template_signup: Option<String>,
}
+
+#[derive(Serialize, Deserialize)]
+pub struct Validation {
+ pub validation_id: Uuid,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub oauth_auth_url: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub note: Option<String>,
+}
+
+pub type ValidationSecretCode = String;
diff --git a/crates/iam/src/command.rs b/crates/iam/src/command.rs
index e9e0f23..980c4d0 100644
--- a/crates/iam/src/command.rs
+++ b/crates/iam/src/command.rs
@@ -1,6 +1,6 @@
use crate::{
- api,
- util::{self, get_config_profile, Result},
+ api::{self, CliError, Validation, ValidationSecretCode},
+ util::{self, error_detail, get_config_profile, Result},
CONFIG_LOGIN_TEMPLATE, CONFIG_SIGNUP_TEMPLATE,
};
use async_std::fs;
@@ -9,10 +9,13 @@ use rand::distributions::{Alphanumeric, DistString};
use secd::{AuthEmail, AuthStore};
use std::{
fs::File,
- io::{self, stdin, stdout, Write},
- str::FromStr,
+ io::{self, stdin, stdout, Read, Write},
+ net::TcpListener,
+ str::{self, FromStr},
};
use strum::VariantNames;
+use tiny_http::Server;
+use uuid::Uuid;
const DEFAULT_LOGIN_EMAIL: &str = "<!doctype html><html><body><p>You requested a login link for %secd_email_address%. Please click the following link<br/><br/>http://localhost:5500/myapp/iam/exchange/%secd_link%<br/><br/>or use code: %secd_code%</p></body></html>";
const DEFAULT_SIGNUP_EMAIL: &str = "<!doctype html><html><body><h1>Welcome to SecD IAM</h1></h1><p>If you did not request this sign up, you can safely ignore this email. Otherwise, please click the following link to validate your account<br/><br/>http://localhost:5500/myapp/iam/exchange/%secd_link%<br/><br/>or use code: %secd_code%</p></body></html>";
@@ -162,3 +165,44 @@ pub async fn admin_init(is_interactive: bool) -> Result<()> {
}
Ok(())
}
+
+pub fn dev_oauth2_listen(port: Option<u16>) -> Result<ValidationSecretCode> {
+ let server = Server::http(&format!("localhost:{}", port.unwrap_or(1337))).map_err(|_| {
+ CliError::InternalError(error_detail(
+ "53abd03d-c426-4bba-969d-f1dbed9af75b",
+ "Failure while creating a server to listen to oauth responese",
+ ))
+ })?;
+
+ let parser = |s: &str| -> Option<ValidationSecretCode> {
+ let maybe_code = s.split("code=").collect::<Vec<&str>>();
+ if maybe_code.len() != 2 {
+ None
+ } else {
+ let maybe_code = maybe_code
+ .last()
+ .map(|s| s.to_string())
+ .map(|c| {
+ c.split("&")
+ .collect::<Vec<&str>>()
+ .first()
+ .map(|s| s.to_string())
+ })
+ .flatten();
+
+ maybe_code.map(|s| s.to_string())
+ }
+ };
+
+ let mut s_code = String::new();
+ for req in server.incoming_requests() {
+ match parser(req.url()) {
+ Some(secret_code) => {
+ s_code = secret_code;
+ break;
+ }
+ None => continue,
+ }
+ }
+ Ok(urlencoding::decode(&s_code)?.to_string())
+}
diff --git a/crates/iam/src/main.rs b/crates/iam/src/main.rs
index c187380..85b3e37 100644
--- a/crates/iam/src/main.rs
+++ b/crates/iam/src/main.rs
@@ -2,10 +2,17 @@ mod api;
mod command;
mod util;
-use api::{AdminAction, Args, CliError, Command, CreateObject, GetObject, LinkObject, ListObject};
+use anyhow::bail;
+use api::{
+ AdminAction, AdminObject, Args, CliError, Command, CreateObject, DevObject, GetObject,
+ LinkObject, ListObject, Validation,
+};
use clap::Parser;
+use command::dev_oauth2_listen;
+use env_logger::Env;
use secd::{Secd, SecdError};
-use util::Result;
+use util::{error_detail, Result};
+use uuid::Uuid;
use crate::api::ValidationMethod;
@@ -15,8 +22,9 @@ const CONFIG_LOGIN_TEMPLATE: &str = "default_login.html";
const CONFIG_SIGNUP_TEMPLATE: &str = "default_signup.html";
const ISSUE_TRACKER_LOC: &str = "https://www.github.com/secdiam/iam";
-#[async_std::main]
+#[tokio::main]
async fn main() {
+ env_logger::init_from_env(Env::default().default_filter_or("debug"));
match exec().await {
Ok(Some(s)) => println!("{}", s),
Err(e) => {
@@ -30,10 +38,15 @@ async fn main() {
async fn exec() -> Result<Option<String>> {
let args = Args::parse();
Ok(match args.command {
- Command::Init { interactive } => admin(AdminAction::Init { interactive })
- .await
- .map_err(|_| CliError::AdminInitializationError)?,
- Command::Admin { action } => admin(action).await?,
+ Command::Init { interactive }
+ | Command::Admin {
+ action: AdminAction::Init { interactive },
+ } => {
+ command::admin_init(interactive)
+ .await
+ .map_err(|_| CliError::AdminInitializationError)?;
+ None
+ }
rest @ _ => {
let cfg = util::read_config(args.profile).map_err(|_| CliError::InvalidProfile)?;
@@ -48,8 +61,14 @@ async fn exec() -> Result<Option<String>> {
.map_err(|e| CliError::SecdInitializationFailure(e.to_string()))?;
match rest {
+ Command::Admin { action } => admin(&secd, action).await?,
Command::Create { object } => create(&secd, object).await?,
+ Command::Dev { object } => dev(object).await?,
Command::Get { object } => get(&secd, object).await?,
+ Command::Init { .. } => bail!(CliError::InternalError(error_detail(
+ "4a696b66-6231-4a2f-811c-4448a41473d2",
+ "Code path should be unreachable",
+ ))),
Command::Link { object, unlink } => link(&secd, object, unlink).await?,
Command::Ls {
object,
@@ -60,28 +79,30 @@ async fn exec() -> Result<Option<String>> {
Command::Repl => {
unimplemented!()
}
- _ => None,
}
}
})
}
-async fn admin(cmd: AdminAction) -> Result<Option<String>> {
+async fn admin(secd: &Secd, cmd: AdminAction) -> Result<Option<String>> {
Ok(match cmd {
AdminAction::Backend { action } => {
println!("do backend stuff!");
None
}
- AdminAction::Create { object } => {
- println!("do create!");
- None
- }
- AdminAction::Init { interactive } => {
- command::admin_init(interactive)
- .await
- .map_err(|_| CliError::AdminInitializationError)?;
- None
- }
+ AdminAction::Create { object } => match object {
+ AdminObject::Oauth2Provider {
+ provider,
+ client_id,
+ secret,
+ redirect_url,
+ } => {
+ secd.create_oauth_provider(&provider, client_id, secret, redirect_url)
+ .await?;
+ None
+ }
+ rest @ _ => unimplemented!(),
+ },
AdminAction::Seal => {
println!("do seal");
None
@@ -90,6 +111,9 @@ async fn admin(cmd: AdminAction) -> Result<Option<String>> {
println!("do unseal: {}", secret_key);
None
}
+ AdminAction::Init { .. } => {
+ panic!("Invariant violation: this path should be impossible")
+ }
})
}
async fn create(secd: &Secd, cmd: CreateObject) -> Result<Option<String>> {
@@ -130,19 +154,57 @@ async fn create(secd: &Secd, cmd: CreateObject) -> Result<Option<String>> {
.await
.map_err(|e| match e {
SecdError::InvalidCode => CliError::InvalidCode,
- _ => CliError::Unknown,
+ _ => CliError::InternalError(error_detail(
+ "17e5c226-5d7d-44a2-b3b5-be3ee958c252",
+ "An unknown error while exchanging a session",
+ )),
})?;
serde_json::to_string(&session).ok()
}
CreateObject::Validation { method, identity } => match method {
- ValidationMethod::Email { address } => {
- secd.create_validation_request(Some(&address)).await?;
- None
+ ValidationMethod::Email { address } => serde_json::to_string(&Validation {
+ validation_id: secd.create_validation_request_email(Some(&address)).await?,
+ note: Some("<secret code> sent to client".into()),
+ oauth_auth_url: None,
+ })
+ .ok(),
+
+ ValidationMethod::Oauth2 {
+ provider,
+ scope,
+ identity,
+ } => {
+ let redirect = secd
+ .create_validation_request_oauth(&provider, scope)
+ .await?
+ .to_string();
+ let validation_id = redirect
+ .split("state=")
+ .collect::<Vec<&str>>()
+ .last()
+ .map(|i| Uuid::parse_str(i).ok())
+ .flatten()
+ .unwrap();
+ serde_json::to_string(&Validation {
+ validation_id,
+ note: Some(
+ "<secret code> is retrieved by completing oauth flow in the browser".into(),
+ ),
+ oauth_auth_url: Some(redirect),
+ })
+ .ok()
}
_ => unimplemented!(),
},
})
}
+
+async fn dev(cmd: DevObject) -> Result<Option<String>> {
+ Ok(match cmd {
+ DevObject::Oauth2Server { port } => serde_json::to_string(&dev_oauth2_listen(port)?).ok(),
+ })
+}
+
async fn get(secd: &Secd, cmd: GetObject) -> Result<Option<String>> {
Ok(match cmd {
GetObject::ApiKey { public_key } => {
diff --git a/crates/iam/src/util.rs b/crates/iam/src/util.rs
index 01ce851..a74ea4a 100644
--- a/crates/iam/src/util.rs
+++ b/crates/iam/src/util.rs
@@ -86,3 +86,7 @@ pub fn read_config(profile_name: Option<String>) -> Result<ConfigProfile> {
Ok(cfg)
}
+
+pub fn error_detail(id: &str, d: &str) -> String {
+ format!("[debug info {}] {}", id, d)
+}
diff --git a/crates/secd/Cargo.toml b/crates/secd/Cargo.toml
index 7e80277..d65bf51 100644
--- a/crates/secd/Cargo.toml
+++ b/crates/secd/Cargo.toml
@@ -6,13 +6,16 @@ edition = "2021"
[dependencies]
async-std = { version = "1.12.0", features = [ "attributes" ] }
async-trait = "0.1"
+anyhow = "1.0"
base64 = "0.13.1"
+clap = { version = "4.0.29", features = ["derive"] }
derive_more = "0.99"
email_address = "0.2"
lazy_static = "1.4"
log = "0.4"
openssl = "0.10.42"
rand = "0.8"
+reqwest = { version = "0.11.13", features = ["json"] }
serde = "1"
serde_json = { version = "1.0", features = ["raw_value"] }
strum = "0.24.1"
@@ -20,4 +23,5 @@ strum_macros = "0.24"
sqlx = { version = "0.6", features = [ "runtime-async-std-native-tls", "postgres", "uuid", "sqlite", "time" ] }
time = { version = "0.3", features = [ "serde" ] }
thiserror = "1.0"
+url = "2.3.1"
uuid = { version = "1.2", features = ["v4", "serde"]} \ No newline at end of file
diff --git a/crates/secd/src/client/mod.rs b/crates/secd/src/client/mod.rs
index 3925657..38426ef 100644
--- a/crates/secd/src/client/mod.rs
+++ b/crates/secd/src/client/mod.rs
@@ -1,13 +1,24 @@
-pub mod email;
-pub mod sqldb;
+pub(crate) mod email;
+pub(crate) mod sqldb;
+pub(crate) mod types;
-use std::collections::HashMap;
+use std::{collections::HashMap, str::FromStr};
use super::Identity;
-use crate::{EmailValidation, Session, SessionSecret};
+use crate::{
+ EmailValidation, OauthProvider, OauthProviderName, OauthResponseType, OauthValidation, Session,
+ SessionSecret, ValidationRequestId, ValidationType,
+};
+use email_address::EmailAddress;
use lazy_static::lazy_static;
+use sqlx::{
+ database::HasValueRef, sqlite::SqliteRow, ColumnIndex, Database, Decode, FromRow, Row, Sqlite,
+ Type,
+};
use thiserror::Error;
+use time::OffsetDateTime;
+use url::Url;
use uuid::Uuid;
pub enum EmailType {
@@ -36,13 +47,15 @@ pub trait EmailMessenger {
#[derive(Error, Debug, derive_more::Display)]
pub enum StoreError {
SqlxError(#[from] sqlx::Error),
- EmailAlreadyExists,
CodeAppearsMoreThanOnce,
CodeDoesNotExist(String),
IdentityIdMustExistInvariant,
- TooManyEmailValidations,
+ TooManyValidations,
+ TooManyIdentitiesFound,
NoEmailValidationFound,
- Unknown,
+ OauthProviderDoesNotExist(OauthProviderName),
+ OauthValidationDoesNotExist(ValidationRequestId),
+ Other(String),
}
const EMAIL_TEMPLATE_DEFAULT_LOGIN: &str = "You requested a login link. Please click the following link %secd_code% to login as %secd_email_address%";
@@ -56,6 +69,7 @@ const PGSQL: &str = "pgsql";
const WRITE_IDENTITY: &str = "write_identity";
const WRITE_EMAIL_VALIDATION: &str = "write_email_validation";
const FIND_EMAIL_VALIDATION: &str = "find_email_validation";
+const READ_VALIDATION_TYPE: &str = "read_validation_type";
const WRITE_EMAIL: &str = "write_email";
@@ -69,6 +83,11 @@ const READ_EMAIL_RAW_ID: &str = "read_email_raw_id";
const WRITE_SESSION: &str = "write_session";
const READ_SESSION: &str = "read_session";
+const WRITE_OAUTH_PROVIDER: &str = "write_oauth_provider";
+const READ_OAUTH_PROVIDER: &str = "read_oauth_provider";
+const WRITE_OAUTH_VALIDATION: &str = "write_oauth_validation";
+const READ_OAUTH_VALIDATION: &str = "read_oauth_validation";
+
lazy_static! {
static ref SQLS: HashMap<&'static str, HashMap<&'static str, &'static str>> = {
let sqlite_sqls: HashMap<&'static str, &'static str> = [
@@ -116,6 +135,26 @@ lazy_static! {
FIND_EMAIL_VALIDATION,
include_str!("../../store/sqlite/sql/find_email_validation.sql"),
),
+ (
+ WRITE_OAUTH_PROVIDER,
+ include_str!("../../store/sqlite/sql/write_oauth_provider.sql"),
+ ),
+ (
+ READ_OAUTH_PROVIDER,
+ include_str!("../../store/sqlite/sql/read_oauth_provider.sql"),
+ ),
+ (
+ READ_OAUTH_VALIDATION,
+ include_str!("../../store/sqlite/sql/read_oauth_validation.sql"),
+ ),
+ (
+ WRITE_OAUTH_VALIDATION,
+ include_str!("../../store/sqlite/sql/write_oauth_validation.sql"),
+ ),
+ (
+ READ_VALIDATION_TYPE,
+ include_str!("../../store/sqlite/sql/read_validation_type.sql"),
+ ),
]
.iter()
.cloned()
@@ -166,6 +205,26 @@ lazy_static! {
FIND_EMAIL_VALIDATION,
include_str!("../../store/pg/sql/find_email_validation.sql"),
),
+ (
+ WRITE_OAUTH_PROVIDER,
+ include_str!("../../store/pg/sql/write_oauth_provider.sql"),
+ ),
+ (
+ READ_OAUTH_PROVIDER,
+ include_str!("../../store/pg/sql/read_oauth_provider.sql"),
+ ),
+ (
+ READ_OAUTH_VALIDATION,
+ include_str!("../../store/pg/sql/read_oauth_validation.sql"),
+ ),
+ (
+ WRITE_OAUTH_VALIDATION,
+ include_str!("../../store/pg/sql/write_oauth_validation.sql"),
+ ),
+ (
+ READ_VALIDATION_TYPE,
+ include_str!("../../store/pg/sql/read_validation_type.sql"),
+ ),
]
.iter()
.cloned()
@@ -180,9 +239,143 @@ lazy_static! {
};
}
+impl<'a, R: Row> FromRow<'a, R> for OauthValidation
+where
+ &'a str: ColumnIndex<R>,
+ OauthProviderName: Decode<'a, R::Database> + Type<R::Database>,
+ OauthResponseType: Decode<'a, R::Database> + Type<R::Database>,
+ OffsetDateTime: Decode<'a, R::Database> + Type<R::Database>,
+ String: Decode<'a, R::Database> + Type<R::Database>,
+ Uuid: Decode<'a, R::Database> + Type<R::Database>,
+{
+ fn from_row(row: &'a R) -> Result<Self, sqlx::Error> {
+ let id: Option<Uuid> = row.try_get("oauth_validation_public_id")?;
+ let identity_id: Option<Uuid> = row.try_get("identity_public_id")?;
+ let access_token: Option<String> = row.try_get("access_token")?;
+ let raw_response: Option<String> = row.try_get("raw_response")?;
+ let created_at: Option<OffsetDateTime> = row.try_get("created_at")?;
+ let validated_at: Option<OffsetDateTime> = row.try_get("validated_at")?;
+ let revoked_at: Option<OffsetDateTime> = row.try_get("revoked_at")?;
+ let deleted_at: Option<OffsetDateTime> = row.try_get("deleted_at")?;
+
+ let op_name: Option<OauthProviderName> = row.try_get("oauth_provider_name")?;
+ let op_flow: Option<String> = row.try_get("oauth_provider_flow")?;
+ let op_base_url: Option<String> = row.try_get("oauth_provider_base_url")?;
+ let op_response_type: Option<OauthResponseType> =
+ row.try_get("oauth_provider_response_type")?;
+ let op_default_scope: Option<String> = row.try_get("oauth_provider_default_scope")?;
+ let op_client_id: Option<String> = row.try_get("oauth_provider_client_id")?;
+ let op_client_secret: Option<String> = row.try_get("oauth_provider_client_secret")?;
+ let op_redirect_url: Option<String> = row.try_get("oauth_provider_redirect_url")?;
+ let op_created_at: Option<OffsetDateTime> = row.try_get("oauth_provider_created_at")?;
+ let op_deleted_at: Option<OffsetDateTime> = row.try_get("oauth_provider_deleted_at")?;
+
+ let op_base_url = op_base_url
+ .map(|s| Url::from_str(&s).ok())
+ .flatten()
+ .ok_or(sqlx::Error::ColumnDecode {
+ index: "oauth_provider_base_url".into(),
+ source: "secd".into(),
+ })?;
+
+ let op_redirect_url = op_redirect_url
+ .map(|s| Url::from_str(&s).ok())
+ .flatten()
+ .ok_or(sqlx::Error::ColumnDecode {
+ index: "oauth_provider_redirect_url".into(),
+ source: "secd".into(),
+ })?;
+
+ Ok(OauthValidation {
+ id,
+ identity_id,
+ access_token,
+ raw_response,
+ created_at: created_at.ok_or(sqlx::Error::ColumnDecode {
+ index: "created_at".into(),
+ source: "secd".into(),
+ })?,
+ validated_at,
+ revoked_at,
+ deleted_at,
+ oauth_provider: OauthProvider {
+ name: op_name.unwrap(),
+ flow: op_flow,
+ base_url: op_base_url,
+ response: op_response_type.ok_or(sqlx::Error::ColumnDecode {
+ index: "oauth_provider_response_type".into(),
+ source: "secd".into(),
+ })?,
+ default_scope: op_default_scope.ok_or(sqlx::Error::ColumnDecode {
+ index: "oauth_provider_default_scope".into(),
+ source: "secd".into(),
+ })?,
+ client_id: op_client_id.ok_or(sqlx::Error::ColumnDecode {
+ index: "oauth_provider_client_id".into(),
+ source: "secd".into(),
+ })?,
+ client_secret: op_client_secret.ok_or(sqlx::Error::ColumnDecode {
+ index: "oauth_provider_client_secret".into(),
+ source: "secd".into(),
+ })?,
+ redirect_url: op_redirect_url,
+ created_at: op_created_at.ok_or(sqlx::Error::ColumnDecode {
+ index: "oauth_provider_created_at".into(),
+ source: "secd".into(),
+ })?,
+ deleted_at: op_deleted_at,
+ },
+ })
+ }
+}
+
+impl<'a, D: Database> Decode<'a, D> for OauthProviderName
+where
+ &'a str: Decode<'a, D>,
+{
+ fn decode(
+ value: <D as HasValueRef<'a>>::ValueRef,
+ ) -> Result<Self, Box<dyn ::std::error::Error + 'static + Send + Sync>> {
+ let v = <&str as Decode<D>>::decode(value)?;
+ <OauthProviderName as clap::ValueEnum>::from_str(v, true)
+ .map_err(|_| "OauthProviderName should exist and decode to a program value.".into())
+ }
+}
+
+impl<D: Database> Type<D> for OauthProviderName
+where
+ str: Type<D>,
+{
+ fn type_info() -> D::TypeInfo {
+ <&str as Type<D>>::type_info()
+ }
+}
+
+impl<'a, D: Database> Decode<'a, D> for OauthResponseType
+where
+ &'a str: Decode<'a, D>,
+{
+ fn decode(
+ value: <D as HasValueRef<'a>>::ValueRef,
+ ) -> Result<Self, Box<dyn ::std::error::Error + 'static + Send + Sync>> {
+ let v = <&str as Decode<D>>::decode(value)?;
+ <OauthResponseType as clap::ValueEnum>::from_str(v, true)
+ .map_err(|_| "OauthResponseType should exist and decode to a program value.".into())
+ }
+}
+
+impl<D: Database> Type<D> for OauthResponseType
+where
+ str: Type<D>,
+{
+ fn type_info() -> D::TypeInfo {
+ <&str as Type<D>>::type_info()
+ }
+}
+
#[async_trait::async_trait]
pub trait Store {
- async fn write_email(&self, identity_id: Uuid, email_address: &str) -> Result<(), StoreError>;
+ async fn write_email(&self, email_address: &str) -> Result<(), StoreError>;
async fn find_email_validation(
&self,
@@ -193,17 +386,37 @@ pub trait Store {
&self,
ev: &EmailValidation,
// TODO: Make this write an EmailValidation
- ) -> Result<Uuid, StoreError>;
+ ) -> anyhow::Result<Uuid>;
async fn find_identity(
&self,
identity_id: Option<&Uuid>,
email: Option<&str>,
- ) -> Result<Option<Identity>, StoreError>;
+ ) -> anyhow::Result<Option<Identity>>;
async fn find_identity_by_code(&self, code: &str) -> Result<Identity, StoreError>;
async fn write_identity(&self, i: &Identity) -> Result<(), StoreError>;
async fn read_identity(&self, identity_id: &Uuid) -> Result<Identity, StoreError>;
async fn write_session(&self, session: &Session) -> Result<(), StoreError>;
async fn read_session(&self, secret: &SessionSecret) -> Result<Session, StoreError>;
+
+ async fn write_oauth_provider(&self, provider: &OauthProvider) -> Result<(), StoreError>;
+ async fn read_oauth_provider(
+ &self,
+ provider: &OauthProviderName,
+ flow: Option<String>,
+ ) -> Result<OauthProvider, StoreError>;
+ async fn write_oauth_validation(
+ &self,
+ validation: &OauthValidation,
+ ) -> anyhow::Result<ValidationRequestId>;
+ async fn read_oauth_validation(
+ &self,
+ validation_id: &ValidationRequestId,
+ ) -> anyhow::Result<OauthValidation>;
+
+ async fn find_validation_type(
+ &self,
+ validation_id: &ValidationRequestId,
+ ) -> anyhow::Result<ValidationType>;
}
diff --git a/crates/secd/src/client/sqldb.rs b/crates/secd/src/client/sqldb.rs
index 6048c48..15cc4b5 100644
--- a/crates/secd/src/client/sqldb.rs
+++ b/crates/secd/src/client/sqldb.rs
@@ -1,19 +1,23 @@
-use std::sync::Arc;
+use std::{str::FromStr, sync::Arc};
use super::{
- EmailValidation, Identity, Session, SessionSecret, Store, StoreError, ERR_MSG_MIGRATION_FAILED,
- FIND_EMAIL_VALIDATION, FIND_IDENTITY, FIND_IDENTITY_BY_CODE, PGSQL, READ_EMAIL_RAW_ID,
- READ_IDENTITY_RAW_ID, READ_SESSION, SQLITE, SQLS, WRITE_EMAIL, WRITE_EMAIL_VALIDATION,
- WRITE_IDENTITY, WRITE_SESSION,
+ EmailValidation, Identity, OauthProvider, OauthProviderName, OauthResponseType, Session,
+ SessionSecret, Store, StoreError, ERR_MSG_MIGRATION_FAILED, FIND_EMAIL_VALIDATION,
+ FIND_IDENTITY, FIND_IDENTITY_BY_CODE, PGSQL, READ_EMAIL_RAW_ID, READ_IDENTITY_RAW_ID,
+ READ_OAUTH_PROVIDER, READ_OAUTH_VALIDATION, READ_SESSION, READ_VALIDATION_TYPE, SQLITE, SQLS,
+ WRITE_EMAIL, WRITE_EMAIL_VALIDATION, WRITE_IDENTITY, WRITE_OAUTH_PROVIDER,
+ WRITE_OAUTH_VALIDATION, WRITE_SESSION,
};
-use crate::util;
-use log::error;
+use crate::{util, OauthValidation, ValidationRequestId, ValidationType};
+use anyhow::bail;
+use log::{debug, error};
use openssl::sha::Sha256;
use sqlx::{
self, database::HasArguments, ColumnIndex, Database, Decode, Encode, Executor, IntoArguments,
Pool, Postgres, Sqlite, Transaction, Type,
};
use time::OffsetDateTime;
+use url::Url;
use uuid::Uuid;
fn get_sqls(root: &str, file: &str) -> Vec<String> {
@@ -97,6 +101,8 @@ where
for<'c> String: Encode<'c, D> + Type<D>,
for<'c> Option<String>: Decode<'c, D> + Type<D>,
for<'c> Option<String>: Encode<'c, D> + Type<D>,
+ for<'c> OauthProviderName: Decode<'c, D> + Type<D>,
+ for<'c> OauthResponseType: Decode<'c, D> + Type<D>,
for<'c> usize: ColumnIndex<<D as Database>::Row>,
for<'c> Uuid: Decode<'c, D> + Type<D>,
for<'c> Uuid: Encode<'c, D> + Type<D>,
@@ -108,29 +114,11 @@ where
for<'c> &'c Pool<D>: Executor<'c, Database = D>,
for<'c> &'c mut Transaction<'c, D>: Executor<'c, Database = D>,
{
- async fn write_email(&self, identity_id: Uuid, email_address: &str) -> Result<(), StoreError> {
+ async fn write_email(&self, email_address: &str) -> Result<(), StoreError> {
let sqls = get_sqls(&self.sqls_root, WRITE_EMAIL);
- let identity_id = self.read_identity_raw_id(&identity_id).await?;
-
- let email_id: (i64,) = match sqlx::query_as(&sqls[0])
+ sqlx::query(&sqls[0])
.bind(email_address)
- .fetch_one(&self.pool)
- .await
- {
- Ok(i) => i,
- Err(sqlx::Error::RowNotFound) => sqlx::query_as::<_, (i64,)>(&sqls[1])
- .bind(email_address)
- .fetch_one(&self.pool)
- .await
- .map_err(util::log_err_sqlx)?,
- Err(e) => return Err(StoreError::SqlxError(e)),
- };
-
- sqlx::query(&sqls[2])
- .bind(identity_id)
- .bind(email_id.0)
- .bind(OffsetDateTime::now_utc())
.execute(&self.pool)
.await
.map_err(util::log_err_sqlx)?;
@@ -154,57 +142,84 @@ where
match rows.len() {
0 => Err(StoreError::NoEmailValidationFound),
1 => Ok(rows.swap_remove(0)),
- _ => Err(StoreError::TooManyEmailValidations),
+ _ => Err(StoreError::TooManyValidations),
}
}
- async fn write_email_validation(&self, ev: &EmailValidation) -> Result<Uuid, StoreError> {
+ async fn write_email_validation(&self, ev: &EmailValidation) -> anyhow::Result<Uuid> {
let sqls = get_sqls(&self.sqls_root, WRITE_EMAIL_VALIDATION);
- let identity_id = self
- .read_identity_raw_id(
- &ev.identity_id
- .ok_or(StoreError::IdentityIdMustExistInvariant)?,
- )
- .await?;
let email_id = self.read_email_raw_id(&ev.email_address).await?;
-
- let new_id = Uuid::new_v4();
+ let validation_id = ev.id.unwrap_or(Uuid::new_v4());
sqlx::query(&sqls[0])
- .bind(ev.id.unwrap_or(new_id))
- .bind(identity_id)
+ .bind(validation_id)
.bind(email_id)
- .bind(ev.attempts)
.bind(&ev.code)
- .bind(ev.is_validated)
+ .bind(ev.is_oauth_derived)
.bind(ev.created_at)
- .bind(ev.expires_at)
+ .bind(ev.validated_at)
+ .bind(ev.expired_at)
.execute(&self.pool)
.await
.map_err(util::log_err_sqlx)?;
- Ok(new_id)
+ if ev.identity_id.is_some() || ev.revoked_at.is_some() || ev.deleted_at.is_some() {
+ sqlx::query(&sqls[1])
+ .bind(ev.identity_id.as_ref())
+ .bind(validation_id)
+ .bind(ev.revoked_at)
+ .bind(ev.deleted_at)
+ .execute(&self.pool)
+ .await
+ .map_err(util::log_err_sqlx)?;
+ }
+
+ Ok(validation_id)
}
async fn find_identity(
&self,
id: Option<&Uuid>,
email: Option<&str>,
- ) -> Result<Option<Identity>, StoreError> {
+ ) -> anyhow::Result<Option<Identity>> {
let sqls = get_sqls(&self.sqls_root, FIND_IDENTITY);
Ok(
match sqlx::query_as::<_, Identity>(&sqls[0])
.bind(id)
.bind(email)
- .fetch_one(&self.pool)
+ .fetch_all(&self.pool)
.await
{
- Ok(i) => Some(i),
+ Ok(mut is) => match is.len() {
+ // if only 1 found, then that's fine
+ // if multiple are fond, then if they all have the same id, that's okay
+ 1 => {
+ let i = is.swap_remove(0);
+ match i.deleted_at {
+ Some(t) if t > OffsetDateTime::now_utc() => Some(i),
+ None => Some(i),
+ _ => None,
+ }
+ }
+ 0 => None,
+ _ => {
+ match is
+ .iter()
+ .filter(|&i| i.id != is[0].id)
+ .collect::<Vec<&Identity>>()
+ .len()
+ {
+ 0 => Some(is.swap_remove(0)),
+ _ => bail!(StoreError::TooManyIdentitiesFound),
+ }
+ }
+ },
Err(sqlx::Error::RowNotFound) => None,
- Err(e) => return Err(StoreError::SqlxError(e)),
+ Err(e) => bail!(StoreError::SqlxError(e)),
},
)
}
+
async fn find_identity_by_code(&self, code: &str) -> Result<Identity, StoreError> {
let sqls = get_sqls(&self.sqls_root, FIND_IDENTITY_BY_CODE);
@@ -250,14 +265,16 @@ where
Ok(())
}
async fn read_identity(&self, id: &Uuid) -> Result<Identity, StoreError> {
- Ok(sqlx::query_as::<_, Identity>(
+ let identity = sqlx::query_as::<_, Identity>(
"
select identity_public_id, data, created_at from identity where identity_public_id = ?",
)
.bind(id)
.fetch_one(&self.pool)
.await
- .map_err(util::log_err_sqlx)?)
+ .map_err(util::log_err_sqlx)?;
+
+ Ok(identity)
}
async fn write_session(&self, session: &Session) -> Result<(), StoreError> {
@@ -269,7 +286,6 @@ select identity_public_id, data, created_at from identity where identity_public_
.bind(&session.identity_id)
.bind(secret_hash.as_ref())
.bind(session.created_at)
- .bind(OffsetDateTime::now_utc())
.bind(session.expires_at)
.bind(session.revoked_at)
.execute(&self.pool)
@@ -296,6 +312,142 @@ select identity_public_id, data, created_at from identity where identity_public_
Ok(session)
}
+
+ async fn write_oauth_provider(&self, provider: &OauthProvider) -> Result<(), StoreError> {
+ let sqls = get_sqls(&self.sqls_root, WRITE_OAUTH_PROVIDER);
+ sqlx::query(&sqls[0])
+ .bind(&provider.name.to_string())
+ .bind(&provider.flow)
+ .bind(&provider.base_url.to_string())
+ .bind(&provider.response.to_string())
+ .bind(&provider.default_scope)
+ .bind(&provider.client_id)
+ // TODO: encrypt secret before writing
+ .bind(&provider.client_secret)
+ .bind(&provider.redirect_url.to_string())
+ .bind(provider.created_at)
+ .bind(provider.deleted_at)
+ .execute(&self.pool)
+ .await
+ .map_err(util::log_err_sqlx)?;
+ Ok(())
+ }
+
+ async fn read_oauth_provider(
+ &self,
+ provider: &OauthProviderName,
+ flow: Option<String>,
+ ) -> Result<OauthProvider, StoreError> {
+ let sqls = get_sqls(&self.sqls_root, READ_OAUTH_PROVIDER);
+ let flow = flow.unwrap_or("default".into());
+ debug!("provider: {:?}, flow: {:?}", provider, flow);
+ // TODO: Write the generic FromRow impl for OauthProvider...
+ let res = sqlx::query_as::<
+ _,
+ (
+ String,
+ String,
+ String,
+ String,
+ String,
+ String,
+ String,
+ OffsetDateTime,
+ Option<OffsetDateTime>,
+ ),
+ >(&sqls[0])
+ .bind(&provider.to_string())
+ .bind(&flow)
+ .fetch_one(&self.pool)
+ .await
+ .map_err(util::log_err_sqlx)?;
+
+ debug!("res: {:?}", res);
+
+ Ok(OauthProvider {
+ name: provider.clone(),
+ flow: Some(res.0),
+ base_url: Url::from_str(&res.1)
+ .map_err(|_| StoreError::OauthProviderDoesNotExist(*provider))?,
+ response: OauthResponseType::from_str(&res.2)
+ .map_err(|_| StoreError::OauthProviderDoesNotExist(*provider))?,
+ default_scope: res.3,
+ client_id: res.4,
+ client_secret: res.5,
+ redirect_url: Url::from_str(&res.6)
+ .map_err(|_| StoreError::OauthProviderDoesNotExist(*provider))?,
+ created_at: res.7,
+ deleted_at: res.8,
+ })
+ }
+ async fn write_oauth_validation(
+ &self,
+ v: &OauthValidation,
+ ) -> anyhow::Result<ValidationRequestId> {
+ let sqls = get_sqls(&self.sqls_root, WRITE_OAUTH_VALIDATION);
+
+ let validation_id = v.id.unwrap_or(Uuid::new_v4());
+ sqlx::query(&sqls[0])
+ .bind(validation_id)
+ .bind(v.oauth_provider.name.to_string())
+ .bind(v.oauth_provider.flow.clone())
+ .bind(v.access_token.clone())
+ .bind(v.raw_response.clone())
+ .bind(v.created_at)
+ .bind(v.validated_at)
+ .execute(&self.pool)
+ .await?;
+
+ if v.identity_id.is_some() || v.revoked_at.is_some() || v.deleted_at.is_some() {
+ sqlx::query(&sqls[1])
+ .bind(v.identity_id.as_ref())
+ .bind(validation_id)
+ .bind(v.revoked_at)
+ .bind(v.deleted_at)
+ .execute(&self.pool)
+ .await?;
+ }
+
+ Ok(validation_id)
+ }
+ async fn read_oauth_validation(
+ &self,
+ validation_id: &ValidationRequestId,
+ ) -> anyhow::Result<OauthValidation> {
+ let sqls = get_sqls(&self.sqls_root, READ_OAUTH_VALIDATION);
+
+ let mut es = sqlx::query_as::<_, OauthValidation>(&sqls[0])
+ .bind(validation_id)
+ .fetch_all(&self.pool)
+ .await?;
+
+ if es.len() != 1 {
+ bail!(StoreError::OauthValidationDoesNotExist(
+ validation_id.clone()
+ ));
+ }
+
+ Ok(es.swap_remove(0))
+ }
+ async fn find_validation_type(
+ &self,
+ validation_id: &ValidationRequestId,
+ ) -> anyhow::Result<ValidationType> {
+ let sqls = get_sqls(&self.sqls_root, READ_VALIDATION_TYPE);
+
+ let mut es = sqlx::query_as::<_, (String,)>(&sqls[0])
+ .bind(validation_id)
+ .fetch_all(&self.pool)
+ .await
+ .map_err(util::log_err_sqlx)?;
+
+ match es.len() {
+ 1 => Ok(ValidationType::from_str(&es.swap_remove(0).0)?),
+ _ => bail!(StoreError::Other(
+ "expected a single validation but recieved 0 or multiple validations".into()
+ )),
+ }
+ }
}
pub struct PgClient {
@@ -320,8 +472,8 @@ impl PgClient {
#[async_trait::async_trait]
impl Store for PgClient {
- async fn write_email(&self, identity_id: Uuid, email_address: &str) -> Result<(), StoreError> {
- self.sql.write_email(identity_id, email_address).await
+ async fn write_email(&self, email_address: &str) -> Result<(), StoreError> {
+ self.sql.write_email(email_address).await
}
async fn find_email_validation(
&self,
@@ -330,14 +482,14 @@ impl Store for PgClient {
) -> Result<EmailValidation, StoreError> {
self.sql.find_email_validation(validation_id, code).await
}
- async fn write_email_validation(&self, ev: &EmailValidation) -> Result<Uuid, StoreError> {
+ async fn write_email_validation(&self, ev: &EmailValidation) -> anyhow::Result<Uuid> {
self.sql.write_email_validation(ev).await
}
async fn find_identity(
&self,
identity_id: Option<&Uuid>,
email: Option<&str>,
- ) -> Result<Option<Identity>, StoreError> {
+ ) -> anyhow::Result<Option<Identity>> {
self.sql.find_identity(identity_id, email).await
}
async fn find_identity_by_code(&self, code: &str) -> Result<Identity, StoreError> {
@@ -355,6 +507,34 @@ impl Store for PgClient {
async fn read_session(&self, secret: &SessionSecret) -> Result<Session, StoreError> {
self.sql.read_session(secret).await
}
+ async fn write_oauth_provider(&self, provider: &OauthProvider) -> Result<(), StoreError> {
+ self.sql.write_oauth_provider(provider).await
+ }
+ async fn read_oauth_provider(
+ &self,
+ provider: &OauthProviderName,
+ flow: Option<String>,
+ ) -> Result<OauthProvider, StoreError> {
+ self.sql.read_oauth_provider(provider, flow).await
+ }
+ async fn write_oauth_validation(
+ &self,
+ validation: &OauthValidation,
+ ) -> anyhow::Result<ValidationRequestId> {
+ self.sql.write_oauth_validation(validation).await
+ }
+ async fn read_oauth_validation(
+ &self,
+ validation_id: &ValidationRequestId,
+ ) -> anyhow::Result<OauthValidation> {
+ self.sql.read_oauth_validation(validation_id).await
+ }
+ async fn find_validation_type(
+ &self,
+ validation_id: &ValidationRequestId,
+ ) -> anyhow::Result<ValidationType> {
+ self.sql.find_validation_type(validation_id).await
+ }
}
pub struct SqliteClient {
@@ -386,8 +566,8 @@ impl SqliteClient {
#[async_trait::async_trait]
impl Store for SqliteClient {
- async fn write_email(&self, identity_id: Uuid, email_address: &str) -> Result<(), StoreError> {
- self.sql.write_email(identity_id, email_address).await
+ async fn write_email(&self, email_address: &str) -> Result<(), StoreError> {
+ self.sql.write_email(email_address).await
}
async fn find_email_validation(
&self,
@@ -396,14 +576,14 @@ impl Store for SqliteClient {
) -> Result<EmailValidation, StoreError> {
self.sql.find_email_validation(validation_id, code).await
}
- async fn write_email_validation(&self, ev: &EmailValidation) -> Result<Uuid, StoreError> {
+ async fn write_email_validation(&self, ev: &EmailValidation) -> anyhow::Result<Uuid> {
self.sql.write_email_validation(ev).await
}
async fn find_identity(
&self,
identity_id: Option<&Uuid>,
email: Option<&str>,
- ) -> Result<Option<Identity>, StoreError> {
+ ) -> anyhow::Result<Option<Identity>> {
self.sql.find_identity(identity_id, email).await
}
async fn find_identity_by_code(&self, code: &str) -> Result<Identity, StoreError> {
@@ -421,4 +601,32 @@ impl Store for SqliteClient {
async fn read_session(&self, secret: &SessionSecret) -> Result<Session, StoreError> {
self.sql.read_session(secret).await
}
+ async fn write_oauth_provider(&self, provider: &OauthProvider) -> Result<(), StoreError> {
+ self.sql.write_oauth_provider(provider).await
+ }
+ async fn read_oauth_provider(
+ &self,
+ provider: &OauthProviderName,
+ flow: Option<String>,
+ ) -> Result<OauthProvider, StoreError> {
+ self.sql.read_oauth_provider(provider, flow).await
+ }
+ async fn write_oauth_validation(
+ &self,
+ validation: &OauthValidation,
+ ) -> anyhow::Result<ValidationRequestId> {
+ self.sql.write_oauth_validation(validation).await
+ }
+ async fn read_oauth_validation(
+ &self,
+ validation_id: &ValidationRequestId,
+ ) -> anyhow::Result<OauthValidation> {
+ self.sql.read_oauth_validation(validation_id).await
+ }
+ async fn find_validation_type(
+ &self,
+ validation_id: &ValidationRequestId,
+ ) -> anyhow::Result<ValidationType> {
+ self.sql.find_validation_type(validation_id).await
+ }
}
diff --git a/crates/secd/src/client/types.rs b/crates/secd/src/client/types.rs
new file mode 100644
index 0000000..bacade4
--- /dev/null
+++ b/crates/secd/src/client/types.rs
@@ -0,0 +1,3 @@
+pub(crate) struct Email {
+ address: String,
+}
diff --git a/crates/secd/src/command/admin.rs b/crates/secd/src/command/admin.rs
new file mode 100644
index 0000000..b04dbef
--- /dev/null
+++ b/crates/secd/src/command/admin.rs
@@ -0,0 +1,57 @@
+use std::str::FromStr;
+
+use time::OffsetDateTime;
+use url::Url;
+
+use crate::{OauthProviderName, Secd, SecdError};
+
+impl OauthProviderName {
+ fn base_url(&self) -> Url {
+ match self {
+ OauthProviderName::Google => {
+ Url::from_str("https://accounts.google.com/o/oauth2/v2/auth").unwrap()
+ }
+ OauthProviderName::Microsoft => {
+ Url::from_str("https://login.microsoftonline.com/common/oauth2/v2.0/authorize")
+ .unwrap()
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn default_scope(&self) -> String {
+ match self {
+ OauthProviderName::Google => "openid%20email".into(),
+ OauthProviderName::Microsoft => "openid%20email".into(),
+ _ => unimplemented!(),
+ }
+ }
+}
+
+impl Secd {
+ pub async fn create_oauth_provider(
+ &self,
+ provider: &OauthProviderName,
+ client_id: String,
+ client_secret: String,
+ redirect_url: Url,
+ ) -> Result<(), SecdError> {
+ self.store
+ .write_oauth_provider(&crate::OauthProvider {
+ name: provider.clone(),
+ flow: Some("default".into()),
+ base_url: provider.base_url(),
+ response: crate::OauthResponseType::Code,
+ default_scope: provider.default_scope(),
+ client_id,
+ client_secret,
+ redirect_url,
+ created_at: OffsetDateTime::now_utc(),
+ deleted_at: None,
+ })
+ .await
+ .map_err(|_| SecdError::Todo)?;
+
+ Ok(())
+ }
+}
diff --git a/crates/secd/src/command/authn.rs b/crates/secd/src/command/authn.rs
new file mode 100644
index 0000000..862d921
--- /dev/null
+++ b/crates/secd/src/command/authn.rs
@@ -0,0 +1,230 @@
+use email_address::EmailAddress;
+use log::debug;
+use rand::distributions::{Alphanumeric, DistString};
+use time::Duration;
+use time::OffsetDateTime;
+use uuid::Uuid;
+
+use crate::util::{build_oauth_auth_url, get_oauth_access_token};
+use crate::OauthRedirectAuthUrl;
+use crate::Validation;
+use crate::ValidationType;
+use crate::INTERNAL_ERR_MSG;
+use crate::{
+ client, util, EmailValidation, Identity, OauthProviderName, Secd, SecdError, Session,
+ ValidationRequestId, ValidationSecretCode, EMAIL_VALIDATION_DURATION, SESSION_DURATION,
+ SESSION_SIZE_BYTES, VALIDATION_CODE_SIZE,
+};
+
+impl Secd {
+ /// create_validation_request_oauth
+ ///
+ /// Generate a request to validate with the specified oauth provider.[
+ // TODO: How to handle different oauth "flows"? e.g. web app vs desktop vs mobile...
+ pub async fn create_validation_request_oauth(
+ &self,
+ provider: &OauthProviderName,
+ scope: Option<String>,
+ ) -> Result<OauthRedirectAuthUrl, SecdError> {
+ if scope.is_some() {
+ return Err(SecdError::NotImplemented(
+ "Only default scopes are currently supported.".into(),
+ ));
+ }
+
+ let p = self
+ .store
+ .read_oauth_provider(provider, None)
+ .await
+ .map_err(|_| SecdError::InternalError(INTERNAL_ERR_MSG.to_string()))?;
+
+ let req_id = self
+ .store
+ .write_oauth_validation(&crate::OauthValidation {
+ id: Some(Uuid::new_v4()),
+ identity_id: None,
+ oauth_provider: p.clone(),
+ access_token: None,
+ raw_response: None,
+ created_at: OffsetDateTime::now_utc(),
+ validated_at: None,
+ revoked_at: None,
+ deleted_at: None,
+ })
+ .await
+ .map_err(|e| util::to_secd_err(e, SecdError::OauthValidationRequestError))?;
+
+ build_oauth_auth_url(&p, req_id)
+ }
+ /// create_validation_request_email
+ ///
+ /// Generate a request to validate the provided email.
+ pub async fn create_validation_request_email(
+ &self,
+ email: Option<&str>,
+ ) -> Result<ValidationRequestId, SecdError> {
+ let now = OffsetDateTime::now_utc();
+
+ let email = match email {
+ Some(ea) => {
+ if EmailAddress::is_valid(ea) {
+ ea
+ } else {
+ return Err(SecdError::InvalidEmailAddress);
+ }
+ }
+ None => return Err(SecdError::InvalidEmailAddress),
+ };
+
+ let mut ev = EmailValidation {
+ id: None,
+ identity_id: None,
+ email_address: email.to_string(),
+ code: Some(
+ Alphanumeric
+ .sample_string(&mut rand::thread_rng(), VALIDATION_CODE_SIZE)
+ .to_lowercase(),
+ ),
+ is_oauth_derived: false,
+ created_at: now,
+ expired_at: now
+ .checked_add(Duration::new(EMAIL_VALIDATION_DURATION, 0))
+ .ok_or(SecdError::EmailValidationExpiryOverflow)?,
+ validated_at: None,
+ revoked_at: None,
+ deleted_at: None,
+ };
+
+ let (req_id, mail_type) = match self
+ .store
+ .find_identity(None, Some(email))
+ .await
+ .map_err(|e| util::log_err(e.into(), SecdError::Todo))?
+ {
+ Some(identity) => {
+ let req_id = {
+ ev.identity_id = Some(identity.id);
+ self.store
+ .write_email_validation(&ev)
+ .await
+ .map_err(|e| util::log_err(e.into(), SecdError::Todo))?
+ };
+ (req_id, client::EmailType::Login)
+ }
+ None => {
+ self.store
+ .write_email(email)
+ .await
+ .map_err(|e| util::log_err(e.into(), SecdError::Todo))?;
+
+ let req_id = {
+ self.store
+ .write_email_validation(&ev)
+ .await
+ .map_err(|e| util::log_err(e.into(), SecdError::Todo))?
+ };
+
+ (req_id, client::EmailType::Signup)
+ }
+ };
+
+ self.email_messenger
+ .send_email(email, &req_id.to_string(), &ev.code.unwrap(), mail_type)
+ .await?;
+
+ Ok(req_id)
+ }
+ /// exchange_secret_for_session
+ ///
+ /// Exchanges a secret, which consists of a validation_request_id and secret_code
+ /// for a session which allows authentication on behalf of the associated identity.
+ ///
+ /// Session secrets should be used to return authorization for the associated identity.
+ pub async fn exchange_code_for_session(
+ &self,
+ validation_request_id: ValidationRequestId,
+ code: ValidationSecretCode,
+ ) -> Result<Session, SecdError> {
+ let mut v: Box<dyn Validation> = match self
+ .store
+ .find_validation_type(&validation_request_id)
+ .await
+ .map_err(|e| util::to_secd_err(e, SecdError::Todo))?
+ {
+ ValidationType::Email => Box::new(
+ self.store
+ .find_email_validation(Some(&validation_request_id), Some(&code))
+ .await
+ .map_err(|e| {
+ util::log_err(e.into(), SecdError::EmailValidationExpiryOverflow)
+ })?,
+ ),
+ ValidationType::Oauth => Box::new({
+ let mut t = self
+ .store
+ .read_oauth_validation(&validation_request_id)
+ .await
+ .map_err(|e| util::to_secd_err(e, SecdError::Todo))?;
+
+ let access_token = get_oauth_access_token(&t, &code)
+ .await
+ .map_err(|_| SecdError::Todo)?;
+
+ t.access_token = Some(access_token);
+ t
+ }),
+ };
+
+ if v.expired() || v.is_validated() {
+ return Err(SecdError::InvalidCode);
+ };
+
+ let mut identity = Identity {
+ id: Uuid::new_v4(),
+ data: None,
+ created_at: OffsetDateTime::now_utc(),
+ deleted_at: None,
+ };
+
+ match v
+ .find_associated_identities(self.store.clone())
+ .await
+ .map_err(|e| util::to_secd_err(e, SecdError::IdentityIdShouldExistInvariant))?
+ {
+ Some(i) => identity.id = i.id,
+ _ => self.store.write_identity(&identity).await.map_err(|_| {
+ SecdError::InternalError("failed to write identity during session exchange".into())
+ })?,
+ };
+
+ v.validate(&identity, self.store.clone())
+ .await
+ .map_err(|e| {
+ util::to_secd_err(
+ e,
+ SecdError::InternalError(
+ "failed to update validation during session exchange".into(),
+ ),
+ )
+ })?;
+
+ // TODO: clear previous sessions if they fit the criteria
+ let now = OffsetDateTime::now_utc();
+ let s = Session {
+ identity_id: identity.id,
+ secret: Some(Alphanumeric.sample_string(&mut rand::thread_rng(), SESSION_SIZE_BYTES)),
+ created_at: now,
+ expires_at: now
+ .checked_add(Duration::new(SESSION_DURATION, 0))
+ .ok_or(SecdError::SessionExpiryOverflow)?,
+ revoked_at: None,
+ };
+
+ self.store
+ .write_session(&s)
+ .await
+ .map_err(|e| util::log_err(e.into(), SecdError::Todo))?;
+
+ Ok(s)
+ }
+}
diff --git a/crates/secd/src/command/mod.rs b/crates/secd/src/command/mod.rs
new file mode 100644
index 0000000..cd0d8c3
--- /dev/null
+++ b/crates/secd/src/command/mod.rs
@@ -0,0 +1,66 @@
+pub mod admin;
+pub mod authn;
+
+use crate::client::{
+ email,
+ sqldb::{PgClient, SqliteClient},
+};
+use crate::{AuthEmail, AuthStore, Secd, SecdError};
+use log::error;
+use std::sync::Arc;
+
+impl Secd {
+ /// init
+ ///
+ /// Initialize SecD with the specified configuration, established the necessary
+ /// constraints, persistance stores, and options.
+ pub async fn init(
+ auth_store: AuthStore,
+ conn_string: Option<&str>,
+ email_messenger: AuthEmail,
+ email_template_login: Option<String>,
+ email_template_signup: Option<String>,
+ ) -> Result<Self, SecdError> {
+ let store = match auth_store {
+ AuthStore::Sqlite => {
+ SqliteClient::new(
+ sqlx::sqlite::SqlitePoolOptions::new()
+ .connect(conn_string.unwrap_or("sqlite::memory:".into()))
+ .await
+ .map_err(|e| SecdError::InitializationFailure(e))?,
+ )
+ .await
+ }
+ AuthStore::Postgres => {
+ PgClient::new(
+ sqlx::postgres::PgPoolOptions::new()
+ .connect(conn_string.expect("No postgres connection string provided."))
+ .await
+ .map_err(|e| SecdError::InitializationFailure(e))?,
+ )
+ .await
+ }
+ rest @ _ => {
+ error!(
+ "requested an AuthStore which has not yet been implemented: {:?}",
+ rest
+ );
+ unimplemented!()
+ }
+ };
+
+ let email_sender = match email_messenger {
+ // TODO: initialize email and SMS templates with secd
+ AuthEmail::LocalStub => email::LocalEmailStubber {
+ email_template_login,
+ email_template_signup,
+ },
+ _ => unimplemented!(),
+ };
+
+ Ok(Secd {
+ store,
+ email_messenger: Arc::new(email_sender),
+ })
+ }
+}
diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs
index 4feda04..faa92ca 100644
--- a/crates/secd/src/lib.rs
+++ b/crates/secd/src/lib.rs
@@ -1,28 +1,28 @@
mod client;
+mod command;
mod util;
use std::sync::Arc;
-use client::{
- email,
- sqldb::{PgClient, SqliteClient},
- EmailMessenger, EmailMessengerError, Store, StoreError,
-};
+use clap::ValueEnum;
+use client::{EmailMessenger, EmailMessengerError, Store};
use derive_more::Display;
use email_address::EmailAddress;
-use log::error;
-use rand::distributions::{Alphanumeric, DistString};
use serde::{Deserialize, Serialize};
+use sqlx::FromRow;
use strum_macros::{EnumString, EnumVariantNames};
-use time::{Duration, OffsetDateTime};
+use time::OffsetDateTime;
+use url::Url;
+use util::get_oauth_identity_data;
use uuid::Uuid;
const SESSION_SIZE_BYTES: usize = 32;
const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */;
const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */;
-const VALIDATION_ATTEMPTS_MAX: i32 = 5;
const VALIDATION_CODE_SIZE: usize = 6;
+const INTERNAL_ERR_MSG: &str = "It seems an invariant was borked or something non-deterministic happened. Please file a bug with secd.";
+
#[derive(sqlx::FromRow, Debug, Serialize)]
pub struct ApiKey {
pub public_key: String,
@@ -38,9 +38,11 @@ pub struct Authorization {
pub struct Identity {
#[sqlx(rename = "identity_public_id")]
id: Uuid,
- created_at: OffsetDateTime,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<String>,
+ created_at: OffsetDateTime,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ deleted_at: Option<OffsetDateTime>,
}
#[derive(sqlx::FromRow, Debug, Serialize)]
@@ -58,6 +60,121 @@ pub struct Session {
pub revoked_at: Option<OffsetDateTime>,
}
+#[async_trait::async_trait]
+trait Validation {
+ fn expired(&self) -> bool;
+ fn is_validated(&self) -> bool;
+ async fn find_associated_identities(
+ &self,
+ store: Arc<dyn Store + Send + Sync>,
+ ) -> anyhow::Result<Option<Identity>>;
+ async fn validate(
+ &mut self,
+ i: &Identity,
+ store: Arc<dyn Store + Send + Sync>,
+ ) -> anyhow::Result<()>;
+}
+
+#[async_trait::async_trait]
+impl Validation for EmailValidation {
+ fn expired(&self) -> bool {
+ let now = OffsetDateTime::now_utc();
+ self.expired_at < now
+ || self.revoked_at.map(|t| t < now).unwrap_or(false)
+ || self.deleted_at.map(|t| t < now).unwrap_or(false)
+ }
+ fn is_validated(&self) -> bool {
+ self.validated_at
+ .map(|t| t >= OffsetDateTime::now_utc())
+ .unwrap_or(false)
+ }
+ async fn find_associated_identities(
+ &self,
+ store: Arc<dyn Store + Send + Sync>,
+ ) -> anyhow::Result<Option<Identity>> {
+ store.find_identity(None, Some(&self.email_address)).await
+ }
+ async fn validate(
+ &mut self,
+ i: &Identity,
+ store: Arc<dyn Store + Send + Sync>,
+ ) -> anyhow::Result<()> {
+ self.identity_id = Some(i.id);
+ self.validated_at = Some(OffsetDateTime::now_utc());
+ store.write_email_validation(&self).await?;
+ Ok(())
+ }
+}
+
+#[async_trait::async_trait]
+impl Validation for OauthValidation {
+ fn expired(&self) -> bool {
+ let now = OffsetDateTime::now_utc();
+ self.revoked_at.map(|t| t < now).unwrap_or(false)
+ || self.deleted_at.map(|t| t < now).unwrap_or(false)
+ }
+ fn is_validated(&self) -> bool {
+ self.validated_at
+ .map(|t| t >= OffsetDateTime::now_utc())
+ .unwrap_or(false)
+ }
+ async fn find_associated_identities(
+ &self,
+ store: Arc<dyn Store + Send + Sync>,
+ ) -> anyhow::Result<Option<Identity>> {
+ let oauth_identity = get_oauth_identity_data(&self).await?;
+
+ let identity = store
+ .find_identity(None, oauth_identity.email.as_deref())
+ .await?;
+
+ let now = OffsetDateTime::now_utc();
+ if let Some(email) = oauth_identity.email.clone() {
+ let identity = identity.unwrap_or(Identity {
+ id: Uuid::new_v4(),
+ data: None,
+ created_at: OffsetDateTime::now_utc(),
+ deleted_at: None,
+ });
+ store.write_identity(&identity).await?;
+ store.write_email(&email).await?;
+ store
+ .write_email_validation(&EmailValidation {
+ id: Some(Uuid::new_v4()),
+ identity_id: Some(identity.id),
+ email_address: email,
+ code: None,
+ is_oauth_derived: true,
+ created_at: now,
+ expired_at: now,
+ validated_at: Some(now),
+ revoked_at: None,
+ deleted_at: None,
+ })
+ .await?;
+ Ok(Some(identity))
+ } else {
+ Ok(identity)
+ }
+ }
+ async fn validate(
+ &mut self,
+ i: &Identity,
+ store: Arc<dyn Store + Send + Sync>,
+ ) -> anyhow::Result<()> {
+ self.identity_id = Some(i.id);
+ self.validated_at = Some(OffsetDateTime::now_utc());
+ store.write_oauth_validation(&self).await?;
+ Ok(())
+ }
+}
+
+#[derive(Debug, EnumString)]
+pub enum ValidationType {
+ Email,
+ Oauth,
+}
+
#[derive(sqlx::FromRow, Debug)]
pub struct EmailValidation {
#[sqlx(rename = "email_validation_public_id")]
@@ -66,16 +183,53 @@ pub struct EmailValidation {
identity_id: Option<IdentityId>,
#[sqlx(rename = "address")]
email_address: String,
- attempts: i32,
- code: String,
- is_validated: bool,
+ code: Option<String>,
+ is_oauth_derived: bool,
+ created_at: OffsetDateTime,
+ expired_at: OffsetDateTime,
+ validated_at: Option<OffsetDateTime>,
+ revoked_at: Option<OffsetDateTime>,
+ deleted_at: Option<OffsetDateTime>,
+}
+
+#[derive(Debug)]
+pub struct OauthValidation {
+ id: Option<Uuid>,
+ identity_id: Option<IdentityId>,
+ oauth_provider: OauthProvider,
+ access_token: Option<String>,
+ raw_response: Option<String>,
created_at: OffsetDateTime,
- expires_at: OffsetDateTime,
+ validated_at: Option<OffsetDateTime>,
revoked_at: Option<OffsetDateTime>,
+ deleted_at: Option<OffsetDateTime>,
+}
+
+#[derive(Debug, Clone)]
+pub struct OauthProvider {
+ pub name: OauthProviderName,
+ pub flow: Option<String>,
+ pub base_url: Url,
+ pub response: OauthResponseType,
+ pub default_scope: String,
+ pub client_id: String,
+ pub client_secret: String,
+ pub redirect_url: Url,
+ pub created_at: OffsetDateTime,
+ pub deleted_at: Option<OffsetDateTime>,
+}
+
+#[derive(Debug, Display, Clone, Copy, ValueEnum, EnumString)]
+pub enum OauthResponseType {
+ Code,
+ IdToken,
+ None,
+ Token,
}
-#[derive(Copy, Display, Clone, Debug)]
-pub enum OauthProvider {
+// TODO: feature gate ValueEnum since it's only needed for iam builds
+#[derive(Copy, Display, Clone, Debug, ValueEnum, EnumString)]
+pub enum OauthProviderName {
Amazon,
Apple,
Dropbox,
@@ -121,19 +275,24 @@ pub type SessionSecret = String;
pub type SessionSecretHash = String;
pub type ValidationRequestId = Uuid;
pub type ValidationSecretCode = String;
+pub type OauthRedirectAuthUrl = Url;
#[derive(Debug, derive_more::Display, thiserror::Error)]
pub enum SecdError {
- InvalidEmailAddress,
- InvalidCode,
- InitializationFailure(sqlx::Error),
- IdentityIdShouldExistInvariant,
EmailSendError(#[from] EmailMessengerError),
- EmailValidationRequestError,
EmailValidationExpiryOverflow,
+ EmailValidationRequestError,
+ OauthValidationRequestError,
+ IdentityIdShouldExistInvariant,
+ InitializationFailure(sqlx::Error),
+ InvalidCode,
+ InvalidEmailAddress,
+ InputValidation(String),
+ InternalError(String),
+ NotImplemented(String),
SessionExpiryOverflow,
Unauthenticated,
- Unknown,
+ Todo,
}
pub struct Secd {
@@ -142,191 +301,6 @@ pub struct Secd {
}
impl Secd {
- pub async fn init(
- auth_store: AuthStore,
- conn_string: Option<&str>,
- email_messenger: AuthEmail,
- email_template_login: Option<String>,
- email_template_signup: Option<String>,
- ) -> Result<Self, SecdError> {
- let store = match auth_store {
- AuthStore::Sqlite => {
- SqliteClient::new(
- sqlx::sqlite::SqlitePoolOptions::new()
- .connect(conn_string.unwrap_or("sqlite::memory:".into()))
- .await
- .map_err(|e| SecdError::InitializationFailure(e))?,
- )
- .await
- }
- AuthStore::Postgres => {
- PgClient::new(
- sqlx::postgres::PgPoolOptions::new()
- .connect(conn_string.expect("No postgres connection string provided."))
- .await
- .map_err(|e| SecdError::InitializationFailure(e))?,
- )
- .await
- }
- rest @ _ => {
- error!(
- "requested an AuthStore which has not yet been implemented: {:?}",
- rest
- );
- unimplemented!()
- }
- };
-
- let email_sender = match email_messenger {
- // TODO: initialize email and SMS templates with secd
- AuthEmail::LocalStub => email::LocalEmailStubber {
- email_template_login,
- email_template_signup,
- },
- _ => unimplemented!(),
- };
-
- Ok(Secd {
- store,
- email_messenger: Arc::new(email_sender),
- })
- }
- /// create_validation_request
- ///
- /// Generate a request to validate the provided email.
- pub async fn create_validation_request(
- &self,
- email: Option<&str>,
- ) -> Result<ValidationRequestId, SecdError> {
- let now = OffsetDateTime::now_utc();
-
- let email = match email {
- Some(ea) => {
- if EmailAddress::is_valid(ea) {
- ea
- } else {
- return Err(SecdError::InvalidEmailAddress);
- }
- }
- None => return Err(SecdError::InvalidEmailAddress),
- };
-
- let mut ev = EmailValidation {
- id: None,
- identity_id: None,
- email_address: email.to_string(),
- attempts: 0,
- code: Alphanumeric
- .sample_string(&mut rand::thread_rng(), VALIDATION_CODE_SIZE)
- .to_lowercase(),
- is_validated: false,
- created_at: now,
- expires_at: now
- .checked_add(Duration::new(EMAIL_VALIDATION_DURATION, 0))
- .ok_or(SecdError::EmailValidationExpiryOverflow)?,
- revoked_at: None,
- };
-
- let (req_id, mail_type) = match self
- .store
- .find_identity(None, Some(email))
- .await
- .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?
- {
- Some(identity) => {
- let req_id = {
- ev.identity_id = Some(identity.id);
- self.store
- .write_email_validation(&ev)
- .await
- .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?
- };
- (req_id, client::EmailType::Login)
- }
- None => {
- let identity = Identity {
- id: Uuid::new_v4(),
- created_at: OffsetDateTime::now_utc(),
- data: None,
- };
- self.store
- .write_identity(&identity)
- .await
- .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?;
- self.store
- .write_email(identity.id, email)
- .await
- .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?;
-
- let req_id = {
- ev.identity_id = Some(identity.id);
- self.store
- .write_email_validation(&ev)
- .await
- .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?
- };
-
- (req_id, client::EmailType::Signup)
- }
- };
-
- self.email_messenger
- .send_email(email, &req_id.to_string(), &ev.code, mail_type)
- .await?;
-
- Ok(req_id)
- }
- /// exchange_secret_for_session
- ///
- /// Exchanges a secret, which consists of a validation_request_id and secret_code
- /// for a session which allows authentication on behalf of the associated identity.
- ///
- /// Session secrets should be used to return authorization for the associated identity.
- pub async fn exchange_code_for_session(
- &self,
- validation_request_id: ValidationRequestId,
- code: ValidationSecretCode,
- ) -> Result<Session, SecdError> {
- let mut ev = self
- .store
- .find_email_validation(Some(&validation_request_id), Some(&code))
- .await
- .map_err(|e| util::log_err(e.into(), SecdError::EmailValidationExpiryOverflow))?;
-
- if ev.is_validated
- || ev.expires_at < OffsetDateTime::now_utc()
- || ev.attempts >= VALIDATION_ATTEMPTS_MAX
- {
- return Err(SecdError::InvalidCode);
- };
-
- ev.is_validated = true;
- ev.attempts += 1;
- self.store
- .write_email_validation(&ev)
- .await
- .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?;
-
- // TODO: clear previous sessions if they fit the criteria
- let now = OffsetDateTime::now_utc();
- let s = Session {
- identity_id: ev
- .identity_id
- .ok_or(SecdError::IdentityIdShouldExistInvariant)?,
- secret: Some(Alphanumeric.sample_string(&mut rand::thread_rng(), SESSION_SIZE_BYTES)),
- created_at: now,
- expires_at: now
- .checked_add(Duration::new(SESSION_DURATION, 0))
- .ok_or(SecdError::SessionExpiryOverflow)?,
- revoked_at: None,
- };
- self.store
- .write_session(&s)
- .await
- .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?;
-
- Ok(s)
- }
/// get_identity
///
/// Return all information associated with the identity id.
@@ -350,7 +324,7 @@ impl Secd {
Ok(Authorization { session })
}
Ok(_) => Err(SecdError::Unauthenticated),
- Err(_e) => Err(SecdError::Unknown),
+ Err(_e) => Err(SecdError::Todo),
}
}
/// revoke_session
diff --git a/crates/secd/src/util/mod.rs b/crates/secd/src/util/mod.rs
index da16901..bb177cb 100644
--- a/crates/secd/src/util/mod.rs
+++ b/crates/secd/src/util/mod.rs
@@ -1,13 +1,27 @@
+use std::str::FromStr;
+
+use anyhow::{bail, Context};
use log::error;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
+use reqwest::header;
+use serde::{Deserialize, Serialize};
+use url::Url;
-use crate::SecdError;
+use crate::{
+ OauthProvider, OauthProviderName, OauthValidation, SecdError, ValidationRequestId,
+ INTERNAL_ERR_MSG,
+};
pub(crate) fn log_err(e: Box<dyn std::error::Error>, new_e: SecdError) -> SecdError {
error!("{:?}", e);
new_e
}
+pub(crate) fn to_secd_err(e: anyhow::Error, new_e: SecdError) -> SecdError {
+ error!("{:?}", e);
+ new_e
+}
+
pub(crate) fn log_err_sqlx(e: sqlx::Error) -> sqlx::Error {
error!("{:?}", e);
e
@@ -19,3 +33,145 @@ pub(crate) fn generate_random_url_safe(n: usize) -> String {
.map(char::from)
.collect()
}
+
+pub(crate) fn remove_trailing_slash(url: &mut Url) -> String {
+ let mut u = url.to_string();
+
+ if u.ends_with('/') {
+ u.pop();
+ }
+
+ u
+}
+
+pub(crate) fn build_oauth_auth_url(
+ p: &OauthProvider,
+ validation_id: ValidationRequestId,
+) -> Result<Url, SecdError> {
+ let redirect_url = remove_trailing_slash(&mut p.redirect_url.clone());
+
+ Ok(Url::from_str(&format!(
+ "{}?client_id={}&response_type={}&redirect_uri={}&scope={}&state={}",
+ p.base_url,
+ p.client_id,
+ p.response.to_string().to_lowercase(),
+ redirect_url,
+ p.default_scope,
+ validation_id.to_string()
+ ))
+ .map_err(|_| SecdError::InternalError(INTERNAL_ERR_MSG.into()))?)
+}
+
+pub(crate) async fn get_oauth_identity_data(
+ validation: &OauthValidation,
+) -> anyhow::Result<OauthAccessIdentity> {
+ let provider = validation.oauth_provider.name;
+ let token = validation
+ .access_token
+ .clone()
+ .ok_or(SecdError::InternalError(
+ "no access token provided with which to build oauth data url".into(),
+ ))?;
+
+ let url = Url::from_str(&format!(
+ "{}{}",
+ match provider {
+ OauthProviderName::Google =>
+ "https://www.googleapis.com/oauth2/v2/userinfo?access_token=",
+ _ => unimplemented!(),
+ },
+ token
+ ))?;
+
+ let resp = reqwest::get(url).await?.json::<serde_json::Value>().await?;
+ let identity = match provider {
+ OauthProviderName::Google => OauthAccessIdentity {
+ email: resp
+ .get("email")
+ .and_then(|v| v.as_str().map(|s| s.to_string())),
+ email_is_verified: resp.get("verified_email").and_then(|v| v.as_bool()),
+ picture_url: resp
+ .get("picture")
+ .and_then(|v| Url::from_str(&v.to_string()).ok()),
+ },
+ _ => unimplemented!(),
+ };
+
+ Ok(identity)
+}
+
+#[derive(Debug, Serialize)]
+pub(crate) struct OauthAccessTokenGoogleRequest {
+ grant_type: String,
+ code: String,
+ client_id: String,
+ client_secret: String,
+ redirect_uri: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub(crate) struct OauthAccessTokenGoogleResponse {
+ access_token: String,
+ expires_in: i32,
+ token_type: String,
+ scope: String,
+ id_token: String,
+}
+
+#[derive(Debug)]
+pub(crate) struct OauthAccessIdentity {
+ pub(crate) email: Option<String>,
+ pub(crate) email_is_verified: Option<bool>,
+ pub(crate) picture_url: Option<Url>,
+}
+
+type AccessTokenRequestData = String;
+
+pub(crate) async fn get_oauth_access_token(
+ validation: &OauthValidation,
+ secret_code: &String,
+) -> anyhow::Result<String> {
+ let provider = validation.oauth_provider.name;
+
+ let url = Url::from_str(match provider {
+ OauthProviderName::Google => "https://accounts.google.com/o/oauth2/token",
+ _ => unimplemented!(),
+ })?;
+
+ let request_data = serde_json::to_string(&match provider {
+ OauthProviderName::Google => OauthAccessTokenGoogleRequest {
+ grant_type: "authorization_code".to_string(),
+ code: secret_code.to_string(),
+ client_id: validation.oauth_provider.client_id.clone(),
+ client_secret: validation.oauth_provider.client_secret.clone(),
+ redirect_uri: remove_trailing_slash(
+ &mut validation.oauth_provider.redirect_url.clone(),
+ ),
+ },
+ _ => unimplemented!(),
+ })?;
+
+ let r = reqwest::Client::new()
+ .post(url)
+ .body(request_data)
+ .header(header::CONTENT_TYPE, "application/json")
+ .send()
+ .await
+ .context(format!(
+ "Failed to successfully POST a new access token for: {}",
+ provider
+ ))?;
+
+ let access_token = match provider {
+ OauthProviderName::Google => {
+ let resp: OauthAccessTokenGoogleResponse = r.json().await.context(format!(
+ "Failed to parse access token response for: {}",
+ provider
+ ))?;
+ resp.access_token
+ }
+ _ => unimplemented!(),
+ };
+
+ Ok(access_token)
+}
diff --git a/crates/secd/store/pg/migrations/20221116062550_bootstrap.sql b/crates/secd/store/pg/migrations/20221116062550_bootstrap.sql
index 3f5fb40..3d4d84c 100644
--- a/crates/secd/store/pg/migrations/20221116062550_bootstrap.sql
+++ b/crates/secd/store/pg/migrations/20221116062550_bootstrap.sql
@@ -3,47 +3,84 @@ create extension if not exists citext;
create schema if not exists secd;
create table if not exists secd.identity (
- identity_id bigserial primary key
+ identity_id bigserial primary key
, identity_public_id uuid
, data text
- , created_at timestamptz not null
+ , created_at timestamptz not null
+ , deleted_at timestamptz
, unique(identity_public_id)
);
-create table if not exists secd.email (
- email_id bigserial primary key
- , address text not null
- , unique(address)
+create table if not exists secd.session (
+ session_id bigserial primary key
+ , identity_id bigint not null references secd.identity(identity_id)
+ , secret_hash bytea not null
+ , created_at timestamptz not null
+ , expired_at timestamptz
+ , revoked_at timestamptz
+ , unique(secret_hash)
);
-create table if not exists secd.identity_email (
- identity_email_id bigserial primary key
- , identity_id bigint not null references secd.identity(identity_id)
- , email_id bigint not null references secd.email(email_id)
+create table if not exists secd.oauth_provider (
+ oauth_provider_id serial primary key
+ , name text not null
+ , flow text not null
+ , base_url text not null
+ , response_type text not null
+ , default_scope text
+ , client_id text not null
+ , client_secret text not null
+ , redirect_url text not null
, created_at timestamptz not null
, deleted_at timestamptz
+ , unique (name, flow)
+);
+
+create table if not exists secd.oauth_validation (
+ oauth_validation_id bigserial primary key
+ , oauth_validation_public_id uuid not null
+ , oauth_provider_id integer not null references secd.oauth_provider(oauth_provider_id)
+ , access_token text
+ , raw_response text
+ , created_at timestamptz not null
+ , validated_at timestamptz
+ , unique (oauth_validation_public_id)
+);
+
+create table if not exists secd.identity_oauth_validation (
+ identity_oauth_validation_id bigserial primary key
+ -- A validation does not require an identity to initiate
+ , identity_id bigint references secd.identity(identity_id)
+ , oauth_validation_id bigint not null references secd.oauth_validation(oauth_validation_id)
+ , revoked_at timestamptz
+ , deleted_at timestamptz
+ , unique(identity_id, oauth_validation_id)
+);
+
+create table if not exists secd.email (
+ email_id bigserial primary key
+ , address text not null
+ , unique(address)
);
create table if not exists secd.email_validation (
email_validation_id bigserial primary key
, email_validation_public_id uuid not null
- , identity_email_id integer not null references secd.identity_email(identity_email_id)
- , attempts integer not null
+ , email_id bigint not null references secd.email(email_id)
, code text
- , is_validated boolean not null default false
+ , is_oauth_derived boolean not null
, created_at timestamptz not null
- , expires_at timestamptz
- , revoked_at timestamptz
+ , validated_at timestamptz
+ , expired_at timestamptz
, unique(email_validation_public_id)
);
-create table if not exists secd.session (
- session_id bigserial primary key
- , identity_id bigint not null references secd.identity(identity_id)
- , secret_hash bytea not null
- , created_at timestamptz not null
- , touched_at timestamptz not null
- , expires_at timestamptz
+create table if not exists secd.identity_email_validation (
+ identity_email_validation_id bigserial primary key
+ -- A validation does not require an identity to initiate
+ , identity_id bigint references secd.identity(identity_id)
+ , email_validation_id bigint not null references secd.email_validation(email_validation_id)
, revoked_at timestamptz
- , unique(secret_hash)
+ , deleted_at timestamptz
+ , unique(identity_id, email_validation_id)
);
diff --git a/crates/secd/store/pg/sql/find_email_validation.sql b/crates/secd/store/pg/sql/find_email_validation.sql
index 96a8cc4..1eb3e43 100644
--- a/crates/secd/store/pg/sql/find_email_validation.sql
+++ b/crates/secd/store/pg/sql/find_email_validation.sql
@@ -2,16 +2,17 @@ select
ev.email_validation_public_id
, i.identity_public_id
, e.address
- , ev.attempts
, ev.code
- , ev.is_validated
+ , ev.is_oauth_derived
, ev.created_at
- , ev.expires_at
- , ev.revoked_at
-from secd.email_validation ev
-join secd.identity_email ie using (identity_email_id)
-join secd.email e using (email_id)
-join secd.identity i using (identity_id)
+ , ev.expired_at
+ , ev.validated_at
+ , iev.revoked_at
+ , iev.deleted_at
+from secd.email_validation ev
+join secd.email e using (email_id)
+left join secd.identity_email_validation iev using (email_validation_id)
+left join secd.identity i using (identity_id)
where (($1 is null) or (email_validation_public_id = $1))
and (($2 is null) or (code = $2));
--
diff --git a/crates/secd/store/pg/sql/find_identity.sql b/crates/secd/store/pg/sql/find_identity.sql
index f4c9cbf..135ff9a 100644
--- a/crates/secd/store/pg/sql/find_identity.sql
+++ b/crates/secd/store/pg/sql/find_identity.sql
@@ -1,9 +1,11 @@
select
- identity_public_id,
- data,
- i.created_at
+ identity_public_id
+ , data
+ , i.created_at
+ , i.deleted_at
from secd.identity i
-join secd.identity_email ie using (identity_id)
-join secd.email e using (email_id)
+join secd.identity_email_validation iev using (identity_id)
+join secd.email_validation ev using (email_validation_id)
+join secd.email e using (email_id)
where (($1 is null) or (i.identity_public_id = $1))
-and (($2 is null) or (e.address = $2))
+and (($2 is null) or (e.address = $2));
diff --git a/crates/secd/store/pg/sql/find_identity_by_code.sql b/crates/secd/store/pg/sql/find_identity_by_code.sql
index e016a0e..e5a0970 100644
--- a/crates/secd/store/pg/sql/find_identity_by_code.sql
+++ b/crates/secd/store/pg/sql/find_identity_by_code.sql
@@ -1,4 +1,4 @@
-select identity_email_id
+select identity_email_validation_id
from secd.email_validation
where email_validation_public_id = $1::uuid
--
@@ -8,4 +8,4 @@ select
, i.created_at
from secd.identity i
left join secd.identity_email ie using (identity_id)
-where ie.identity_email_id = $1;
+where ie.identity_email_validation_id = $1;
diff --git a/crates/secd/store/pg/sql/read_oauth_provider.sql b/crates/secd/store/pg/sql/read_oauth_provider.sql
new file mode 100644
index 0000000..edaa114
--- /dev/null
+++ b/crates/secd/store/pg/sql/read_oauth_provider.sql
@@ -0,0 +1,12 @@
+select flow
+ , base_url
+ , response_type
+ , default_scope
+ , client_id
+ , client_secret
+ , redirect_url
+ , created_at
+ , deleted_at
+from secd.oauth_provider
+where name = $1
+and flow = $2;
diff --git a/crates/secd/store/pg/sql/read_oauth_validation.sql b/crates/secd/store/pg/sql/read_oauth_validation.sql
new file mode 100644
index 0000000..d8361ea
--- /dev/null
+++ b/crates/secd/store/pg/sql/read_oauth_validation.sql
@@ -0,0 +1,23 @@
+select oauth_validation_public_id
+ , i.identity_public_id
+ , ov.access_token
+ , ov.raw_response
+ , ov.created_at
+ , ov.validated_at
+ , iov.revoked_at
+ , iov.deleted_at
+ , op.name as oauth_provider_name
+ , op.flow as oauth_provider_flow
+ , op.base_url as oauth_provider_base_url
+ , op.response_type as oauth_provider_response_type
+ , op.default_scope as oauth_provider_default_scope
+ , op.client_id as oauth_provider_client_id
+ , op.client_secret as oauth_provider_client_secret
+ , op.redirect_url as oauth_provider_redirect_url
+ , op.created_at as oauth_provider_created_at
+ , op.deleted_at as oauth_provider_deleted_at
+from secd.oauth_validation ov
+join secd.oauth_provider op using(oauth_provider_id)
+left join secd.identity_oauth_validation iov using(oauth_validation_id)
+left join secd.identity i using(identity_id)
+where oauth_validation_public_id = $1;
diff --git a/crates/secd/store/pg/sql/read_validation_type.sql b/crates/secd/store/pg/sql/read_validation_type.sql
new file mode 100644
index 0000000..2eceb98
--- /dev/null
+++ b/crates/secd/store/pg/sql/read_validation_type.sql
@@ -0,0 +1,7 @@
+select 'Email'
+from secd.email_validation
+where email_validation_public_id = $1
+union
+select 'Oauth'
+from secd.oauth_validation
+where oauth_validation_public_id = $1;
diff --git a/crates/secd/store/pg/sql/write_email.sql b/crates/secd/store/pg/sql/write_email.sql
index cdcc971..06a1dc5 100644
--- a/crates/secd/store/pg/sql/write_email.sql
+++ b/crates/secd/store/pg/sql/write_email.sql
@@ -4,8 +4,3 @@ insert into secd.email (
$1
) on conflict (address) do nothing
returning email_id;
---
-select email_id from secd.email where address = $1;
---
-insert into secd.identity_email (identity_id, email_id, created_at) values ($1, $2, $3);
---
diff --git a/crates/secd/store/pg/sql/write_email_validation.sql b/crates/secd/store/pg/sql/write_email_validation.sql
index d99a04c..ff25b87 100644
--- a/crates/secd/store/pg/sql/write_email_validation.sql
+++ b/crates/secd/store/pg/sql/write_email_validation.sql
@@ -1,27 +1,43 @@
insert into secd.email_validation
(
email_validation_public_id
- , identity_email_id
- , attempts
+ , email_id
, code
- , is_validated
+ , is_oauth_derived
, created_at
- , expires_at
+ , validated_at
+ , expired_at
)
values (
$1
- , (
- select identity_email_id
- from secd.identity_email
- where identity_id = $2
- and email_id = $3
- )
+ , $2
+ , $3
, $4
, $5
, $6
, $7
- , $8
) on conflict (email_validation_public_id) do update
- set attempts = excluded.attempts
- , is_validated = excluded.is_validated
- , expires_at = excluded.expires_at;
+ set validated_at = excluded.validated_at
+ , expired_at = excluded.expired_at;
+--
+insert into secd.identity_email_validation (
+ identity_id
+ , email_validation_id
+ , revoked_at
+ , deleted_at
+) values (
+ (
+ select identity_id
+ from secd.identity
+ where identity_public_id = $1
+ )
+ , (
+ select email_validation_id
+ from secd.email_validation
+ where email_validation_public_id = $2
+ )
+ , $3
+ , $4
+) on conflict (identity_id, email_validation_id) do update
+ set revoked_at = excluded.revoked_at
+ , deleted_at = excluded.deleted_at;
diff --git a/crates/secd/store/pg/sql/write_identity.sql b/crates/secd/store/pg/sql/write_identity.sql
index 7d53ee1..94a51fe 100644
--- a/crates/secd/store/pg/sql/write_identity.sql
+++ b/crates/secd/store/pg/sql/write_identity.sql
@@ -6,4 +6,6 @@ insert into secd.identity (
$1,
$2,
$3
-);
+) on conflict(identity_public_id) do update
+ set data = excluded.data
+ , deleted_at = excluded.deleted_at;
diff --git a/crates/secd/store/pg/sql/write_oauth_provider.sql b/crates/secd/store/pg/sql/write_oauth_provider.sql
new file mode 100644
index 0000000..ba69857
--- /dev/null
+++ b/crates/secd/store/pg/sql/write_oauth_provider.sql
@@ -0,0 +1,25 @@
+insert into secd.oauth_provider (
+ oauth_provider_id
+ , name
+ , flow
+ , base_url
+ , response_type
+ , default_scope
+ , client_id
+ , client_secret
+ , redirect_url
+ , created_at
+ , deleted_at
+) values (
+ default
+ , $1
+ , $2
+ , $3
+ , $4
+ , $5
+ , $6
+ , $7
+ , $8
+ , $9
+ , $10
+) on conflict (name, flow) do nothing;
diff --git a/crates/secd/store/pg/sql/write_oauth_validation.sql b/crates/secd/store/pg/sql/write_oauth_validation.sql
new file mode 100644
index 0000000..11f2578
--- /dev/null
+++ b/crates/secd/store/pg/sql/write_oauth_validation.sql
@@ -0,0 +1,45 @@
+insert into secd.oauth_validation (
+ oauth_validation_public_id
+ , oauth_provider_id
+ , access_token
+ , raw_response
+ , created_at
+ , validated_at
+) values (
+ $1
+ , (
+ select oauth_provider_id
+ from secd.oauth_provider
+ where name = $2
+ and flow = $3
+ )
+ , $4
+ , $5
+ , $6
+ , $7
+) on conflict (oauth_validation_public_id) do update
+ set access_token = excluded.access_token
+ , validated_at = excluded.validated_at
+ , raw_response = excluded.raw_response;
+--
+insert into secd.identity_oauth_validation (
+ identity_id
+ , oauth_validation_id
+ , revoked_at
+ , deleted_at
+) values (
+ (
+ select identity_id
+ from secd.identity
+ where identity_public_id = $1
+ )
+ , (
+ select oauth_validation_id
+ from secd.oauth_validation
+ where oauth_validation_public_id = $2
+ )
+ , $3
+ , $4
+) on conflict (identity_id, oauth_validation_id) do update
+ set revoked_at = excluded.revoked_at
+ , deleted_at = excluded.deleted_at;
diff --git a/crates/secd/store/pg/sql/write_session.sql b/crates/secd/store/pg/sql/write_session.sql
index 86cde55..1b238c6 100644
--- a/crates/secd/store/pg/sql/write_session.sql
+++ b/crates/secd/store/pg/sql/write_session.sql
@@ -2,8 +2,7 @@ insert into secd.session (
identity_id
, secret_hash
, created_at
- , touched_at
- , expires_at
+ , expired_at
, revoked_at
) values (
(select identity_id from secd.identity where identity_public_id = $1)
@@ -11,8 +10,6 @@ insert into secd.session (
, $3
, $4
, $5
- , $6
) on conflict (secret_hash) do update
- set touched_at = excluded.touched_at
- , revoked_at = excluded.revoked_at;
+ set revoked_at = excluded.revoked_at;
--
diff --git a/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql b/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql
index aa95afc..a8784f5 100644
--- a/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql
+++ b/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql
@@ -2,44 +2,81 @@ create table if not exists identity (
identity_id integer primary key autoincrement
, identity_public_id uuid
, data text
- , created_at timestamp not null
+ , created_at timestamptz not null
+ , deleted_at timestamptz
, unique(identity_public_id)
);
+create table if not exists session (
+ session_id integer primary key autoincrement
+ , identity_id bigint not null references identity(identity_id)
+ , secret_hash bytea not null
+ , created_at timestamptz not null
+ , expired_at timestamptz
+ , revoked_at timestamptz
+ , unique(secret_hash)
+);
+
+create table if not exists oauth_provider (
+ oauth_provider_id integer primary key autoincrement
+ , name text not null
+ , flow text not null
+ , base_url text not null
+ , response_type text not null
+ , default_scope text
+ , client_id text not null
+ , client_secret text not null
+ , redirect_url text not null
+ , created_at timestamptz not null
+ , deleted_at timestamptz
+ , unique (name, flow)
+);
+
+create table if not exists oauth_validation (
+ oauth_validation_id integer primary key autoincrement
+ , oauth_validation_public_id uuid not null
+ , oauth_provider_id integer not null references oauth_provider(oauth_provider_id)
+ , access_token text
+ , raw_response text
+ , created_at timestamptz not null
+ , validated_at timestamptz
+ , unique (oauth_validation_public_id)
+);
+
+create table if not exists identity_oauth_validation (
+ identity_oauth_validation_id integer primary key autoincrement
+ -- A validation does not require an identity to initiate
+ , identity_id bigint references identity(identity_id)
+ , oauth_validation_id bigint not null references oauth_validation(oauth_validation_id)
+ , revoked_at timestamptz
+ , deleted_at timestamptz
+ , unique(identity_id, oauth_validation_id)
+);
+
create table if not exists email (
email_id integer primary key autoincrement
, address text not null
, unique(address)
);
-create table if not exists identity_email (
- identity_email_id integer primary key autoincrement
- , identity_id integer not null references identity(identity_id)
- , email_id integer not null references email(email_id)
- , created_at timestamp not null
- , deleted_at timestamp
-);
-
create table if not exists email_validation (
email_validation_id integer primary key autoincrement
- , email_validation_public_id text not null -- uuid
- , identity_email_id integer not null references identity_email(identity_email_id)
- , attempts integer not null
+ , email_validation_public_id uuid not null
+ , email_id bigint not null references email(email_id)
, code text
- , is_validated boolean not null
- , created_at timestamp not null
- , expires_at timestamp
- , revoked_at timestamp
+ , is_oauth_derived boolean not null
+ , created_at timestamptz not null
+ , validated_at timestamptz
+ , expired_at timestamptz
, unique(email_validation_public_id)
);
-create table if not exists session (
- session_id integer primary key autoincrement
- , identity_id not null references identity(identity_id)
- , secret_hash blob not null
- , created_at timestamp not null
- , touched_at timestamp not null
- , expires_at timestamp
- , revoked_at timestamp
- , unique(secret_hash)
+create table if not exists identity_email_validation (
+ identity_email_validation_id integer primary key autoincrement
+ -- A validation does not require an identity to initiate
+ , identity_id bigint references identity(identity_id)
+ , email_validation_id bigint not null references email_validation(email_validation_id)
+ , revoked_at timestamptz
+ , deleted_at timestamptz
+ , unique(identity_id, email_validation_id)
);
diff --git a/crates/secd/store/sqlite/sql/find_email_validation.sql b/crates/secd/store/sqlite/sql/find_email_validation.sql
index a34c149..d7f311c 100644
--- a/crates/secd/store/sqlite/sql/find_email_validation.sql
+++ b/crates/secd/store/sqlite/sql/find_email_validation.sql
@@ -2,15 +2,17 @@ select
ev.email_validation_public_id
, i.identity_public_id
, e.address
- , ev.attempts
, ev.code
- , ev.is_validated
+ , ev.is_oauth_derived
, ev.created_at
- , ev.expires_at
- , ev.revoked_at
-from email_validation ev
-join identity_email ie using (identity_email_id)
-join email e using (email_id)
-join identity i using (identity_id)
+ , ev.expired_at
+ , ev.validated_at
+ , iev.revoked_at
+ , iev.deleted_at
+from email_validation ev
+join email e using (email_id)
+left join identity_email_validation iev using (email_validation_id)
+left join identity i using (identity_id)
where ((?1 is null) or (email_validation_public_id = ?1))
and ((?2 is null) or (code = ?2));
+--
diff --git a/crates/secd/store/sqlite/sql/find_identity.sql b/crates/secd/store/sqlite/sql/find_identity.sql
index bd1654d..f94e7b1 100644
--- a/crates/secd/store/sqlite/sql/find_identity.sql
+++ b/crates/secd/store/sqlite/sql/find_identity.sql
@@ -1,9 +1,11 @@
select
- identity_public_id,
- data,
- i.created_at
+ identity_public_id
+ , data
+ , i.created_at
+ , i.deleted_at
from identity i
-join identity_email ie using (identity_id)
-join email e using (email_id)
+join identity_email_validation iev using (identity_id)
+join email_validation ev using (email_validation_id)
+join email e using (email_id)
where ((?1 is null) or (i.identity_public_id = ?1))
-and ((?2 is null) or (e.address = ?2))
+and ((?2 is null) or (e.address = ?2));
diff --git a/crates/secd/store/sqlite/sql/find_identity_by_code.sql b/crates/secd/store/sqlite/sql/find_identity_by_code.sql
index 77844ff..b70a13a 100644
--- a/crates/secd/store/sqlite/sql/find_identity_by_code.sql
+++ b/crates/secd/store/sqlite/sql/find_identity_by_code.sql
@@ -1,11 +1,11 @@
-select identity_email_id
-from secd.email_validation
-where email_validation_public_id = ?1;
+select identity_email_validation_id
+from email_validation
+where email_validation_public_id = $1::uuid
--
select
identity_public_id
, data
, i.created_at
-from secd.identity i
-left join secd.identity_email ie using (identity_id)
-where ie.identity_email_id = ?1;
+from identity i
+left join identity_email ie using (identity_id)
+where ie.identity_email_validation_id = ?1;
diff --git a/crates/secd/store/sqlite/sql/read_email_raw_id.sql b/crates/secd/store/sqlite/sql/read_email_raw_id.sql
index 0bbafad..a65c717 100644
--- a/crates/secd/store/sqlite/sql/read_email_raw_id.sql
+++ b/crates/secd/store/sqlite/sql/read_email_raw_id.sql
@@ -1 +1 @@
-select email_id from email where address = ?
+select email_id from email where address = ?1
diff --git a/crates/secd/store/sqlite/sql/read_identity_raw_id.sql b/crates/secd/store/sqlite/sql/read_identity_raw_id.sql
index 552c570..2bdb718 100644
--- a/crates/secd/store/sqlite/sql/read_identity_raw_id.sql
+++ b/crates/secd/store/sqlite/sql/read_identity_raw_id.sql
@@ -1,2 +1,2 @@
-select identity_id from identity where identity_public_id = ?;
+select identity_id from identity where identity_public_id = ?1;
--
diff --git a/crates/secd/store/sqlite/sql/read_oauth_provider.sql b/crates/secd/store/sqlite/sql/read_oauth_provider.sql
new file mode 100644
index 0000000..5c33cf0
--- /dev/null
+++ b/crates/secd/store/sqlite/sql/read_oauth_provider.sql
@@ -0,0 +1,12 @@
+select flow
+ , base_url
+ , response_type
+ , default_scope
+ , client_id
+ , client_secret
+ , redirect_url
+ , created_at
+ , deleted_at
+from oauth_provider
+where name = ?1
+and flow = ?2;
diff --git a/crates/secd/store/sqlite/sql/read_oauth_validation.sql b/crates/secd/store/sqlite/sql/read_oauth_validation.sql
new file mode 100644
index 0000000..75f5a94
--- /dev/null
+++ b/crates/secd/store/sqlite/sql/read_oauth_validation.sql
@@ -0,0 +1,23 @@
+select oauth_validation_public_id
+ , i.identity_public_id
+ , ov.access_token
+ , ov.raw_response
+ , ov.created_at
+ , ov.validated_at
+ , iov.revoked_at
+ , iov.deleted_at
+ , op.name as oauth_provider_name
+ , op.flow as oauth_provider_flow
+ , op.base_url as oauth_provider_base_url
+ , op.response_type as oauth_provider_response_type
+ , op.default_scope as oauth_provider_default_scope
+ , op.client_id as oauth_provider_client_id
+ , op.client_secret as oauth_provider_client_secret
+ , op.redirect_url as oauth_provider_redirect_url
+ , op.created_at as oauth_provider_created_at
+ , op.deleted_at as oauth_provider_deleted_at
+from oauth_validation ov
+join oauth_provider op using(oauth_provider_id)
+left join identity_oauth_validation iov using(oauth_validation_id)
+left join identity i using(identity_id)
+where oauth_validation_public_id = ?1;
diff --git a/crates/secd/store/sqlite/sql/read_validation_type.sql b/crates/secd/store/sqlite/sql/read_validation_type.sql
new file mode 100644
index 0000000..cc02ead
--- /dev/null
+++ b/crates/secd/store/sqlite/sql/read_validation_type.sql
@@ -0,0 +1,7 @@
+select 'Email'
+from email_validation
+where email_validation_public_id = ?1
+union
+select 'Oauth'
+from oauth_validation
+where oauth_validation_public_id = ?1;
diff --git a/crates/secd/store/sqlite/sql/write_email.sql b/crates/secd/store/sqlite/sql/write_email.sql
index c127d9c..a64aed4 100644
--- a/crates/secd/store/sqlite/sql/write_email.sql
+++ b/crates/secd/store/sqlite/sql/write_email.sql
@@ -1,11 +1,6 @@
insert into email (
address
) values (
- ?1
+ $1
) on conflict (address) do nothing
returning email_id;
---
-select email_id from email where email = ?1;
---
-insert into identity_email (identity_id, email_id, created_at) values (?1, ?2, ?3);
---
diff --git a/crates/secd/store/sqlite/sql/write_email_validation.sql b/crates/secd/store/sqlite/sql/write_email_validation.sql
index 37b13e1..d839310 100644
--- a/crates/secd/store/sqlite/sql/write_email_validation.sql
+++ b/crates/secd/store/sqlite/sql/write_email_validation.sql
@@ -1,27 +1,43 @@
insert into email_validation
(
email_validation_public_id
- , identity_email_id
- , attempts
+ , email_id
, code
- , is_validated
+ , is_oauth_derived
, created_at
- , expires_at
+ , validated_at
+ , expired_at
)
values (
?1
- , (
- select identity_email_id
- from identity_email
- where identity_id = ?2
- and email_id = ?3
- )
+ , ?2
+ , ?3
, ?4
, ?5
, ?6
, ?7
- , ?8
) on conflict (email_validation_public_id) do update
- set attempts = excluded.attempts
- , is_validated = excluded.is_validated
- , expires_at = excluded.expires_at;
+ set validated_at = excluded.validated_at
+ , expired_at = excluded.expired_at;
+--
+insert into identity_email_validation (
+ identity_id
+ , email_validation_id
+ , revoked_at
+ , deleted_at
+) values (
+ (
+ select identity_id
+ from identity
+ where identity_public_id = ?1
+ )
+ , (
+ select email_validation_id
+ from email_validation
+ where email_validation_public_id = ?2
+ )
+ , ?3
+ , ?4
+) on conflict (identity_id, email_validation_id) do update
+ set revoked_at = excluded.revoked_at
+ , deleted_at = excluded.deleted_at;
diff --git a/crates/secd/store/sqlite/sql/write_identity.sql b/crates/secd/store/sqlite/sql/write_identity.sql
index ff54468..8cf46c5 100644
--- a/crates/secd/store/sqlite/sql/write_identity.sql
+++ b/crates/secd/store/sqlite/sql/write_identity.sql
@@ -1 +1,11 @@
-insert into identity (identity_public_id, data, created_at) values (?1, ?2, ?3);
+insert into identity (
+ identity_public_id,
+ data,
+ created_at
+) values (
+ ?1,
+ ?2,
+ ?3
+) on conflict(identity_public_id) do update
+ set data = excluded.data
+ , deleted_at = excluded.deleted_at;
diff --git a/crates/secd/store/sqlite/sql/write_oauth_provider.sql b/crates/secd/store/sqlite/sql/write_oauth_provider.sql
new file mode 100644
index 0000000..421caf7
--- /dev/null
+++ b/crates/secd/store/sqlite/sql/write_oauth_provider.sql
@@ -0,0 +1,23 @@
+insert into oauth_provider (
+ name
+ , flow
+ , base_url
+ , response_type
+ , default_scope
+ , client_id
+ , client_secret
+ , redirect_url
+ , created_at
+ , deleted_at
+) values (
+ ?1
+ , ?2
+ , ?3
+ , ?4
+ , ?5
+ , ?6
+ , ?7
+ , ?8
+ , ?9
+ , ?10
+) on conflict (name, flow) do nothing;
diff --git a/crates/secd/store/sqlite/sql/write_oauth_validation.sql b/crates/secd/store/sqlite/sql/write_oauth_validation.sql
new file mode 100644
index 0000000..ccb11aa
--- /dev/null
+++ b/crates/secd/store/sqlite/sql/write_oauth_validation.sql
@@ -0,0 +1,45 @@
+insert into oauth_validation (
+ oauth_validation_public_id
+ , oauth_provider_id
+ , access_token
+ , raw_response
+ , created_at
+ , validated_at
+) values (
+ ?1
+ , (
+ select oauth_provider_id
+ from oauth_provider
+ where name = ?2
+ and flow = ?3
+ )
+ , ?4
+ , ?5
+ , ?6
+ , ?7
+) on conflict (oauth_validation_public_id) do update
+ set access_token = excluded.access_token
+ , validated_at = excluded.validated_at
+ , raw_response = excluded.raw_response;
+--
+insert into identity_oauth_validation (
+ identity_id
+ , oauth_validation_id
+ , revoked_at
+ , deleted_at
+) values (
+ (
+ select identity_id
+ from identity
+ where identity_public_id = ?1
+ )
+ , (
+ select oauth_validation_id
+ from oauth_validation
+ where oauth_validation_public_id = ?2
+ )
+ , ?3
+ , ?4
+) on conflict (identity_id, oauth_validation_id) do update
+ set revoked_at = excluded.revoked_at
+ , deleted_at = excluded.deleted_at;
diff --git a/crates/secd/store/sqlite/sql/write_session.sql b/crates/secd/store/sqlite/sql/write_session.sql
index 3c26986..480af54 100644
--- a/crates/secd/store/sqlite/sql/write_session.sql
+++ b/crates/secd/store/sqlite/sql/write_session.sql
@@ -2,8 +2,7 @@ insert into session (
identity_id
, secret_hash
, created_at
- , touched_at
- , expires_at
+ , expired_at
, revoked_at
) values (
(select identity_id from identity where identity_public_id = ?1)
@@ -11,8 +10,6 @@ insert into session (
, ?3
, ?4
, ?5
- , ?6
) on conflict (secret_hash) do update
- set touched_at = excluded.touched_at
- , revoked_at = excluded.revoked_at;
+ set revoked_at = excluded.revoked_at;
--
diff --git a/justfile b/justfile
index c40de35..4b46fb9 100644
--- a/justfile
+++ b/justfile
@@ -1,7 +1,10 @@
run-debug:
@RUST_BACKTRACE=1 cargo run $@
-build:
+build-dev:
+ @cargo build
+
+build-prod:
@cargo build --release
start-postgres: