backend: verify passkey ceremonies
This commit is contained in:
parent
026e4cf90b
commit
554a784639
254
Cargo.lock
generated
254
Cargo.lock
generated
|
|
@ -132,6 +132,45 @@ dependencies = [
|
||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asn1-rs"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs-derive",
|
||||||
|
"asn1-rs-impl",
|
||||||
|
"displaydoc",
|
||||||
|
"nom 7.1.3",
|
||||||
|
"num-traits",
|
||||||
|
"rusticata-macros",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asn1-rs-derive"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
"synstructure",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asn1-rs-impl"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assert-json-diff"
|
name = "assert-json-diff"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
|
|
@ -203,7 +242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
@ -251,12 +290,29 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.21.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64urlsafedata"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b08e33815c87d8cadcddb1e74ac307368a3751fbe40c961538afa21a1899f21c"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"pastey",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|
@ -810,6 +866,12 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
|
|
@ -909,6 +971,20 @@ version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4"
|
checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der-parser"
|
||||||
|
version = "9.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs",
|
||||||
|
"displaydoc",
|
||||||
|
"nom 7.1.3",
|
||||||
|
"num-bigint",
|
||||||
|
"num-traits",
|
||||||
|
"rusticata-macros",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
|
|
@ -1460,6 +1536,17 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "half"
|
||||||
|
version = "2.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crunchy",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
|
|
@ -1646,7 +1733,7 @@ version = "0.1.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
@ -2096,7 +2183,7 @@ name = "llm-engine"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
|
|
@ -2479,6 +2566,16 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-bigint"
|
||||||
|
version = "0.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
|
||||||
|
dependencies = [
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
@ -2496,6 +2593,15 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
|
|
@ -2542,6 +2648,15 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oid-registry"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
|
|
@ -2630,6 +2745,12 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pastey"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdf-extract"
|
name = "pdf-extract"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
|
@ -3258,7 +3379,7 @@ version = "0.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
|
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
|
@ -3346,6 +3467,15 @@ dependencies = [
|
||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusticata-macros"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
|
||||||
|
dependencies = [
|
||||||
|
"nom 7.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.44"
|
version = "0.38.44"
|
||||||
|
|
@ -3562,6 +3692,16 @@ dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_cbor_2"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34aec2709de9078e077090abd848e967abab63c9fb3fdb5d4799ad359d8d482c"
|
||||||
|
dependencies = [
|
||||||
|
"half",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_core"
|
name = "serde_core"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
|
|
@ -4061,7 +4201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7"
|
checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"fancy-regex",
|
"fancy-regex",
|
||||||
"filedescriptor",
|
"filedescriptor",
|
||||||
|
|
@ -4171,12 +4311,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
|
"itoa",
|
||||||
"libc",
|
"libc",
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"num_threads",
|
"num_threads",
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"time-core",
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4185,6 +4327,16 @@ version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
|
@ -4524,7 +4676,7 @@ checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||||
name = "tui"
|
name = "tui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"client",
|
"client",
|
||||||
"crossterm 0.28.1",
|
"crossterm 0.28.1",
|
||||||
"fs4",
|
"fs4",
|
||||||
|
|
@ -4686,6 +4838,7 @@ dependencies = [
|
||||||
"idna",
|
"idna",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5211,6 +5364,74 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webauthn-attestation-ca"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6475c0bbd1a3f04afaa3e98880408c5be61680c5e6bd3c6f8c250990d5d3e18e"
|
||||||
|
dependencies = [
|
||||||
|
"base64urlsafedata",
|
||||||
|
"openssl",
|
||||||
|
"openssl-sys",
|
||||||
|
"serde",
|
||||||
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webauthn-rs"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c548915e0e92ee946bbf2aecf01ea21bef53d974b0793cc6732ba81a03fc422"
|
||||||
|
dependencies = [
|
||||||
|
"base64urlsafedata",
|
||||||
|
"serde",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
|
"webauthn-rs-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webauthn-rs-core"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "296d2d501feb715d80b8e186fb88bab1073bca17f460303a1013d17b673bea6a"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"base64urlsafedata",
|
||||||
|
"der-parser",
|
||||||
|
"hex",
|
||||||
|
"nom 7.1.3",
|
||||||
|
"openssl",
|
||||||
|
"openssl-sys",
|
||||||
|
"rand 0.9.4",
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"serde",
|
||||||
|
"serde_cbor_2",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
|
"webauthn-attestation-ca",
|
||||||
|
"webauthn-rs-proto",
|
||||||
|
"x509-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webauthn-rs-proto"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c37393beac9c1ed1ca6dbb30b1e01783fb316ab3a45d90ecd48c99052dd7ef1e"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"base64urlsafedata",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-root-certs"
|
name = "webpki-root-certs"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
|
|
@ -5577,7 +5798,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
|
checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assert-json-diff",
|
"assert-json-diff",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"deadpool",
|
"deadpool",
|
||||||
"futures",
|
"futures",
|
||||||
"http",
|
"http",
|
||||||
|
|
@ -5777,6 +5998,23 @@ version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "x509-parser"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs",
|
||||||
|
"data-encoding",
|
||||||
|
"der-parser",
|
||||||
|
"lazy_static",
|
||||||
|
"nom 7.1.3",
|
||||||
|
"oid-registry",
|
||||||
|
"rusticata-macros",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xattr"
|
name = "xattr"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
|
|
@ -5868,7 +6106,9 @@ dependencies = [
|
||||||
"toml",
|
"toml",
|
||||||
"tower",
|
"tower",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"webauthn-rs",
|
||||||
"worker",
|
"worker",
|
||||||
"worker-runtime",
|
"worker-runtime",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -99,4 +99,6 @@ tokio-tungstenite = "0.29"
|
||||||
tower = "0.5"
|
tower = "0.5"
|
||||||
toml = "1.1"
|
toml = "1.1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
url = "2.5"
|
||||||
uuid = "1.23"
|
uuid = "1.23"
|
||||||
|
webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ worker.workspace = true
|
||||||
worker-runtime = { workspace = true, features = ["ws-server", "fs-store"] }
|
worker-runtime = { workspace = true, features = ["ws-server", "fs-store"] }
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
url.workspace = true
|
||||||
uuid = { workspace = true, features = ["v7"] }
|
uuid = { workspace = true, features = ["v7"] }
|
||||||
|
webauthn-rs = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ pub const WORKSPACE_BACKEND_CONFIG_TEMPLATE: &str =
|
||||||
include_str!("../../../resources/workspace-backend.default.toml");
|
include_str!("../../../resources/workspace-backend.default.toml");
|
||||||
const DEFAULT_LISTEN: &str = "127.0.0.1:8787";
|
const DEFAULT_LISTEN: &str = "127.0.0.1:8787";
|
||||||
const DEFAULT_FRONTEND_URL: &str = "http://127.0.0.1:5173";
|
const DEFAULT_FRONTEND_URL: &str = "http://127.0.0.1:5173";
|
||||||
const DEFAULT_AUTH_PUBLIC_BASE_URL: &str = "http://127.0.0.1:8787";
|
const DEFAULT_AUTH_PUBLIC_BASE_URL: &str = "http://localhost:8787";
|
||||||
const DEFAULT_AUTH_RP_ID: &str = "127.0.0.1";
|
const DEFAULT_AUTH_RP_ID: &str = "localhost";
|
||||||
const DEFAULT_AUTH_COOKIE_NAME: &str = "yoi_workspace_session";
|
const DEFAULT_AUTH_COOKIE_NAME: &str = "yoi_workspace_session";
|
||||||
const DEFAULT_MAX_RECORDS: usize = 200;
|
const DEFAULT_MAX_RECORDS: usize = 200;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,20 @@ use tokio::net::TcpListener;
|
||||||
use tokio_tungstenite::connect_async;
|
use tokio_tungstenite::connect_async;
|
||||||
use tokio_tungstenite::tungstenite::Message as TungsteniteMessage;
|
use tokio_tungstenite::tungstenite::Message as TungsteniteMessage;
|
||||||
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||||
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use webauthn_rs::prelude::{
|
||||||
|
CreationChallengeResponse, Passkey, PasskeyAuthentication, PasskeyRegistration,
|
||||||
|
PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse, Webauthn,
|
||||||
|
WebauthnBuilder,
|
||||||
|
};
|
||||||
use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest};
|
use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest};
|
||||||
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
|
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
|
||||||
|
|
||||||
use crate::auth::{
|
use crate::auth::{
|
||||||
AuthPublicConfig, AuthenticatedUser, RequestActor, auth_error, is_expired, mint_secret,
|
AuthPublicConfig, AuthenticatedUser, RequestActor, auth_error, is_expired, mint_secret, new_id,
|
||||||
new_challenge, new_id, new_user_code, normalize_handle, resolve_request_actor, rfc3339_after,
|
new_user_code, normalize_handle, resolve_request_actor, rfc3339_after, session_set_cookie,
|
||||||
session_set_cookie, token_hash,
|
token_hash,
|
||||||
};
|
};
|
||||||
use crate::companion::{
|
use crate::companion::{
|
||||||
CompanionCancelRequest, CompanionConsole, CompanionMessageRequest, CompanionMessageResponse,
|
CompanionCancelRequest, CompanionConsole, CompanionMessageRequest, CompanionMessageResponse,
|
||||||
|
|
@ -138,9 +145,9 @@ impl ServerConfig {
|
||||||
embedded_runtime_store_root,
|
embedded_runtime_store_root,
|
||||||
static_assets_dir: None,
|
static_assets_dir: None,
|
||||||
auth: AuthConfig::Passkey {
|
auth: AuthConfig::Passkey {
|
||||||
rp_id: "127.0.0.1".to_string(),
|
rp_id: "localhost".to_string(),
|
||||||
origin: "http://127.0.0.1:8787".to_string(),
|
origin: "http://localhost:8787".to_string(),
|
||||||
public_base_url: "http://127.0.0.1:8787".to_string(),
|
public_base_url: "http://localhost:8787".to_string(),
|
||||||
cookie_name: "yoi_workspace_session".to_string(),
|
cookie_name: "yoi_workspace_session".to_string(),
|
||||||
},
|
},
|
||||||
max_records: 200,
|
max_records: 200,
|
||||||
|
|
@ -2503,36 +2510,15 @@ struct PasskeyRegistrationOptionsRequest {
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct PasskeyRegistrationOptionsResponse {
|
struct PasskeyRegistrationOptionsResponse {
|
||||||
challenge: String,
|
challenge_id: String,
|
||||||
rp: PublicKeyCredentialRpEntity,
|
public_key: CreationChallengeResponse,
|
||||||
user: PublicKeyCredentialUserEntity,
|
|
||||||
timeout_ms: u64,
|
|
||||||
attestation: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct PublicKeyCredentialRpEntity {
|
|
||||||
id: String,
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct PublicKeyCredentialUserEntity {
|
|
||||||
id: String,
|
|
||||||
name: String,
|
|
||||||
display_name: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct PasskeyRegistrationCompleteRequest {
|
struct PasskeyRegistrationCompleteRequest {
|
||||||
challenge: String,
|
challenge_id: String,
|
||||||
credential_id: String,
|
credential: RegisterPublicKeyCredential,
|
||||||
public_key_cose: String,
|
|
||||||
#[serde(default)]
|
|
||||||
transports: Vec<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
sign_count: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -2544,23 +2530,15 @@ struct PasskeyLoginOptionsRequest {
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct PasskeyLoginOptionsResponse {
|
struct PasskeyLoginOptionsResponse {
|
||||||
challenge: String,
|
challenge_id: String,
|
||||||
rp_id: String,
|
public_key: RequestChallengeResponse,
|
||||||
timeout_ms: u64,
|
|
||||||
allow_credentials: Vec<PasskeyAllowedCredential>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct PasskeyAllowedCredential {
|
|
||||||
credential_id: String,
|
|
||||||
transports: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct PasskeyLoginCompleteRequest {
|
struct PasskeyLoginCompleteRequest {
|
||||||
challenge: String,
|
challenge_id: String,
|
||||||
credential_id: String,
|
credential: PublicKeyCredential,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
|
@ -2634,32 +2612,41 @@ async fn post_passkey_registration_options(
|
||||||
Json(request): Json<PasskeyRegistrationOptionsRequest>,
|
Json(request): Json<PasskeyRegistrationOptionsRequest>,
|
||||||
) -> ApiResult<Json<PasskeyRegistrationOptionsResponse>> {
|
) -> ApiResult<Json<PasskeyRegistrationOptionsResponse>> {
|
||||||
let user = ensure_user_account(&api, &request.handle, request.display_name.as_deref())?;
|
let user = ensure_user_account(&api, &request.handle, request.display_name.as_deref())?;
|
||||||
|
let webauthn = webauthn(&api.config)?;
|
||||||
|
let exclude_credentials = passkeys_for_user(&api, &user.user_id)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|passkey| passkey.cred_id().clone())
|
||||||
|
.collect();
|
||||||
|
let user_unique_id = Uuid::now_v7();
|
||||||
|
let (public_key, state) = webauthn
|
||||||
|
.start_passkey_registration(
|
||||||
|
user_unique_id,
|
||||||
|
&user.handle,
|
||||||
|
&user.display_name,
|
||||||
|
Some(exclude_credentials),
|
||||||
|
)
|
||||||
|
.map_err(|error| auth_error("webauthn_registration_options_failed", &error.to_string()))?;
|
||||||
|
let challenge_id = new_id("webauthn-registration");
|
||||||
let auth = auth_public_config(&api.config);
|
let auth = auth_public_config(&api.config);
|
||||||
let challenge = new_challenge();
|
|
||||||
api.store.put_auth_challenge(&AuthChallengeRecord {
|
api.store.put_auth_challenge(&AuthChallengeRecord {
|
||||||
challenge_id: new_id("auth-challenge"),
|
challenge_id: challenge_id.clone(),
|
||||||
ceremony: "passkey_registration".to_string(),
|
ceremony: "passkey_registration".to_string(),
|
||||||
challenge: challenge.clone(),
|
challenge: challenge_id.clone(),
|
||||||
user_id: Some(user.user_id.clone()),
|
user_id: Some(user.user_id),
|
||||||
rp_id: auth.rp_id.clone(),
|
rp_id: auth.rp_id,
|
||||||
origin: auth.origin.clone(),
|
origin: auth.origin,
|
||||||
|
state_json: Some(
|
||||||
|
serde_json::to_string(&state).map_err(|error| {
|
||||||
|
auth_error("webauthn_state_serialize_failed", &error.to_string())
|
||||||
|
})?,
|
||||||
|
),
|
||||||
expires_at: rfc3339_after(Duration::minutes(5)),
|
expires_at: rfc3339_after(Duration::minutes(5)),
|
||||||
created_at: crate::auth::now_rfc3339(),
|
created_at: crate::auth::now_rfc3339(),
|
||||||
consumed_at: None,
|
consumed_at: None,
|
||||||
})?;
|
})?;
|
||||||
Ok(Json(PasskeyRegistrationOptionsResponse {
|
Ok(Json(PasskeyRegistrationOptionsResponse {
|
||||||
challenge,
|
challenge_id,
|
||||||
rp: PublicKeyCredentialRpEntity {
|
public_key,
|
||||||
id: auth.rp_id,
|
|
||||||
name: "Yoi Workspace".to_string(),
|
|
||||||
},
|
|
||||||
user: PublicKeyCredentialUserEntity {
|
|
||||||
id: user.user_id,
|
|
||||||
name: user.handle,
|
|
||||||
display_name: user.display_name,
|
|
||||||
},
|
|
||||||
timeout_ms: 300_000,
|
|
||||||
attestation: "none".to_string(),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2669,8 +2656,8 @@ async fn post_passkey_registration_complete(
|
||||||
) -> ApiResult<Json<AuthUserResponse>> {
|
) -> ApiResult<Json<AuthUserResponse>> {
|
||||||
let challenge = api
|
let challenge = api
|
||||||
.store
|
.store
|
||||||
.consume_auth_challenge(
|
.consume_auth_challenge_by_id(
|
||||||
&request.challenge,
|
&request.challenge_id,
|
||||||
"passkey_registration",
|
"passkey_registration",
|
||||||
&crate::auth::now_rfc3339(),
|
&crate::auth::now_rfc3339(),
|
||||||
)?
|
)?
|
||||||
|
|
@ -2699,21 +2686,33 @@ async fn post_passkey_registration_complete(
|
||||||
"passkey registration user does not exist",
|
"passkey registration user does not exist",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let transports_json = if request.transports.is_empty() {
|
let state_json = challenge.state_json.ok_or_else(|| {
|
||||||
None
|
auth_error(
|
||||||
} else {
|
"missing_webauthn_state",
|
||||||
Some(
|
"passkey registration state was not persisted",
|
||||||
serde_json::to_string(&request.transports)
|
|
||||||
.map_err(|error| auth_error("invalid_passkey_transports", &error.to_string()))?,
|
|
||||||
)
|
)
|
||||||
};
|
})?;
|
||||||
|
let state: PasskeyRegistration = serde_json::from_str(&state_json)
|
||||||
|
.map_err(|error| auth_error("webauthn_state_deserialize_failed", &error.to_string()))?;
|
||||||
|
let webauthn = webauthn(&api.config)?;
|
||||||
|
let passkey = webauthn
|
||||||
|
.finish_passkey_registration(&request.credential, &state)
|
||||||
|
.map_err(|error| {
|
||||||
|
auth_error(
|
||||||
|
"webauthn_registration_verification_failed",
|
||||||
|
&error.to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let credential_id = passkey_credential_id(&passkey)?;
|
||||||
api.store
|
api.store
|
||||||
.upsert_passkey_credential(&PasskeyCredentialRecord {
|
.upsert_passkey_credential(&PasskeyCredentialRecord {
|
||||||
credential_id: request.credential_id,
|
credential_id,
|
||||||
user_id: user.user_id.clone(),
|
user_id: user.user_id.clone(),
|
||||||
public_key_cose: request.public_key_cose,
|
public_key_cose: serde_json::to_string(&passkey).map_err(|error| {
|
||||||
transports_json,
|
auth_error("webauthn_passkey_serialize_failed", &error.to_string())
|
||||||
sign_count: request.sign_count,
|
})?,
|
||||||
|
transports_json: None,
|
||||||
|
sign_count: 0,
|
||||||
created_at: crate::auth::now_rfc3339(),
|
created_at: crate::auth::now_rfc3339(),
|
||||||
last_used_at: None,
|
last_used_at: None,
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -2731,41 +2730,39 @@ async fn post_passkey_login_options(
|
||||||
None => api.store.any_user()?,
|
None => api.store.any_user()?,
|
||||||
}
|
}
|
||||||
.ok_or_else(|| auth_error("unknown_auth_user", "no matching user account exists"))?;
|
.ok_or_else(|| auth_error("unknown_auth_user", "no matching user account exists"))?;
|
||||||
let credentials = api.store.list_passkey_credentials_for_user(&user.user_id)?;
|
let passkeys = passkeys_for_user(&api, &user.user_id)?;
|
||||||
if credentials.is_empty() {
|
if passkeys.is_empty() {
|
||||||
return Err(auth_error(
|
return Err(auth_error(
|
||||||
"passkey_not_registered",
|
"passkey_not_registered",
|
||||||
"user has no registered passkey credentials",
|
"user has no registered passkey credentials",
|
||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
let webauthn = webauthn(&api.config)?;
|
||||||
|
let (public_key, state) = webauthn
|
||||||
|
.start_passkey_authentication(&passkeys)
|
||||||
|
.map_err(|error| auth_error("webauthn_login_options_failed", &error.to_string()))?;
|
||||||
|
let challenge_id = new_id("webauthn-login");
|
||||||
let auth = auth_public_config(&api.config);
|
let auth = auth_public_config(&api.config);
|
||||||
let challenge = new_challenge();
|
|
||||||
api.store.put_auth_challenge(&AuthChallengeRecord {
|
api.store.put_auth_challenge(&AuthChallengeRecord {
|
||||||
challenge_id: new_id("auth-challenge"),
|
challenge_id: challenge_id.clone(),
|
||||||
ceremony: "passkey_login".to_string(),
|
ceremony: "passkey_login".to_string(),
|
||||||
challenge: challenge.clone(),
|
challenge: challenge_id.clone(),
|
||||||
user_id: Some(user.user_id),
|
user_id: Some(user.user_id),
|
||||||
rp_id: auth.rp_id.clone(),
|
rp_id: auth.rp_id,
|
||||||
origin: auth.origin,
|
origin: auth.origin,
|
||||||
|
state_json: Some(
|
||||||
|
serde_json::to_string(&state).map_err(|error| {
|
||||||
|
auth_error("webauthn_state_serialize_failed", &error.to_string())
|
||||||
|
})?,
|
||||||
|
),
|
||||||
expires_at: rfc3339_after(Duration::minutes(5)),
|
expires_at: rfc3339_after(Duration::minutes(5)),
|
||||||
created_at: crate::auth::now_rfc3339(),
|
created_at: crate::auth::now_rfc3339(),
|
||||||
consumed_at: None,
|
consumed_at: None,
|
||||||
})?;
|
})?;
|
||||||
Ok(Json(PasskeyLoginOptionsResponse {
|
Ok(Json(PasskeyLoginOptionsResponse {
|
||||||
challenge,
|
challenge_id,
|
||||||
rp_id: auth.rp_id,
|
public_key,
|
||||||
timeout_ms: 300_000,
|
|
||||||
allow_credentials: credentials
|
|
||||||
.into_iter()
|
|
||||||
.map(|credential| PasskeyAllowedCredential {
|
|
||||||
credential_id: credential.credential_id,
|
|
||||||
transports: credential
|
|
||||||
.transports_json
|
|
||||||
.and_then(|raw| serde_json::from_str(&raw).ok())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2775,8 +2772,8 @@ async fn post_passkey_login_complete(
|
||||||
) -> ApiResult<Response> {
|
) -> ApiResult<Response> {
|
||||||
let challenge = api
|
let challenge = api
|
||||||
.store
|
.store
|
||||||
.consume_auth_challenge(
|
.consume_auth_challenge_by_id(
|
||||||
&request.challenge,
|
&request.challenge_id,
|
||||||
"passkey_login",
|
"passkey_login",
|
||||||
&crate::auth::now_rfc3339(),
|
&crate::auth::now_rfc3339(),
|
||||||
)?
|
)?
|
||||||
|
|
@ -2793,26 +2790,63 @@ async fn post_passkey_login_complete(
|
||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
let credential = api
|
let user_id = challenge.user_id.ok_or_else(|| {
|
||||||
|
auth_error(
|
||||||
|
"invalid_passkey_challenge",
|
||||||
|
"passkey login challenge is not bound to a user",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let user = api
|
||||||
.store
|
.store
|
||||||
.get_passkey_credential(&request.credential_id)?
|
.get_user(&user_id)?
|
||||||
|
.ok_or_else(|| auth_error("unknown_auth_user", "passkey user does not exist"))?;
|
||||||
|
let state_json = challenge.state_json.ok_or_else(|| {
|
||||||
|
auth_error(
|
||||||
|
"missing_webauthn_state",
|
||||||
|
"passkey login state was not persisted",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let state: PasskeyAuthentication = serde_json::from_str(&state_json)
|
||||||
|
.map_err(|error| auth_error("webauthn_state_deserialize_failed", &error.to_string()))?;
|
||||||
|
let webauthn = webauthn(&api.config)?;
|
||||||
|
let auth_result = webauthn
|
||||||
|
.finish_passkey_authentication(&request.credential, &state)
|
||||||
|
.map_err(|error| auth_error("webauthn_login_verification_failed", &error.to_string()))?;
|
||||||
|
let credential_id = serde_json::to_value(auth_result.cred_id())
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.as_str().map(ToOwned::to_owned))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
auth_error(
|
||||||
|
"invalid_webauthn_credential_id",
|
||||||
|
"verified credential id was not serializable",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let stored = api
|
||||||
|
.store
|
||||||
|
.get_passkey_credential(&credential_id)?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
auth_error(
|
auth_error(
|
||||||
"unknown_passkey_credential",
|
"unknown_passkey_credential",
|
||||||
"passkey credential is not registered",
|
"verified passkey credential is not registered",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
if Some(credential.user_id.clone()) != challenge.user_id {
|
if stored.user_id != user.user_id {
|
||||||
return Err(auth_error(
|
return Err(auth_error(
|
||||||
"passkey_user_mismatch",
|
"passkey_user_mismatch",
|
||||||
"passkey credential does not belong to the challenged user",
|
"verified passkey credential does not belong to the challenged user",
|
||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
let user = api
|
api.store
|
||||||
.store
|
.upsert_passkey_credential(&PasskeyCredentialRecord {
|
||||||
.get_user(&credential.user_id)?
|
credential_id,
|
||||||
.ok_or_else(|| auth_error("unknown_auth_user", "passkey user does not exist"))?;
|
user_id: user.user_id.clone(),
|
||||||
|
public_key_cose: stored.public_key_cose,
|
||||||
|
transports_json: stored.transports_json,
|
||||||
|
sign_count: u64::from(auth_result.counter()),
|
||||||
|
created_at: stored.created_at,
|
||||||
|
last_used_at: Some(crate::auth::now_rfc3339()),
|
||||||
|
})?;
|
||||||
let session_token = mint_secret("yoi_sess");
|
let session_token = mint_secret("yoi_sess");
|
||||||
api.store.create_browser_session(&BrowserSessionRecord {
|
api.store.create_browser_session(&BrowserSessionRecord {
|
||||||
session_id: new_id("session"),
|
session_id: new_id("session"),
|
||||||
|
|
@ -2989,6 +3023,42 @@ async fn get_auth_whoami(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn webauthn(config: &ServerConfig) -> ApiResult<Webauthn> {
|
||||||
|
let auth = auth_public_config(config);
|
||||||
|
let origin = Url::parse(&auth.origin)
|
||||||
|
.map_err(|error| auth_error("invalid_webauthn_origin", &error.to_string()))?;
|
||||||
|
WebauthnBuilder::new(&auth.rp_id, &origin)
|
||||||
|
.map_err(|error| auth_error("webauthn_builder_failed", &error.to_string()))?
|
||||||
|
.rp_name("Yoi Workspace")
|
||||||
|
.build()
|
||||||
|
.map_err(|error| auth_error("webauthn_builder_failed", &error.to_string()).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn passkeys_for_user(api: &WorkspaceApi, user_id: &str) -> ApiResult<Vec<Passkey>> {
|
||||||
|
api.store
|
||||||
|
.list_passkey_credentials_for_user(user_id)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|record| {
|
||||||
|
serde_json::from_str::<Passkey>(&record.public_key_cose).map_err(|error| {
|
||||||
|
auth_error("webauthn_passkey_deserialize_failed", &error.to_string()).into()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn passkey_credential_id(passkey: &Passkey) -> ApiResult<String> {
|
||||||
|
serde_json::to_value(passkey.cred_id())
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.as_str().map(ToOwned::to_owned))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
auth_error(
|
||||||
|
"invalid_webauthn_credential_id",
|
||||||
|
"verified passkey credential id was not serializable",
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn auth_public_config(config: &ServerConfig) -> AuthPublicConfig {
|
fn auth_public_config(config: &ServerConfig) -> AuthPublicConfig {
|
||||||
match &config.auth {
|
match &config.auth {
|
||||||
AuthConfig::Passkey {
|
AuthConfig::Passkey {
|
||||||
|
|
@ -8551,7 +8621,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn auth_passkey_session_and_device_login_flow_round_trip() {
|
async fn passkey_registration_rejects_unverified_credential_response() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let app = test_app(dir.path()).await;
|
let app = test_app(dir.path()).await;
|
||||||
|
|
||||||
|
|
@ -8564,42 +8634,28 @@ mod tests {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let registration_challenge = registration_options["challenge"].as_str().unwrap();
|
assert!(registration_options["public_key"].is_object());
|
||||||
let registered = post_json(
|
let challenge_id = registration_options["challenge_id"].as_str().unwrap();
|
||||||
app.clone(),
|
|
||||||
"/api/auth/passkeys/registration/complete",
|
|
||||||
json!({
|
|
||||||
"challenge": registration_challenge,
|
|
||||||
"credential_id": "credential-1",
|
|
||||||
"public_key_cose": "public-key-cose",
|
|
||||||
"transports": ["internal"]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(registered["user"]["handle"], "alice");
|
|
||||||
|
|
||||||
let login_options = post_json(
|
let response = app
|
||||||
app.clone(),
|
|
||||||
"/api/auth/passkeys/login/options",
|
|
||||||
json!({ "handle": "alice" }),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(
|
|
||||||
login_options["allow_credentials"][0]["credential_id"],
|
|
||||||
"credential-1"
|
|
||||||
);
|
|
||||||
let login_challenge = login_options["challenge"].as_str().unwrap();
|
|
||||||
let login_response = app
|
|
||||||
.clone()
|
.clone()
|
||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.method("POST")
|
.method("POST")
|
||||||
.uri("/api/auth/passkeys/login/complete")
|
.uri("/api/auth/passkeys/registration/complete")
|
||||||
.header(CONTENT_TYPE, "application/json")
|
.header(CONTENT_TYPE, "application/json")
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
serde_json::to_vec(&json!({
|
serde_json::to_vec(&json!({
|
||||||
"challenge": login_challenge,
|
"challenge_id": challenge_id,
|
||||||
"credential_id": "credential-1"
|
"credential": {
|
||||||
|
"id": "credential-1",
|
||||||
|
"rawId": "Y3JlZGVudGlhbC0x",
|
||||||
|
"type": "public-key",
|
||||||
|
"response": {
|
||||||
|
"clientDataJSON": "e30",
|
||||||
|
"attestationObject": "e30"
|
||||||
|
}
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
))
|
))
|
||||||
|
|
@ -8607,71 +8663,22 @@ mod tests {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(login_response.status(), StatusCode::OK);
|
assert_ne!(response.status(), StatusCode::OK);
|
||||||
let cookie = login_response
|
|
||||||
.headers()
|
|
||||||
.get(SET_COOKIE)
|
|
||||||
.unwrap()
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
let login_bytes = to_bytes(login_response.into_body(), usize::MAX)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let login_json: Value = serde_json::from_slice(&login_bytes).unwrap();
|
|
||||||
assert_eq!(login_json["user"]["handle"], "alice");
|
|
||||||
|
|
||||||
let device = post_json(
|
let login_without_registered_passkey = app
|
||||||
app.clone(),
|
|
||||||
"/api/auth/device-login/start",
|
|
||||||
json!({ "client_name": "test cli" }),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
let approved = app
|
|
||||||
.clone()
|
|
||||||
.oneshot(
|
.oneshot(
|
||||||
Request::builder()
|
Request::builder()
|
||||||
.method("POST")
|
.method("POST")
|
||||||
.uri("/api/auth/device-login/approve")
|
.uri("/api/auth/passkeys/login/options")
|
||||||
.header(CONTENT_TYPE, "application/json")
|
.header(CONTENT_TYPE, "application/json")
|
||||||
.header("Cookie", cookie)
|
|
||||||
.body(Body::from(
|
.body(Body::from(
|
||||||
serde_json::to_vec(&json!({
|
serde_json::to_vec(&json!({ "handle": "alice" })).unwrap(),
|
||||||
"user_code": device["user_code"].as_str().unwrap()
|
|
||||||
}))
|
|
||||||
.unwrap(),
|
|
||||||
))
|
))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(approved.status(), StatusCode::OK);
|
assert_ne!(login_without_registered_passkey.status(), StatusCode::OK);
|
||||||
|
|
||||||
let polled = post_json(
|
|
||||||
app.clone(),
|
|
||||||
"/api/auth/device-login/poll",
|
|
||||||
json!({ "device_code": device["device_code"].as_str().unwrap() }),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(polled["status"], "approved");
|
|
||||||
let access_token = polled["access_token"].as_str().unwrap();
|
|
||||||
|
|
||||||
let whoami = app
|
|
||||||
.oneshot(
|
|
||||||
Request::builder()
|
|
||||||
.method("GET")
|
|
||||||
.uri("/api/auth/whoami")
|
|
||||||
.header("Authorization", format!("Bearer {access_token}"))
|
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(whoami.status(), StatusCode::OK);
|
|
||||||
let whoami_bytes = to_bytes(whoami.into_body(), usize::MAX).await.unwrap();
|
|
||||||
let whoami_json: Value = serde_json::from_slice(&whoami_bytes).unwrap();
|
|
||||||
assert_eq!(whoami_json["actor"]["handle"], "alice");
|
|
||||||
assert_eq!(whoami_json["actor"]["auth_method"], "api_token");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_json(app: Router, uri: &str) -> Value {
|
async fn get_json(app: Router, uri: &str) -> Value {
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,11 @@ const MIGRATIONS: &[Migration] = &[
|
||||||
name: "account identity and login flow schema",
|
name: "account identity and login flow schema",
|
||||||
apply: create_account_identity_tables,
|
apply: create_account_identity_tables,
|
||||||
},
|
},
|
||||||
|
Migration {
|
||||||
|
version: 9,
|
||||||
|
name: "webauthn challenge state",
|
||||||
|
apply: add_webauthn_challenge_state,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
struct Migration {
|
struct Migration {
|
||||||
|
|
@ -116,6 +121,7 @@ pub struct AuthChallengeRecord {
|
||||||
pub user_id: Option<String>,
|
pub user_id: Option<String>,
|
||||||
pub rp_id: String,
|
pub rp_id: String,
|
||||||
pub origin: String,
|
pub origin: String,
|
||||||
|
pub state_json: Option<String>,
|
||||||
pub expires_at: String,
|
pub expires_at: String,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub consumed_at: Option<String>,
|
pub consumed_at: Option<String>,
|
||||||
|
|
@ -229,6 +235,12 @@ pub trait ControlPlaneStore: Send + Sync {
|
||||||
ceremony: &str,
|
ceremony: &str,
|
||||||
consumed_at: &str,
|
consumed_at: &str,
|
||||||
) -> Result<Option<AuthChallengeRecord>>;
|
) -> Result<Option<AuthChallengeRecord>>;
|
||||||
|
fn consume_auth_challenge_by_id(
|
||||||
|
&self,
|
||||||
|
challenge_id: &str,
|
||||||
|
ceremony: &str,
|
||||||
|
consumed_at: &str,
|
||||||
|
) -> Result<Option<AuthChallengeRecord>>;
|
||||||
fn create_browser_session(&self, record: &BrowserSessionRecord) -> Result<()>;
|
fn create_browser_session(&self, record: &BrowserSessionRecord) -> Result<()>;
|
||||||
fn resolve_browser_session(&self, token_hash: &str) -> Result<Option<BrowserSessionRecord>>;
|
fn resolve_browser_session(&self, token_hash: &str) -> Result<Option<BrowserSessionRecord>>;
|
||||||
fn create_api_token(&self, record: &ApiTokenRecord) -> Result<()>;
|
fn create_api_token(&self, record: &ApiTokenRecord) -> Result<()>;
|
||||||
|
|
@ -524,9 +536,9 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
fn put_auth_challenge(&self, record: &AuthChallengeRecord) -> Result<()> {
|
fn put_auth_challenge(&self, record: &AuthChallengeRecord) -> Result<()> {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
r#"INSERT INTO auth_challenges (challenge_id, ceremony, challenge, user_id, rp_id, origin, expires_at, created_at, consumed_at)
|
r#"INSERT INTO auth_challenges (challenge_id, ceremony, challenge, user_id, rp_id, origin, state_json, expires_at, created_at, consumed_at)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)"#,
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)"#,
|
||||||
params![record.challenge_id, record.ceremony, record.challenge, record.user_id, record.rp_id, record.origin, record.expires_at, record.created_at, record.consumed_at],
|
params![record.challenge_id, record.ceremony, record.challenge, record.user_id, record.rp_id, record.origin, record.state_json, record.expires_at, record.created_at, record.consumed_at],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|
@ -559,6 +571,33 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn consume_auth_challenge_by_id(
|
||||||
|
&self,
|
||||||
|
challenge_id: &str,
|
||||||
|
ceremony: &str,
|
||||||
|
consumed_at: &str,
|
||||||
|
) -> Result<Option<AuthChallengeRecord>> {
|
||||||
|
self.with_conn(|conn| {
|
||||||
|
let record = conn
|
||||||
|
.query_row(
|
||||||
|
auth_challenge_select_sql(
|
||||||
|
"WHERE challenge_id = ?1 AND ceremony = ?2 AND consumed_at IS NULL",
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
params![challenge_id, ceremony],
|
||||||
|
read_auth_challenge_record,
|
||||||
|
)
|
||||||
|
.optional()?;
|
||||||
|
if let Some(record) = record.as_ref() {
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE auth_challenges SET consumed_at = ?2 WHERE challenge_id = ?1",
|
||||||
|
params![record.challenge_id, consumed_at],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(record)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn create_browser_session(&self, record: &BrowserSessionRecord) -> Result<()> {
|
fn create_browser_session(&self, record: &BrowserSessionRecord) -> Result<()> {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|
@ -1025,7 +1064,7 @@ fn read_passkey_credential_record(
|
||||||
|
|
||||||
fn auth_challenge_select_sql(where_clause: &str) -> String {
|
fn auth_challenge_select_sql(where_clause: &str) -> String {
|
||||||
format!(
|
format!(
|
||||||
"SELECT challenge_id, ceremony, challenge, user_id, rp_id, origin, expires_at, created_at, consumed_at FROM auth_challenges {where_clause}"
|
"SELECT challenge_id, ceremony, challenge, user_id, rp_id, origin, state_json, expires_at, created_at, consumed_at FROM auth_challenges {where_clause}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1037,9 +1076,10 @@ fn read_auth_challenge_record(row: &rusqlite::Row<'_>) -> rusqlite::Result<AuthC
|
||||||
user_id: row.get(3)?,
|
user_id: row.get(3)?,
|
||||||
rp_id: row.get(4)?,
|
rp_id: row.get(4)?,
|
||||||
origin: row.get(5)?,
|
origin: row.get(5)?,
|
||||||
expires_at: row.get(6)?,
|
state_json: row.get(6)?,
|
||||||
created_at: row.get(7)?,
|
expires_at: row.get(7)?,
|
||||||
consumed_at: row.get(8)?,
|
created_at: row.get(8)?,
|
||||||
|
consumed_at: row.get(9)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1226,6 +1266,13 @@ CREATE INDEX IF NOT EXISTS idx_worker_workdir_links_worker
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_webauthn_challenge_state(conn: &Connection) -> Result<()> {
|
||||||
|
if !column_exists(conn, "auth_challenges", "state_json")? {
|
||||||
|
conn.execute_batch("ALTER TABLE auth_challenges ADD COLUMN state_json TEXT;")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn create_account_identity_tables(conn: &Connection) -> Result<()> {
|
fn create_account_identity_tables(conn: &Connection) -> Result<()> {
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -1265,6 +1312,7 @@ CREATE TABLE IF NOT EXISTS auth_challenges (
|
||||||
user_id TEXT REFERENCES users(user_id) ON DELETE CASCADE,
|
user_id TEXT REFERENCES users(user_id) ON DELETE CASCADE,
|
||||||
rp_id TEXT NOT NULL,
|
rp_id TEXT NOT NULL,
|
||||||
origin TEXT NOT NULL,
|
origin TEXT NOT NULL,
|
||||||
|
state_json TEXT,
|
||||||
expires_at TEXT NOT NULL,
|
expires_at TEXT NOT NULL,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
consumed_at TEXT
|
consumed_at TEXT
|
||||||
|
|
@ -1897,7 +1945,7 @@ mod tests {
|
||||||
let db = dir.path().join("control-plane.sqlite");
|
let db = dir.path().join("control-plane.sqlite");
|
||||||
let store = SqliteWorkspaceStore::open(&db).unwrap();
|
let store = SqliteWorkspaceStore::open(&db).unwrap();
|
||||||
|
|
||||||
assert_eq!(store.schema_version().await.unwrap(), 8);
|
assert_eq!(store.schema_version().await.unwrap(), 9);
|
||||||
|
|
||||||
let record = WorkspaceRecord {
|
let record = WorkspaceRecord {
|
||||||
workspace_id: "local-dev".to_string(),
|
workspace_id: "local-dev".to_string(),
|
||||||
|
|
@ -1910,7 +1958,7 @@ mod tests {
|
||||||
store.upsert_workspace(&record).await.unwrap();
|
store.upsert_workspace(&record).await.unwrap();
|
||||||
|
|
||||||
let reopened = SqliteWorkspaceStore::open(&db).unwrap();
|
let reopened = SqliteWorkspaceStore::open(&db).unwrap();
|
||||||
assert_eq!(reopened.schema_version().await.unwrap(), 8);
|
assert_eq!(reopened.schema_version().await.unwrap(), 9);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
reopened.get_workspace("local-dev").await.unwrap(),
|
reopened.get_workspace("local-dev").await.unwrap(),
|
||||||
Some(record)
|
Some(record)
|
||||||
|
|
@ -2127,7 +2175,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let store = SqliteWorkspaceStore::from_connection(conn).unwrap();
|
let store = SqliteWorkspaceStore::from_connection(conn).unwrap();
|
||||||
assert_eq!(store.schema_version().await.unwrap(), 8);
|
assert_eq!(store.schema_version().await.unwrap(), 9);
|
||||||
|
|
||||||
store
|
store
|
||||||
.with_conn(|conn| {
|
.with_conn(|conn| {
|
||||||
|
|
@ -2317,7 +2365,7 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn account_and_login_records_round_trip() {
|
async fn account_and_login_records_round_trip() {
|
||||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||||
assert_eq!(store.schema_version().await.unwrap(), 8);
|
assert_eq!(store.schema_version().await.unwrap(), 9);
|
||||||
let now = "2026-07-22T00:00:00Z".to_string();
|
let now = "2026-07-22T00:00:00Z".to_string();
|
||||||
let account = AccountRecord {
|
let account = AccountRecord {
|
||||||
account_id: "acct-user-alice".to_string(),
|
account_id: "acct-user-alice".to_string(),
|
||||||
|
|
@ -2388,6 +2436,7 @@ mod tests {
|
||||||
user_id: Some(user.user_id.clone()),
|
user_id: Some(user.user_id.clone()),
|
||||||
rp_id: "127.0.0.1".to_string(),
|
rp_id: "127.0.0.1".to_string(),
|
||||||
origin: "http://127.0.0.1:8787".to_string(),
|
origin: "http://127.0.0.1:8787".to_string(),
|
||||||
|
state_json: None,
|
||||||
expires_at: "2026-07-22T00:05:00Z".to_string(),
|
expires_at: "2026-07-22T00:05:00Z".to_string(),
|
||||||
created_at: now.clone(),
|
created_at: now.clone(),
|
||||||
consumed_at: None,
|
consumed_at: None,
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,9 @@ Human browser auth is modeled as Passkey/WebAuthn ceremony endpoints:
|
||||||
- `POST /api/auth/passkeys/login/complete`
|
- `POST /api/auth/passkeys/login/complete`
|
||||||
- `GET /api/auth/whoami`
|
- `GET /api/auth/whoami`
|
||||||
|
|
||||||
The store persists registration/login challenges, passkey credential IDs, COSE public key payloads, transports, browser sessions, and challenge consumption. Successful login mints an HttpOnly `SameSite=Lax` cookie session.
|
The store persists registration/login challenges, serialized WebAuthn ceremony state, verified passkey credentials, browser sessions, and challenge consumption. Successful login mints an HttpOnly `SameSite=Lax` cookie session.
|
||||||
|
|
||||||
Current boundary: the API and persistence shape are WebAuthn-oriented, but cryptographic attestation/assertion verification is not implemented in this step. The completion endpoints consume the server-issued challenge and validate credential/user binding. A follow-up should wire a WebAuthn verifier crate and replace the provisional completion payloads with verified browser `PublicKeyCredential` responses before this is exposed outside trusted development use.
|
Completion endpoints require browser `PublicKeyCredential` responses and verify them with `webauthn-rs` before a passkey is stored or a browser session is issued. Fake or stale credential responses are rejected by the verifier, and consumed challenges cannot be reused.
|
||||||
|
|
||||||
## CLI/TUI device login
|
## CLI/TUI device login
|
||||||
|
|
||||||
|
|
@ -52,4 +52,3 @@ The resolved actor includes `user_id`, `account_id`, `handle`, `display_name`, a
|
||||||
- Organization membership and RBAC.
|
- Organization membership and RBAC.
|
||||||
- Password login, OAuth login, or other human auth methods.
|
- Password login, OAuth login, or other human auth methods.
|
||||||
- Token scoping beyond Backend API bearer authentication.
|
- Token scoping beyond Backend API bearer authentication.
|
||||||
- Production WebAuthn cryptographic verification; the schema/API are prepared for it, but the verifier integration remains follow-up work.
|
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,9 @@ max_records = 200
|
||||||
[auth]
|
[auth]
|
||||||
# WebAuthn / Passkey relying-party settings. For local development keep rp_id
|
# WebAuthn / Passkey relying-party settings. For local development keep rp_id
|
||||||
# aligned with the browser host in public_base_url/origin.
|
# aligned with the browser host in public_base_url/origin.
|
||||||
rp_id = "127.0.0.1"
|
rp_id = "localhost"
|
||||||
origin = "http://127.0.0.1:8787"
|
origin = "http://localhost:8787"
|
||||||
public_base_url = "http://127.0.0.1:8787"
|
public_base_url = "http://localhost:8787"
|
||||||
cookie_name = "yoi_workspace_session"
|
cookie_name = "yoi_workspace_session"
|
||||||
|
|
||||||
# Repository registry. Browser/API repository projection reads only configured
|
# Repository registry. Browser/API repository projection reads only configured
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user