about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2021-12-06 20:39:04 +0300
committerVika <vika@fireburn.ru>2021-12-06 20:42:50 +0300
commit9a9776230ce8d12d305ca8db19cc76f20ae40926 (patch)
tree26d1e166ae68825e55084b43c52afcb446521b11
parent913779655c1feddc55d37bebbdd7df87eb7c9c0f (diff)
downloadkittybox-9a9776230ce8d12d305ca8db19cc76f20ae40926.tar.zst
Added support for IndieAuth client sign in
This will allow readers to view private posts intended just for them.

Additionally fixed bugs in patterns due to which webmentions might not
have been sent.
-rw-r--r--Cargo.lock303
-rw-r--r--Cargo.toml3
-rw-r--r--flake.nix11
-rw-r--r--src/database/file/mod.rs2
-rw-r--r--src/frontend/login.rs292
-rw-r--r--src/frontend/mod.rs26
-rw-r--r--src/frontend/templates/mod.rs43
-rw-r--r--src/lib.rs20
-rw-r--r--src/main.rs19
-rw-r--r--src/micropub/post.rs2
10 files changed, 554 insertions, 167 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 659cae5..d4d0544 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -82,9 +82,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.44"
+version = "1.0.51"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
+checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
 
 [[package]]
 name = "arrayref"
@@ -171,18 +171,16 @@ dependencies = [
 
 [[package]]
 name = "async-h1"
-version = "2.3.2"
+version = "2.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc5142de15b549749cce62923a50714b0d7b77f5090ced141599e78899865451"
+checksum = "8101020758a4fc3a7c326cb42aa99e9fa77cbfb76987c128ad956406fe1f70a7"
 dependencies = [
  "async-channel",
  "async-dup",
  "async-std",
- "byte-pool",
  "futures-core",
  "http-types",
  "httparse",
- "lazy_static",
  "log 0.4.14",
  "pin-project",
 ]
@@ -226,9 +224,9 @@ dependencies = [
 
 [[package]]
 name = "async-process"
-version = "1.2.0"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b21b63ab5a0db0369deb913540af2892750e42d949faacc7a61495ac418a1692"
+checksum = "83137067e3a2a6a06d67168e49e68a0957d215410473a740cea95a2425c0b7c6"
 dependencies = [
  "async-io",
  "blocking",
@@ -428,9 +426,9 @@ dependencies = [
 
 [[package]]
 name = "blocking"
-version = "1.0.2"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5e170dbede1f740736619b776d7251cb1b9095c435c34d8ca9f57fcd2f335e9"
+checksum = "046e47d4b2d391b1f6f8b407b1deb8dee56c1852ccd868becf2710f601b5f427"
 dependencies = [
  "async-channel",
  "async-task",
@@ -442,19 +440,9 @@ dependencies = [
 
 [[package]]
 name = "bumpalo"
-version = "3.7.1"
+version = "3.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538"
-
-[[package]]
-name = "byte-pool"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca"
-dependencies = [
- "crossbeam-queue",
- "stable_deref_trait",
-]
+checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
 
 [[package]]
 name = "byteorder"
@@ -476,9 +464,9 @@ checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba"
 
 [[package]]
 name = "cc"
-version = "1.0.70"
+version = "1.0.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
+checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
 
 [[package]]
 name = "cfg-if"
@@ -528,9 +516,9 @@ dependencies = [
 
 [[package]]
 name = "combine"
-version = "4.6.1"
+version = "4.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a909e4d93292cd8e9c42e189f61681eff9d67b6541f96b8a1a737f23737bd001"
+checksum = "b2b2f5d0ee456f3928812dfc8c6d9a1d592b98678f6d56db9b0cd2b7bc6c8db5"
 dependencies = [
  "bytes",
  "futures-core",
@@ -612,9 +600,9 @@ checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba"
 
 [[package]]
 name = "crc32fast"
-version = "1.2.1"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
+checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836"
 dependencies = [
  "cfg-if 1.0.0",
 ]
@@ -716,6 +704,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "data-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
+
+[[package]]
 name = "deadpool"
 version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -731,14 +725,14 @@ dependencies = [
 
 [[package]]
 name = "derive_more"
-version = "0.99.16"
+version = "0.99.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df"
+checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
 dependencies = [
  "convert_case",
  "proc-macro2",
  "quote",
- "rustc_version 0.3.3",
+ "rustc_version 0.4.0",
  "syn",
 ]
 
@@ -800,9 +794,9 @@ dependencies = [
 
 [[package]]
 name = "encoding_rs"
-version = "0.8.28"
+version = "0.8.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065"
+checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
 dependencies = [
  "cfg-if 1.0.0",
 ]
@@ -847,13 +841,13 @@ dependencies = [
 
 [[package]]
 name = "fd-lock"
-version = "3.0.0"
+version = "3.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8806dd91a06a7a403a8e596f9bfbfb34e469efbc363fc9c9713e79e26472e36"
+checksum = "cfc110fe50727d46a428eed832df40affe9bf74d077cac1bf3f2718e823f14c5"
 dependencies = [
  "cfg-if 1.0.0",
  "libc",
- "winapi 0.3.9",
+ "windows-sys",
 ]
 
 [[package]]
@@ -918,9 +912,9 @@ dependencies = [
 
 [[package]]
 name = "futures"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
+checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -933,9 +927,9 @@ dependencies = [
 
 [[package]]
 name = "futures-channel"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
+checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -943,15 +937,15 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
+checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
+checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -960,9 +954,9 @@ dependencies = [
 
 [[package]]
 name = "futures-io"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
+checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11"
 
 [[package]]
 name = "futures-lite"
@@ -981,12 +975,10 @@ dependencies = [
 
 [[package]]
 name = "futures-macro"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb"
+checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd"
 dependencies = [
- "autocfg",
- "proc-macro-hack",
  "proc-macro2",
  "quote",
  "syn",
@@ -994,23 +986,22 @@ dependencies = [
 
 [[package]]
 name = "futures-sink"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
+checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af"
 
 [[package]]
 name = "futures-task"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
+checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
 
 [[package]]
 name = "futures-util"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
+checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
 dependencies = [
- "autocfg",
  "futures-channel",
  "futures-core",
  "futures-io",
@@ -1020,8 +1011,6 @@ dependencies = [
  "memchr 2.4.1",
  "pin-project-lite 0.2.7",
  "pin-utils",
- "proc-macro-hack",
- "proc-macro-nested",
  "slab",
 ]
 
@@ -1078,9 +1067,9 @@ dependencies = [
 
 [[package]]
 name = "gloo-timers"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f"
+checksum = "6f16c88aa13d2656ef20d1c042086b8767bbe2bdb62526894275a1b062161b2e"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -1220,9 +1209,9 @@ checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac"
 
 [[package]]
 name = "instant"
-version = "0.1.11"
+version = "0.1.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
 dependencies = [
  "cfg-if 1.0.0",
 ]
@@ -1260,6 +1249,7 @@ dependencies = [
  "async-std",
  "async-trait",
  "chrono",
+ "data-encoding",
  "easy-scraper",
  "ellipse",
  "env_logger 0.8.4",
@@ -1275,12 +1265,14 @@ dependencies = [
  "newbase60",
  "paste",
  "prometheus",
+ "rand 0.8.4",
  "redis",
  "relative-path",
  "retainer",
  "serde",
  "serde_json",
  "serde_urlencoded",
+ "sha2",
  "surf",
  "tempdir",
  "test-logger",
@@ -1331,9 +1323,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.102"
+version = "0.2.109"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
+checksum = "f98a04dce437184842841303488f70d0188c5f51437d2a834dc097eafa909a01"
 
 [[package]]
 name = "lock_api"
@@ -1583,9 +1575,9 @@ dependencies = [
 
 [[package]]
 name = "paste"
-version = "1.0.5"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58"
+checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5"
 
 [[package]]
 name = "percent-encoding"
@@ -1594,15 +1586,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
 
 [[package]]
-name = "pest"
-version = "2.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
-dependencies = [
- "ucd-trie",
-]
-
-[[package]]
 name = "phf"
 version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1702,9 +1685,9 @@ checksum = "d15b6607fa632996eb8a17c9041cb6071cb75ac057abd45dece578723ea8c7c0"
 
 [[package]]
 name = "polling"
-version = "2.1.0"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92341d779fa34ea8437ef4d82d440d5e1ce3f3ff7f824aa64424cd481f9a1f25"
+checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259"
 dependencies = [
  "cfg-if 1.0.0",
  "libc",
@@ -1726,9 +1709,9 @@ dependencies = [
 
 [[package]]
 name = "ppv-lite86"
-version = "0.2.10"
+version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
+checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
 
 [[package]]
 name = "precomputed-hash"
@@ -1743,16 +1726,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
 
 [[package]]
-name = "proc-macro-nested"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
-
-[[package]]
 name = "proc-macro2"
-version = "1.0.29"
+version = "1.0.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
+checksum = "fb37d2df5df740e582f28f8560cf425f52bb267d872fe58358eadb554909f07a"
 dependencies = [
  "unicode-xid",
 ]
@@ -1790,15 +1767,15 @@ dependencies = [
 
 [[package]]
 name = "protobuf"
-version = "2.25.1"
+version = "2.25.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23129d50f2c9355ced935fce8a08bd706ee2e7ce2b3b33bf61dace0e379ac63a"
+checksum = "47c327e191621a2158159df97cdbc2e7074bb4e940275e35abf38eb3d2595754"
 
 [[package]]
 name = "quote"
-version = "1.0.9"
+version = "1.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
+checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
 dependencies = [
  "proc-macro2",
 ]
@@ -1933,9 +1910,9 @@ dependencies = [
 
 [[package]]
 name = "redis"
-version = "0.21.3"
+version = "0.21.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd71bdb3d0d6e9183e675c977f652fbf8abc3b63fcb722e9abb42f82ef839b65"
+checksum = "7f23ceed4c0e76b322657c2c3352ea116f9ec60a1a1aefeb3c84ed062c50865b"
 dependencies = [
  "async-std",
  "async-trait",
@@ -1999,9 +1976,9 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
 
 [[package]]
 name = "relative-path"
-version = "1.5.0"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9629de8974fd69c97684736786b807edd3da456d3e3f95341dd9d4cbd8f5ad6"
+checksum = "73d4caf086b102ab49d0525b721594a555ab55c6556086bbe52a430ad26c3bd7"
 
 [[package]]
 name = "remove_dir_all"
@@ -2056,11 +2033,11 @@ dependencies = [
 
 [[package]]
 name = "rustc_version"
-version = "0.3.3"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
 dependencies = [
- "semver 0.11.0",
+ "semver 1.0.4",
 ]
 
 [[package]]
@@ -2078,9 +2055,9 @@ dependencies = [
 
 [[package]]
 name = "ryu"
-version = "1.0.5"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568"
 
 [[package]]
 name = "scopeguard"
@@ -2124,17 +2101,14 @@ version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
 dependencies = [
- "semver-parser 0.7.0",
+ "semver-parser",
 ]
 
 [[package]]
 name = "semver"
-version = "0.11.0"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
-dependencies = [
- "semver-parser 0.10.2",
-]
+checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
 
 [[package]]
 name = "semver-parser"
@@ -2143,15 +2117,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
 
 [[package]]
-name = "semver-parser"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
-dependencies = [
- "pest",
-]
-
-[[package]]
 name = "serde"
 version = "1.0.130"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2173,9 +2138,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.68"
+version = "1.0.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
+checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
 dependencies = [
  "itoa",
  "ryu",
@@ -2184,9 +2149,9 @@ dependencies = [
 
 [[package]]
 name = "serde_qs"
-version = "0.8.4"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8a72808528a89fa9eca23bbb6a1eb92cb639b881357269b6510f11e50c0f8a9"
+checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6"
 dependencies = [
  "percent-encoding",
  "serde",
@@ -2270,15 +2235,15 @@ checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b"
 
 [[package]]
 name = "slab"
-version = "0.4.4"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590"
+checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
 
 [[package]]
 name = "smallvec"
-version = "1.6.1"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
+checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
 
 [[package]]
 name = "socket2"
@@ -2368,12 +2333,13 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
 
 [[package]]
 name = "string_cache"
-version = "0.8.1"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a"
+checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6"
 dependencies = [
  "lazy_static",
  "new_debug_unreachable",
+ "parking_lot",
  "phf_shared",
  "precomputed-hash",
  "serde",
@@ -2399,9 +2365,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
 
 [[package]]
 name = "surf"
-version = "2.3.1"
+version = "2.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73f856d60bdb4679fc9ec516c34093484e963431b5016a8429f85a8e74b5ccaa"
+checksum = "718b1ae6b50351982dedff021db0def601677f2120938b070eadb10ba4038dd7"
 dependencies = [
  "async-std",
  "async-trait",
@@ -2429,9 +2395,9 @@ checksum = "45f6ee7c7b87caf59549e9fe45d6a69c75c8019e79e212a835c5da0e92f0ba08"
 
 [[package]]
 name = "syn"
-version = "1.0.77"
+version = "1.0.82"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0"
+checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2485,18 +2451,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
 
 [[package]]
 name = "thiserror"
-version = "1.0.29"
+version = "1.0.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88"
+checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.29"
+version = "1.0.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c"
+checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2608,9 +2574,9 @@ dependencies = [
 
 [[package]]
 name = "tinyvec"
-version = "1.5.0"
+version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
+checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
 dependencies = [
  "tinyvec_macros",
 ]
@@ -2623,9 +2589,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
 [[package]]
 name = "tokio"
-version = "1.12.0"
+version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc"
+checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
 dependencies = [
  "autocfg",
  "bytes",
@@ -2635,9 +2601,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-util"
-version = "0.6.8"
+version = "0.6.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd"
+checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"
 dependencies = [
  "bytes",
  "futures-core",
@@ -2654,12 +2620,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
 
 [[package]]
-name = "ucd-trie"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
-
-[[package]]
 name = "unicase"
 version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2670,9 +2630,9 @@ dependencies = [
 
 [[package]]
 name = "unicode-bidi"
-version = "0.3.6"
+version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
 
 [[package]]
 name = "unicode-normalization"
@@ -2738,9 +2698,9 @@ checksum = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f"
 
 [[package]]
 name = "value-bag"
-version = "1.0.0-alpha.7"
+version = "1.0.0-alpha.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd320e1520f94261153e96f7534476ad869c14022aee1e59af7c778075d840ae"
+checksum = "79923f7731dc61ebfba3633098bf3ac533bbd35ccd8c57e7088d9a5eebe0263f"
 dependencies = [
  "ctor",
  "sval",
@@ -2919,3 +2879,46 @@ name = "winapi-x86_64-pc-windows-gnu"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82ca39602d5cbfa692c4b67e3bcbb2751477355141c1ed434c94da4186836ff6"
+dependencies = [
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52695a41e536859d5308cc613b4a022261a274390b25bd29dfff4bf08505f3c2"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f54725ac23affef038fecb177de6c9bf065787c2f432f79e3c373da92f3e1d8a"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d5158a43cc43623c0729d1ad6647e62fa384a3d135fd15108d37c683461f64"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc31f409f565611535130cfe7ee8e6655d3fa99c1c61013981e491921b5ce954"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f"
diff --git a/Cargo.toml b/Cargo.toml
index faa822c..63adeb5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -34,6 +34,7 @@ test-logger = "^0.1.0"       # Simple helper to initialize env_logger before uni
 
 [dependencies]
 async-trait = "^0.1.50"      # Type erasure for async trait methods
+data-encoding = "^2.3.2"     # Efficient and customizable data-encoding functions like base64, base32, and hex
 easy-scraper = "^0.2.0"      # HTML scraping library focused on ease of use
 ellipse = "^0.2.0"           # Truncate and ellipsize strings in a human-friendly way
 env_logger = "^0.8.3"        # A logging implementation for `log` which is configured via an environment variable
@@ -46,11 +47,13 @@ log = "^0.4.14"              # A lightweight logging facade for Rust
 markdown = "^0.3.0"          # Native Rust library for parsing Markdown and (outputting HTML)
 markup = "^0.12.0"           # HTML templating engine... ok also very funny about markdown and markup... i just realized the pun...
 newbase60 = "^0.1.3"         # A library that implements Tantek Çelik's New Base 60
+rand = "^0.8.4"              # Random number generators.
 retainer = "^0.2.2"          # Minimal async cache in Rust with support for key expirations
 serde_json = "^1.0.64"       # A JSON serialization file format
 serde_urlencoded = "^0.7.0"  # `x-www-form-urlencoded` meets Serde
 tide = "^0.16.0"             # A minimal and pragmatic Rust web application framework built for rapid development
 relative-path = "^1.5.0"     # Portable relative paths for Rust
+sha2 = "^0.9.8"              # SHA-2 series of algorithms for Rust
 [dependencies.anyhow]
 version = "^1.0.42"
 optional = true
diff --git a/flake.nix b/flake.nix
index 0b0e039..4366190 100644
--- a/flake.nix
+++ b/flake.nix
@@ -124,6 +124,12 @@
             example = "/run/secrets/kittybox-shared-secret";
             description = "A shared secret that will, when passed, allow unlimited editing access to database. Keep it safe.";
           };
+          cookieSecretFile = mkOption {
+            type = types.str;
+            default = "/var/lib/kittybox/cookie_secret_key";
+            example = "/run/secrets/kittybox-cookie-secret";
+            description = "A secret file to encrypt cookies with the contents of. Should be at least 32 bytes in length. A random persistent file will be generated if this variable is left untouched.";
+          };
         };
       };
       config = lib.mkIf cfg.enable {
@@ -139,6 +145,7 @@
             cfg.authorizationEndpoint
             cfg.internalTokenFile
             cfg.bind cfg.port
+            cfg.cookieSecretFile
           ];
 
           environment = {
@@ -151,6 +158,7 @@
             #REDIS_URI = if (cfg.redisUri == null) then "redis://127.0.0.1:6379/" else cfg.redisUri;
             BACKEND_URI = cfg.backendUri;
             RUST_LOG = "${cfg.logLevel}";
+            COOKIE_SECRET_FILE = "${cfg.cookieSecretFile}";
           };
 
           script = ''
@@ -159,6 +167,9 @@
                 export KITTYBOX_INTERNAL_TOKEN=$(${pkgs.coreutils}/bin/cat ${cfg.internalTokenFile})
               fi
             ''}
+            if [[ ${cfg.cookieSecretFile} == /var/lib/kittybox/cookie_secret_key && ! -f /var/lib/kittybox/cookie_secret_key ]]; then
+                cat /dev/urandom | tr -Cd '[:alnum:]' | head -c 128 > /var/lib/kittybox/cookie_secret_key
+            fi
             exec ${cfg.package}/bin/kittybox
           '';
 
diff --git a/src/database/file/mod.rs b/src/database/file/mod.rs
index 4fb7f47..d556f46 100644
--- a/src/database/file/mod.rs
+++ b/src/database/file/mod.rs
@@ -482,11 +482,11 @@ impl Storage for FileStorage {
                         // Hack to unwrap the Option and sieve out broken links
                         // Broken links return None, and Stream::filter_map skips Nones.
                         .try_filter_map(|post: Option<serde_json::Value>| async move { Ok(post) })
+                        .try_filter_map(|post| async move { Ok(filter_post(post, user)) })
                         .and_then(|mut post| async move {
                             hydrate_author(&mut post, user, self).await;
                             Ok(post)
                         })
-                        .try_filter_map(|post| async move { Ok(filter_post(post, user)) })
                         .take(limit);
 
                     match posts.try_collect::<Vec<serde_json::Value>>().await {
diff --git a/src/frontend/login.rs b/src/frontend/login.rs
new file mode 100644
index 0000000..09fa75f
--- /dev/null
+++ b/src/frontend/login.rs
@@ -0,0 +1,292 @@
+use std::convert::TryInto;
+use std::str::FromStr;
+use log::{debug, error};
+use rand::Rng;
+use http_types::Mime;
+use tide::{Request, Response, Result};
+use serde::{Serialize, Deserialize};
+use sha2::{Sha256, Digest};
+
+use crate::frontend::{FrontendError, IndiewebEndpoints};
+use crate::{ApplicationState, database::Storage};
+use crate::frontend::templates::Template;
+
+markup::define! {
+    LoginPage {
+        form[method="POST"] {
+            h1 { "Sign in with your website" }
+            p {
+                "Signing in to Kittybox might allow you to view private content "
+                    "intended for your eyes only."
+            }
+
+            section {
+                label[for="url"] { "Your website URL" }
+                input[id="url", name="url", placeholder="https://example.com/"];
+                input[type="submit"];
+            }
+        }
+    }
+}
+
+pub async fn form<S: Storage>(req: Request<ApplicationState<S>>) -> Result {
+    let owner = req.url().origin().ascii_serialization() + "/";
+    let storage = &req.state().storage;
+    let authorization_endpoint = req.state().authorization_endpoint.to_string();
+    let token_endpoint = req.state().token_endpoint.to_string();
+    let blog_name = storage.get_setting("site_name", &owner).await.unwrap_or_else(|_| "Kitty Box!".to_string());
+    let feeds = storage.get_channels(&owner).await.unwrap_or_default();
+
+    Ok(Response::builder(200)
+       .body(Template {
+           title: "Sign in with IndieAuth",
+           blog_name: &blog_name,
+           endpoints: IndiewebEndpoints {
+               authorization_endpoint,
+               token_endpoint,
+               webmention: None,
+               microsub: None
+           },
+           feeds,
+           user: req.session().get("user"),
+           content: LoginPage {}.to_string(),
+       }.to_string())
+       .content_type("text/html; charset=utf-8")
+       .build())
+}
+
+#[derive(Serialize, Deserialize)]
+struct LoginForm {
+    url: String
+}
+
+#[derive(Serialize, Deserialize)]
+struct IndieAuthClientState {
+    /// A random value to protect from CSRF attacks.
+    nonce: String,
+    /// The user's initial "me" value.
+    me: String,
+    /// Authorization endpoint used.
+    authorization_endpoint: String
+}
+
+
+#[derive(Serialize, Deserialize)]
+struct IndieAuthRequestParams {
+    response_type: String,         // can only have "code". TODO make an enum
+    client_id: String,             // always a URL. TODO consider making a URL
+    redirect_uri: surf::Url,       // callback URI for IndieAuth
+    state: String,                 // CSRF protection, should include randomness and be passed through
+    code_challenge: String,        // base64-encoded PKCE challenge
+    code_challenge_method: String, // usually "S256". TODO make an enum
+    scope: Option<String>,         // oAuth2 scopes to grant,
+    me: surf::Url,                 // User's entered profile URL
+    
+}
+
+/// Handle login requests. Find the IndieAuth authorization endpoint and redirect to it.
+pub async fn handler<S: Storage>(mut req: Request<ApplicationState<S>>) -> Result {
+    let content_type = req.content_type();
+    if content_type.is_none() {
+        return Err(FrontendError::with_code(400, "Use the login form, Luke.").into());
+    }
+    if content_type.unwrap() != Mime::from_str("application/x-www-form-urlencoded").unwrap() {
+        return Err(FrontendError::with_code(400, "Login form results must be a urlencoded form").into());
+    }
+
+    let form = req.body_form::<LoginForm>().await?; // FIXME check if it returns 400 or 500 on error
+    let homepage_uri = surf::Url::parse(&form.url)?;
+    let http = &req.state().http_client;
+    
+    let mut fetch_response = http.get(&homepage_uri).send().await?;
+    if fetch_response.status() != 200 {
+        return Err(FrontendError::with_code(500, "Error fetching your authorization endpoint. Check if your website's okay.").into());
+    }
+
+    let mut authorization_endpoint: Option<surf::Url> = None;
+    if let Some(links) = fetch_response.header("Link") {
+        // NOTE: this is the same Link header parser used in src/micropub/post.rs:459.
+        // One should refactor it to a function to use independently and improve later
+        for link in links.iter().flat_map(|i| i.as_str().split(',')) {
+            debug!("Trying to match {} as authorization_endpoint", link);
+            let mut split_link = link.split(';');
+
+            match split_link.next() {
+                Some(uri) => {
+                    if let Some(uri) = uri.strip_prefix('<').and_then(|uri| uri.strip_suffix('>')) {
+                        debug!("uri: {}", uri);
+                        for prop in split_link {
+                            debug!("prop: {}", prop);
+                            let lowercased = prop.to_ascii_lowercase();
+                            let trimmed = lowercased.trim();
+                            if trimmed == "rel=\"authorization_endpoint\""
+                                || trimmed == "rel=authorization_endpoint"
+                            {
+                                if let Ok(endpoint) = homepage_uri.join(uri) {
+                                    debug!("Found authorization endpoint {} for user {}", endpoint, homepage_uri.as_str());
+                                    authorization_endpoint = Some(endpoint);
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                },
+                None => continue,
+            }
+        }
+    }
+    // If the authorization_endpoint is still not found after the Link parsing gauntlet,
+    // bring out the big guns and parse HTML to find it.
+    if authorization_endpoint.is_none() {
+        let body = fetch_response.body_string().await?;
+        let pattern = easy_scraper::Pattern::new(r#"<link rel="authorization_endpoint" href="{{url}}">"#)
+            .expect("Cannot parse the pattern for authorization_endpoint");
+        let matches = pattern.matches(&body);
+        debug!("Matches for authorization_endpoint in HTML: {:?}", matches);
+        if !matches.is_empty() {
+            if let Ok(endpoint) = homepage_uri.join(&matches[0]["url"]) {
+                debug!("Found authorization endpoint {} for user {}", endpoint, homepage_uri.as_str());
+                authorization_endpoint = Some(endpoint)
+            }
+        }
+    };
+    // If even after this the authorization endpoint is still not found, bail out.
+    if authorization_endpoint.is_none() {
+        error!("Couldn't find authorization_endpoint for {}", homepage_uri.as_str());
+        return Err(FrontendError::with_code(400, "Your website doesn't support the IndieAuth protocol.").into());
+    }
+    let mut authorization_endpoint: surf::Url = authorization_endpoint.unwrap();
+    let mut rng = rand::thread_rng();
+    let state: String = data_encoding::BASE64URL.encode(serde_urlencoded::to_string(IndieAuthClientState {
+        nonce: (0..8).map(|_| {
+            let idx = rng.gen_range(0..INDIEAUTH_PKCE_CHARSET.len());
+            INDIEAUTH_PKCE_CHARSET[idx] as char
+        }).collect(),
+        me: homepage_uri.to_string(),
+        authorization_endpoint: authorization_endpoint.to_string()
+    })?.as_bytes());
+    // PKCE code generation
+    let code_verifier: String = (0..128).map(|_| {
+        let idx = rng.gen_range(0..INDIEAUTH_PKCE_CHARSET.len());
+        INDIEAUTH_PKCE_CHARSET[idx] as char
+    }).collect();
+    let mut hasher = Sha256::new();
+    hasher.update(code_verifier.as_bytes());
+    let code_challenge: String = data_encoding::BASE64URL.encode(&hasher.finalize());
+
+    authorization_endpoint.set_query(Some(&serde_urlencoded::to_string(IndieAuthRequestParams {
+        response_type: "code".to_string(),
+        client_id: req.url().origin().ascii_serialization(),
+        redirect_uri: req.url().join("login/callback")?,
+        state: state.clone(), code_challenge,
+        code_challenge_method: "S256".to_string(),
+        scope: Some("profile".to_string()),
+        me: homepage_uri,
+    })?));
+
+    let cookies = vec![
+        format!(r#"indieauth_state="{}"; Same-Site: None; Secure; Max-Age: 600"#, state),
+        format!(r#"indieauth_code_verifier="{}"; Same-Site: None; Secure; Max-Age: 600"#, code_verifier)
+    ];
+
+    let cookie_header = cookies.iter().map(|i| -> http_types::headers::HeaderValue {
+        (i as &str).try_into().unwrap()
+    }).collect::<Vec<_>>();
+    
+    Ok(Response::builder(302)
+       .header("Location", authorization_endpoint.to_string())
+       .header("Set-Cookie", &*cookie_header)
+       .build())
+}
+
+const INDIEAUTH_PKCE_CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
+                                        abcdefghijklmnopqrstuvwxyz\
+                                        1234567890-._~";
+
+#[derive(Deserialize)]
+struct IndieAuthCallbackResponse {
+    code: Option<String>,
+    error: Option<String>,
+    error_description: Option<String>,
+    #[allow(dead_code)]
+    error_uri: Option<String>,
+    // This needs to be further decoded to receive state back and will always be present
+    state: String
+}
+
+impl IndieAuthCallbackResponse {
+    fn is_successful(&self) -> bool {
+        !self.code.is_none()
+    }
+}
+
+#[derive(Serialize, Deserialize)]
+struct IndieAuthCodeRedeem {
+    grant_type: String,
+    code: String,
+    client_id: String,
+    redirect_uri: String,
+    code_verifier: String
+}
+
+#[derive(Serialize, Deserialize)]
+struct IndieWebProfile {
+    name: Option<String>,
+    url: Option<String>,
+    email: Option<String>,
+    photo: Option<String>
+}
+
+#[derive(Serialize, Deserialize)]
+struct IndieAuthResponse {
+    me: String,
+    scope: Option<String>,
+    access_token: Option<String>,
+    token_type: Option<String>,
+    profile: Option<IndieWebProfile>
+}
+
+/// Handle IndieAuth parameters, fetch the final h-card and redirect the user to the homepage.
+pub async fn callback<S: Storage>(mut req: Request<ApplicationState<S>>) -> Result {
+    let params: IndieAuthCallbackResponse = req.query()?;
+    let http: &surf::Client = &req.state().http_client;
+    let origin = req.url().origin().ascii_serialization();
+
+    if req.cookie("indieauth_state").unwrap().value() != &params.state {
+        return Err(FrontendError::with_code(400, "The state doesn't match. A possible CSRF attack was prevented. Please try again later.").into());
+    }
+    let state: IndieAuthClientState = serde_urlencoded::from_bytes(
+        &data_encoding::BASE64URL.decode(params.state.as_bytes())?
+    )?;
+
+    if !params.is_successful() {
+        return Err(FrontendError::with_code(400, &format!("The authorization endpoint indicated a following error: {:?}: {:?}", &params.error, &params.error_description)).into())
+    }
+
+    let authorization_endpoint = surf::Url::parse(&state.authorization_endpoint).unwrap();
+    let mut code_response = http.post(authorization_endpoint)
+        .body_string(serde_urlencoded::to_string(IndieAuthCodeRedeem {
+            grant_type: "authorization_code".to_string(),
+            code: params.code.unwrap().to_string(),
+            client_id: origin.to_string(),
+            redirect_uri: origin + "/login/callback",
+            code_verifier: req.cookie("indieauth_code_verifier").unwrap().value().to_string()
+        })?)
+        .header("Content-Type", "application/x-www-form-urlencoded")
+        .header("Accept", "application/json")
+        .send().await?;
+
+    if code_response.status() != 200 {
+        return Err(FrontendError::with_code(code_response.status(), &format!("Authorization endpoint returned an error when redeeming the code: {}", code_response.body_string().await?)).into());
+    }
+
+    let json: IndieAuthResponse = code_response.body_json().await?;
+    drop(http);
+    let session = req.session_mut();
+    session.insert("user", &json.me)?;
+
+    // TODO redirect to the page user came from
+    Ok(Response::builder(302)
+       .header("Location", "/")
+       .build())
+}
diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs
index 5426f7e..76114c5 100644
--- a/src/frontend/mod.rs
+++ b/src/frontend/mod.rs
@@ -1,3 +1,5 @@
+use std::convert::TryInto;
+
 use crate::database::Storage;
 use crate::ApplicationState;
 use log::{error, info};
@@ -6,8 +8,9 @@ use tide::{Next, Request, Response, Result, StatusCode};
 
 static POSTS_PER_PAGE: usize = 20;
 
-mod templates;
+pub mod login;
 
+mod templates;
 use templates::{ErrorPage, MainPage, OnboardingPage, Template};
 
 #[derive(Clone, Serialize, Deserialize)]
@@ -30,11 +33,11 @@ struct FrontendError {
     code: StatusCode,
 }
 impl FrontendError {
-    pub fn with_code(code: StatusCode, msg: &str) -> Self {
+    pub fn with_code<C>(code: C, msg: &str) -> Self where C: TryInto<StatusCode> {
         Self {
             msg: msg.to_string(),
             source: None,
-            code,
+            code: code.try_into().unwrap_or_else(|_| StatusCode::InternalServerError),
         }
     }
     pub fn msg(&self) -> &str {
@@ -227,7 +230,7 @@ pub async fn mainpage<S: Storage>(mut req: Request<ApplicationState<S>>) -> Resu
     let query = req.query::<QueryParams>()?;
     let authorization_endpoint = req.state().authorization_endpoint.to_string();
     let token_endpoint = req.state().token_endpoint.to_string();
-    let user: Option<String> = None;
+    let user: Option<String> = req.session().get("user");
 
     #[cfg(any(not(debug_assertions), test))]
     let url = req.url();
@@ -260,6 +263,7 @@ pub async fn mainpage<S: Storage>(mut req: Request<ApplicationState<S>>) -> Resu
                             microsub: None,
                         },
                         feeds: Vec::default(),
+                        user: None,
                         content: OnboardingPage {}.to_string(),
                     }
                     .to_string(),
@@ -288,6 +292,7 @@ pub async fn mainpage<S: Storage>(mut req: Request<ApplicationState<S>>) -> Resu
                         .get_channels(hcard_url)
                         .await
                         .unwrap_or_else(|_| Vec::default()),
+                    user,
                     content: MainPage {
                         feed: &feed?,
                         card: &card?,
@@ -304,7 +309,7 @@ pub async fn render_post<S: Storage>(mut req: Request<ApplicationState<S>>) -> R
     let query = req.query::<QueryParams>()?;
     let authorization_endpoint = req.state().authorization_endpoint.to_string();
     let token_endpoint = req.state().token_endpoint.to_string();
-    let user: Option<String> = None;
+    let user: Option<String> = req.session().get("user");
 
     // This cannot error out as the URL must be valid. Or there is something horribly wrong
     // and we shouldn't serve this request anyway.
@@ -381,6 +386,7 @@ pub async fn render_post<S: Storage>(mut req: Request<ApplicationState<S>>) -> R
                     .get_channels(&owner)
                     .await
                     .unwrap_or_else(|_| Vec::default()),
+                user,
                 content: template,
             }
             .to_string(),
@@ -415,11 +421,16 @@ where
             .get_channels(&owner)
             .await
             .unwrap_or_else(|_| Vec::default());
+        let user: Option<String> = request.session().get("user");
         let mut res = next.run(request).await;
         let mut code: Option<StatusCode> = None;
+        let mut msg: Option<String> = None;
         if let Some(err) = res.downcast_error::<FrontendError>() {
             code = Some(err.code());
             error!("Error caught while processing request: {}", err.msg());
+            if err.code() == 400 {
+                msg = Some(err.msg().to_string());
+            }
             let mut err: &dyn std::error::Error = err;
             while let Some(e) = err.source() {
                 error!("Caused by: {}", e);
@@ -439,8 +450,9 @@ where
                         webmention: None,
                         microsub: None,
                     },
-                    feeds: feeds,
-                    content: ErrorPage { code }.to_string(),
+                    feeds,
+                    user,
+                    content: ErrorPage { code, msg }.to_string(),
                 }
                 .to_string(),
             );
diff --git a/src/frontend/templates/mod.rs b/src/frontend/templates/mod.rs
index 100e16d..dbc23c9 100644
--- a/src/frontend/templates/mod.rs
+++ b/src/frontend/templates/mod.rs
@@ -25,7 +25,7 @@ mod onboarding;
 pub use onboarding::OnboardingPage;
 
 markup::define! {
-    Template<'a>(title: &'a str, blog_name: &'a str, endpoints: IndiewebEndpoints, feeds: Vec<MicropubChannel>, content: String) {
+    Template<'a>(title: &'a str, blog_name: &'a str, endpoints: IndiewebEndpoints, feeds: Vec<MicropubChannel>, user: Option<String>, content: String) {
         @markup::doctype()
         html {
             head {
@@ -45,14 +45,22 @@ markup::define! {
                 }
             }
             body {
+                // TODO Somehow compress headerbar into a menu when the screen space is tight
                 nav#headerbar {
                     ul {
-                        // TODO print a list of feeds and allow jumping to them
                         li { a#homepage[href="/"] { @blog_name } }
                         @for feed in feeds.iter() {
                             li { a[href=&feed.uid] { @feed.name } }
                         }
-                        li.shiftright { a#login[href="/login"] { "Login" } }
+                        li.shiftright {
+                            @if user.is_none() {
+                                a#login[href="/login"] { "Sign in" }
+                            } else {
+                                span {
+                                    @user.as_ref().unwrap() " - " a#logout[href="/logout"] { "Sign out" }
+                                }
+                            }
+                        }
                     }
                 }
                 main {
@@ -372,7 +380,7 @@ markup::define! {
         }
         @Feed { feed }
     }
-    ErrorPage(code: StatusCode) {
+    ErrorPage(code: StatusCode, msg: Option<String>) {
         h1 { @format!("HTTP {} {}", code, code.canonical_reason()) }
         @match code {
             StatusCode::Unauthorized => {
@@ -399,11 +407,32 @@ markup::define! {
                 p { "Wait, do you seriously expect my website to brew you coffee? It's not a coffee machine!" }
 
                 p {
-                    small { "I could brew you some coffee tho if we meet one day... "
-                    small { i { "i-it's nothing personal, I just like brewing coffee, b-baka!!!~ >.<!" } } }
+                    small {
+                        "I could brew you some coffee tho if we meet one day... "
+                        small {
+                            i {
+                                "i-it's nothing personal, I just like brewing coffee, b-baka!!!~ >.<!"
+                            }
+                        }
+                    }
                 }
             }
-            _ => { p { "It seems like you have found an error. Not to worry, it has already been logged." } }
+            StatusCode::BadRequest => {
+                @if msg.is_none() {
+                    p {
+                        "There was an undescribed error in your request. "
+                        "Please try again later or with a different request."
+                    }
+                } else {
+                    p {
+                        "There was a following error in your request: "
+                        @msg.as_ref().unwrap()
+                    }
+                }
+            }
+            _ => {
+                p { "It seems like you have found an error. Not to worry, it has already been logged." }
+            }
         }
         P { "For now, may I suggest to visit " a[href="/"] {"the main page"} " of this website?" }
 
diff --git a/src/lib.rs b/src/lib.rs
index 817bda7..eb915c2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -19,6 +19,7 @@ where
     authorization_endpoint: surf::Url,
     media_endpoint: Option<String>,
     internal_token: Option<String>,
+    cookie_secret: String,
     http_client: surf::Client,
     storage: StorageBackend,
 }
@@ -48,6 +49,13 @@ where
         .with(frontend::ErrorHandlerMiddleware {})
         .get(frontend::mainpage)
         .post(frontend::onboarding_receiver);
+    app.at("/login")
+        .with(frontend::ErrorHandlerMiddleware {})
+        .get(frontend::login::form)
+        .post(frontend::login::handler);
+    app.at("/login/callback")
+        .with(frontend::ErrorHandlerMiddleware {})
+        .get(frontend::login::callback);
     app.at("/static/*path")
         .with(frontend::ErrorHandlerMiddleware {})
         .get(frontend::handle_static);
@@ -64,7 +72,14 @@ where
     app.at("/metrics").get(metrics::gather);
 
     app.with(metrics::InstrumentationMiddleware {});
-
+    app.with(
+        tide::sessions::SessionMiddleware::new(
+            tide::sessions::CookieStore::new(),
+            &app.state().cookie_secret.as_bytes()
+        )
+            .with_cookie_name("kittybox_session")
+            .without_save_unchanged()
+    );
     app
 }
 
@@ -93,6 +108,7 @@ pub async fn get_app_with_file(
     authorization_endpoint: surf::Url,
     backend_uri: String,
     media_endpoint: Option<String>,
+    cookie_secret: String,
     internal_token: Option<String>,
 ) -> App<database::FileStorage> {
     let folder = backend_uri.strip_prefix("file://").unwrap();
@@ -102,6 +118,7 @@ pub async fn get_app_with_file(
         media_endpoint,
         authorization_endpoint,
         internal_token,
+        cookie_secret,
         storage: database::FileStorage::new(path).await.unwrap(),
         http_client: surf::Client::new(),
     });
@@ -128,6 +145,7 @@ pub async fn get_app_with_test_file(
         authorization_endpoint: Url::parse("https://indieauth.com/auth").unwrap(),
         storage: backend.clone(),
         internal_token: None,
+        cookie_secret: "1234567890abcdefghijklmnopqrstuvwxyz".to_string(),
         http_client: surf::Client::new(),
     });
     (tempdir, backend, equip_app(app))
diff --git a/src/main.rs b/src/main.rs
index aec3be0..4f5f9ed 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -60,6 +60,24 @@ async fn main() -> Result<(), std::io::Error> {
 
     let internal_token: Option<String> = env::var("KITTYBOX_INTERNAL_TOKEN").ok();
 
+    let cookie_secret: String = match env::var("COOKIE_SECRET").ok() {
+        Some(value) => value,
+        None => {
+            if let Some(filename) = env::var("COOKIE_SECRET_FILE").ok() {
+                use async_std::io::ReadExt;
+
+                let mut file = async_std::fs::File::open(filename).await?;
+                let mut temp_string = String::new();
+                file.read_to_string(&mut temp_string).await?;
+
+                temp_string
+            } else {
+                error!("COOKIE_SECRET or COOKIE_SECRET_FILE is not set, will not be able to log in users securely!");
+                std::process::exit(1);
+            }
+        }
+    };
+
     let host = env::var("SERVE_AT")
         .ok()
         .unwrap_or_else(|| "0.0.0.0:8080".to_string());
@@ -73,6 +91,7 @@ async fn main() -> Result<(), std::io::Error> {
             authorization_endpoint,
             backend_uri,
             media_endpoint,
+            cookie_secret,
             internal_token,
         )
         .await;
diff --git a/src/micropub/post.rs b/src/micropub/post.rs
index 070c822..c465a6f 100644
--- a/src/micropub/post.rs
+++ b/src/micropub/post.rs
@@ -483,7 +483,7 @@ async fn post_process_new_post<S: Storage>(
                 // TODO: Replace this function once the MF2 parser is ready
                 // A compliant parser's output format includes rels,
                 // we could just find a Webmention one in there
-                let pattern = easy_scraper::Pattern::new(r#"<link href="{url}" rel="webmention">"#)
+                let pattern = easy_scraper::Pattern::new(r#"<link href="{{url}}" rel="webmention">"#)
                     .expect("Pattern for webmentions couldn't be parsed");
                 let matches = pattern.matches(&body);
                 if matches.is_empty() {