diff --git a/Pipfile b/Pipfile
index 3e3da956e..7dcb084a0 100644
--- a/Pipfile
+++ b/Pipfile
@@ -45,6 +45,7 @@ kubernetes = "*"
docker = "*"
xmlsec = "*"
geoip2 = "*"
+webauthn = "*"
[requires]
python_version = "3.9"
diff --git a/Pipfile.lock b/Pipfile.lock
index 08301bf4f..2ec45f827 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "7151710a45e6ca0bd25335b14be005aa5179eb91de361de93686022c9b71c3d1"
+ "sha256": "933685b75680e3a06d2f523239d848b14d1507d385de42401863f4fb6345366c"
},
"pipfile-spec": 6,
"requires": {
@@ -56,6 +56,7 @@
"sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245",
"sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1"
],
+ "markers": "python_version >= '3.6'",
"version": "==3.7.3"
},
"aioredis": {
@@ -70,6 +71,7 @@
"sha256:1e759a7f202d910939de6eca45c23a107f6b71111f41d1282c648e9ac3d21901",
"sha256:affdd263d8b8eb3c98170b78bf83867cdb6a14901d586e00ddb65bfe2f0c4e60"
],
+ "markers": "python_version >= '3.6'",
"version": "==5.0.5"
},
"asgiref": {
@@ -77,6 +79,7 @@
"sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
"sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
],
+ "markers": "python_version >= '3.5'",
"version": "==3.3.1"
},
"async-timeout": {
@@ -84,6 +87,7 @@
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
],
+ "markers": "python_full_version >= '3.5.3'",
"version": "==3.0.1"
},
"attrs": {
@@ -91,6 +95,7 @@
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.3.0"
},
"autobahn": {
@@ -98,6 +103,7 @@
"sha256:41a3a3f89cde48643baf4e105d9491c566295f9abee951379e59121784044b8b",
"sha256:7e6b1bf95196b733978bab2d54a7ab8899c16ce11be369dc58422c07b7eea726"
],
+ "markers": "python_version >= '3.6'",
"version": "==21.2.1"
},
"automat": {
@@ -116,7 +122,8 @@
},
"boto3": {
"hashes": [
- "sha256:877f204dabe1bfa21aa9cfaacc72bd4b70a897d0fdcea799afa5c4743b6fc7ac"
+ "sha256:877f204dabe1bfa21aa9cfaacc72bd4b70a897d0fdcea799afa5c4743b6fc7ac",
+ "sha256:3a8412020a59509e783755b5c9b910a4fc7f6b6f2b9473e7cd1e07b67672e0d1"
],
"index": "pypi",
"version": "==1.17.9"
@@ -126,6 +133,7 @@
"sha256:c8614c230e7a8e042a8c07d47caea50ad21cb51415289bd34fa6d0382beddad7",
"sha256:d725840b881be62fc52e8e24a6ada651128cf7f1ed1639b87322a7a213ffdbad"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.20.9"
},
"cachetools": {
@@ -133,8 +141,15 @@
"sha256:1d9d5f567be80f7c07d765e21b814326d78c61eb0c3a637dffc0e5d1796cb2e2",
"sha256:f469e29e7aa4cff64d8de4aad95ce76de8ea1125a16c68e0d93f65c3c3dc92e9"
],
+ "markers": "python_version ~= '3.5'",
"version": "==4.2.1"
},
+ "cbor2": {
+ "hashes": [
+ "sha256:a33aa2e5534fd74401ac95686886e655e3b2ce6383b3f958199b6e70a87c94bf"
+ ],
+ "version": "==5.2.0"
+ },
"celery": {
"hashes": [
"sha256:5e8d364e058554e83bbb116e8377d90c79be254785f357cb2cec026e79febe13",
@@ -220,6 +235,7 @@
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"click-didyoumean": {
@@ -288,6 +304,7 @@
"sha256:0052c9887600c57054a5867d4b0240159fa009faa3bcf6a1627271d9cdcb005a",
"sha256:c22b692707f514de9013651ecb687f2abe4f35cf6fe292ece634e9f1737bc7e3"
],
+ "markers": "python_version >= '3.6'",
"version": "==3.0.1"
},
"defusedxml": {
@@ -428,6 +445,7 @@
"hashes": [
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
],
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.18.2"
},
"geoip2": {
@@ -443,6 +461,7 @@
"sha256:1b461d079b5650efe492a7814e95c536ffa9e7a96e39a6d16189c1604f18554f",
"sha256:8ce6862cf4e9252de10045f05fa80393fde831da9c2b45c39288edeee3cde7f2"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.26.1"
},
"gunicorn": {
@@ -458,6 +477,7 @@
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
],
+ "markers": "python_version >= '3.6'",
"version": "==0.12.0"
},
"hiredis": {
@@ -509,6 +529,7 @@
"sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390",
"sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.0"
},
"httptools": {
@@ -554,6 +575,7 @@
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
],
+ "markers": "python_version >= '3.5'",
"version": "==0.5.1"
},
"itypes": {
@@ -568,6 +590,7 @@
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419",
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.11.3"
},
"jmespath": {
@@ -575,6 +598,7 @@
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
],
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.0"
},
"jsonschema": {
@@ -589,6 +613,7 @@
"sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006",
"sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c"
],
+ "markers": "python_version >= '3.6'",
"version": "==5.0.2"
},
"kubernetes": {
@@ -601,8 +626,11 @@
},
"ldap3": {
"hashes": [
- "sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91",
- "sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57"
+ "sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57",
+ "sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056",
+ "sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59",
+ "sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c",
+ "sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91"
],
"index": "pypi",
"version": "==2.9"
@@ -705,12 +733,14 @@
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.1"
},
"maxminddb": {
"hashes": [
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
],
+ "markers": "python_version >= '3.6'",
"version": "==2.0.3"
},
"msgpack": {
@@ -786,6 +816,7 @@
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
],
+ "markers": "python_version >= '3.6'",
"version": "==5.1.0"
},
"oauthlib": {
@@ -793,6 +824,7 @@
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.1.0"
},
"packaging": {
@@ -815,6 +847,7 @@
"sha256:0fa02fa80363844a4ab4b8d6891f62dd0645ba672723130423ca4037b80c1974",
"sha256:62c811e46bd09130fb11ab759012a4ae385ce4fb2073442d1898867a824183bd"
],
+ "markers": "python_full_version >= '3.6.1'",
"version": "==3.0.16"
},
"psycopg2-binary": {
@@ -860,15 +893,37 @@
},
"pyasn1": {
"hashes": [
+ "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
+ "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
+ "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
+ "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
+ "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
+ "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
+ "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
- "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
+ "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
+ "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
+ "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
+ "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3",
+ "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"
],
"version": "==0.4.8"
},
"pyasn1-modules": {
"hashes": [
+ "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
- "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"
+ "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199",
+ "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811",
+ "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0",
+ "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74",
+ "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d",
+ "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8",
+ "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd",
+ "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405",
+ "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb",
+ "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed",
+ "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"
],
"version": "==0.2.8"
},
@@ -877,6 +932,7 @@
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.20"
},
"pycryptodome": {
@@ -948,6 +1004,7 @@
"sha256:f933ecf4cb736c7af60a6a533db2bf569717f2318b265f92907acff1db43bc34",
"sha256:fc9c55dc1ed57db76595f2d19a479fc1c3a1be2c9da8de798a93d286c5f65f38"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==3.10.1"
},
"pyhamcrest": {
@@ -955,6 +1012,7 @@
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
],
+ "markers": "python_version >= '3.5'",
"version": "==2.0.2"
},
"pyjwkest": {
@@ -976,12 +1034,14 @@
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7"
},
"pyrsistent": {
"hashes": [
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
],
+ "markers": "python_version >= '3.5'",
"version": "==0.17.3"
},
"python-dateutil": {
@@ -989,6 +1049,7 @@
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.1"
},
"python-dotenv": {
@@ -1045,6 +1106,7 @@
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==3.5.3"
},
"requests": {
@@ -1052,11 +1114,13 @@
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.25.1"
},
"requests-oauthlib": {
"hashes": [
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
+ "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc",
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
],
"index": "pypi",
@@ -1105,6 +1169,7 @@
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
},
"sqlparse": {
@@ -1112,6 +1177,7 @@
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
],
+ "markers": "python_version >= '3.5'",
"version": "==0.4.1"
},
"structlog": {
@@ -1159,6 +1225,7 @@
"sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467",
"sha256:f3c19e5bd42bbe4bf345704ad7c326c74d3fd7a1b3844987853bef180be638d4"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==20.3.0"
},
"txaio": {
@@ -1166,6 +1233,7 @@
"sha256:1488d31d564a116538cc1265ac3f7979fb6223bb5a9e9f1479436ee2c17d8549",
"sha256:a8676d6c68aea1f0e2548c4afdb8e6253873af3bc2659bb5bcd9f39dff7ff90f"
],
+ "markers": "python_version >= '3.6'",
"version": "==20.12.1"
},
"typing-extensions": {
@@ -1181,6 +1249,7 @@
"sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
"sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.0.1"
},
"urllib3": {
@@ -1225,6 +1294,7 @@
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
],
+ "markers": "python_version >= '3.6'",
"version": "==5.0.0"
},
"watchgod": {
@@ -1241,6 +1311,14 @@
],
"version": "==0.2.5"
},
+ "webauthn": {
+ "hashes": [
+ "sha256:238391b2e2cc60fb51a2cd2d2d6be149920b9af6184651353d9f95856617a9e7",
+ "sha256:8ad9072ff1d6169f3be30d4dc8733ea563dd266962397bc58b40f674a6af74ac"
+ ],
+ "index": "pypi",
+ "version": "==0.4.7"
+ },
"websocket-client": {
"hashes": [
"sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549",
@@ -1334,6 +1412,7 @@
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
],
+ "markers": "python_version >= '3.6'",
"version": "==1.6.3"
},
"zope.interface": {
@@ -1391,6 +1470,7 @@
"sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd",
"sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==5.2.0"
}
},
@@ -1407,6 +1487,7 @@
"sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
"sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
],
+ "markers": "python_version >= '3.5'",
"version": "==3.3.1"
},
"astroid": {
@@ -1414,6 +1495,7 @@
"sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1",
"sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38"
],
+ "markers": "python_version >= '3.5'",
"version": "==2.4.1"
},
"attrs": {
@@ -1421,6 +1503,7 @@
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.3.0"
},
"autopep8": {
@@ -1451,6 +1534,7 @@
"sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410",
"sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"
],
+ "markers": "python_version >= '3.5'",
"version": "==1.0.1"
},
"bumpversion": {
@@ -1466,6 +1550,7 @@
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"colorama": {
@@ -1559,6 +1644,7 @@
"sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839",
"sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.8.4"
},
"flake8-polyfill": {
@@ -1573,6 +1659,7 @@
"sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac",
"sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"
],
+ "markers": "python_version >= '3.4'",
"version": "==4.0.5"
},
"gitpython": {
@@ -1580,6 +1667,7 @@
"sha256:8621a7e777e276a5ec838b59280ba5272dd144a18169c36c903d8b38b99f750a",
"sha256:c5347c81d232d9b8e7f47b68a83e5dc92e7952127133c5f2df9133f2c75a1b29"
],
+ "markers": "python_version >= '3.4'",
"version": "==3.1.13"
},
"iniconfig": {
@@ -1594,6 +1682,7 @@
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==4.3.21"
},
"lazy-object-proxy": {
@@ -1620,6 +1709,7 @@
"sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4",
"sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.4.3"
},
"mccabe": {
@@ -1656,6 +1746,7 @@
"sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9",
"sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"
],
+ "markers": "python_version >= '2.6'",
"version": "==5.5.1"
},
"pep8-naming": {
@@ -1670,6 +1761,7 @@
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.13.1"
},
"prospector": {
@@ -1684,6 +1776,7 @@
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.10.0"
},
"pycodestyle": {
@@ -1691,6 +1784,7 @@
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.6.0"
},
"pydocstyle": {
@@ -1698,6 +1792,7 @@
"sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325",
"sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"
],
+ "markers": "python_version >= '3.5'",
"version": "==5.1.1"
},
"pyflakes": {
@@ -1705,6 +1800,7 @@
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.2.0"
},
"pylint": {
@@ -1747,6 +1843,7 @@
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7"
},
"pytest": {
@@ -1870,6 +1967,7 @@
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
},
"smmap": {
@@ -1877,6 +1975,7 @@
"sha256:7bfcf367828031dc893530a29cb35eb8c8f2d7c8f2d0989354d75d24c8573714",
"sha256:84c2751ef3072d4f6b2785ec7ee40244c6f45eb934d9e543e2c51f1bd3d54c50"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.0.5"
},
"snowballstemmer": {
@@ -1891,6 +1990,7 @@
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
],
+ "markers": "python_version >= '3.5'",
"version": "==0.4.1"
},
"stevedore": {
@@ -1898,6 +1998,7 @@
"sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee",
"sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"
],
+ "markers": "python_version >= '3.6'",
"version": "==3.3.0"
},
"toml": {
@@ -1905,6 +2006,7 @@
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2"
},
"typed-ast": {
diff --git a/authentik/admin/templates/administration/stage/list.html b/authentik/admin/templates/administration/stage/list.html
index def5e20ba..d71c30348 100644
--- a/authentik/admin/templates/administration/stage/list.html
+++ b/authentik/admin/templates/administration/stage/list.html
@@ -68,7 +68,7 @@
{% for flow in stage.flow_set.all %}
- - {{ flow.slug }}<
+ - {{ flow.slug }}
{% empty %}
- -
{% endfor %}
diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py
index 71e27a9cf..aaa1355c6 100644
--- a/authentik/api/v2/urls.py
+++ b/authentik/api/v2/urls.py
@@ -57,15 +57,18 @@ from authentik.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProvide
from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
from authentik.sources.oauth.api import OAuthSourceViewSet
from authentik.sources.saml.api import SAMLSourceViewSet
+from authentik.stages.authenticator_static.api import AuthenticatorStaticStageViewSet
+from authentik.stages.authenticator_totp.api import AuthenticatorTOTPStageViewSet
+from authentik.stages.authenticator_validate.api import (
+ AuthenticatorValidateStageViewSet,
+)
+from authentik.stages.authenticator_webauthn.api import AuthenticateWebAuthnStageViewSet
from authentik.stages.captcha.api import CaptchaStageViewSet
from authentik.stages.consent.api import ConsentStageViewSet
from authentik.stages.dummy.api import DummyStageViewSet
from authentik.stages.email.api import EmailStageViewSet
from authentik.stages.identification.api import IdentificationStageViewSet
from authentik.stages.invitation.api import InvitationStageViewSet, InvitationViewSet
-from authentik.stages.otp_static.api import OTPStaticStageViewSet
-from authentik.stages.otp_time.api import OTPTimeStageViewSet
-from authentik.stages.otp_validate.api import OTPValidateStageViewSet
from authentik.stages.password.api import PasswordStageViewSet
from authentik.stages.prompt.api import PromptStageViewSet, PromptViewSet
from authentik.stages.user_delete.api import UserDeleteStageViewSet
@@ -134,15 +137,16 @@ router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
router.register("propertymappings/scope", ScopeMappingViewSet)
router.register("stages/all", StageViewSet)
+router.register("stages/authenticator/static", AuthenticatorStaticStageViewSet)
+router.register("stages/authenticator/totp", AuthenticatorTOTPStageViewSet)
+router.register("stages/authenticator/validate", AuthenticatorValidateStageViewSet)
+router.register("stages/authenticator/webauthn", AuthenticateWebAuthnStageViewSet)
router.register("stages/captcha", CaptchaStageViewSet)
router.register("stages/consent", ConsentStageViewSet)
router.register("stages/email", EmailStageViewSet)
router.register("stages/identification", IdentificationStageViewSet)
router.register("stages/invitation", InvitationStageViewSet)
router.register("stages/invitation/invitations", InvitationViewSet)
-router.register("stages/otp_static", OTPStaticStageViewSet)
-router.register("stages/otp_time", OTPTimeStageViewSet)
-router.register("stages/otp_validate", OTPValidateStageViewSet)
router.register("stages/password", PasswordStageViewSet)
router.register("stages/prompt/prompts", PromptViewSet)
router.register("stages/prompt/stages", PromptStageViewSet)
diff --git a/authentik/flows/api.py b/authentik/flows/api.py
index 4389f8fe9..dc66c5414 100644
--- a/authentik/flows/api.py
+++ b/authentik/flows/api.py
@@ -3,7 +3,7 @@ from dataclasses import dataclass
from django.core.cache import cache
from django.db.models import Model
-from django.shortcuts import get_object_or_404
+from django.shortcuts import get_object_or_404, reverse
from drf_yasg2.utils import swagger_auto_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
@@ -18,8 +18,11 @@ from rest_framework.serializers import (
)
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
+from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.flows.models import Flow, FlowStageBinding, Stage
from authentik.flows.planner import cache_key
+from authentik.lib.templatetags.authentik_utils import verbose_name
+from authentik.lib.utils.reflection import all_subclasses
class FlowSerializer(ModelSerializer):
@@ -154,24 +157,19 @@ class FlowViewSet(ModelViewSet):
return Response({"diagram": diagram})
-class StageSerializer(ModelSerializer):
+class StageSerializer(ModelSerializer, MetaNameSerializer):
"""Stage Serializer"""
- __type__ = SerializerMethodField(method_name="get_type")
- verbose_name = SerializerMethodField(method_name="get_verbose_name")
+ object_type = SerializerMethodField()
- def get_type(self, obj: Stage) -> str:
+ def get_object_type(self, obj):
"""Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace("stage", "")
- def get_verbose_name(self, obj: Stage) -> str:
- """Get verbose name for UI"""
- return obj._meta.verbose_name
-
class Meta:
model = Stage
- fields = ["pk", "name", "__type__", "verbose_name"]
+ fields = ["pk", "name", "object_type", "verbose_name", "verbose_name_plural"]
class StageViewSet(ReadOnlyModelViewSet):
@@ -183,6 +181,23 @@ class StageViewSet(ReadOnlyModelViewSet):
def get_queryset(self):
return Stage.objects.select_subclasses()
+ @swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
+ @action(detail=False)
+ def types(self, request: Request) -> Response:
+ """Get all creatable stage types"""
+ data = []
+ for subclass in all_subclasses(self.queryset.model, False):
+ data.append(
+ {
+ "name": verbose_name(subclass),
+ "description": subclass.__doc__,
+ "link": reverse("authentik_admin:stage-create")
+ + f"?type={subclass.__name__}",
+ }
+ )
+ data = sorted(data, key=lambda x: x["name"])
+ return Response(TypeCreateSerializer(data, many=True).data)
+
class FlowStageBindingSerializer(ModelSerializer):
"""FlowStageBinding Serializer"""
diff --git a/authentik/flows/migrations/0008_default_flows.py b/authentik/flows/migrations/0008_default_flows.py
index d903d8b4e..50b27bb40 100644
--- a/authentik/flows/migrations/0008_default_flows.py
+++ b/authentik/flows/migrations/0008_default_flows.py
@@ -50,21 +50,21 @@ def create_default_authentication_flow(
target=flow,
stage=identification_stage,
defaults={
- "order": 0,
+ "order": 10,
},
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow,
stage=password_stage,
defaults={
- "order": 1,
+ "order": 20,
},
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow,
stage=login_stage,
defaults={
- "order": 2,
+ "order": 100,
},
)
diff --git a/authentik/flows/tests/test_api.py b/authentik/flows/tests/test_api.py
index 3dafebfc3..4aa1400aa 100644
--- a/authentik/flows/tests/test_api.py
+++ b/authentik/flows/tests/test_api.py
@@ -37,7 +37,7 @@ class TestFlowsAPI(APITestCase):
def test_api_serializer(self):
"""Test that stage serializer returns the correct type"""
obj = DummyStage()
- self.assertEqual(StageSerializer().get_type(obj), "dummy")
+ self.assertEqual(StageSerializer().get_object_type(obj), "dummy")
self.assertEqual(StageSerializer().get_verbose_name(obj), "Dummy Stage")
def test_api_viewset(self):
diff --git a/authentik/flows/transfer/common.py b/authentik/flows/transfer/common.py
index 9c022dffe..7309e4d30 100644
--- a/authentik/flows/transfer/common.py
+++ b/authentik/flows/transfer/common.py
@@ -1,9 +1,10 @@
"""transfer common classes"""
from dataclasses import asdict, dataclass, field, is_dataclass
-from json.encoder import JSONEncoder
from typing import Any, Dict, List
from uuid import UUID
+from django.core.serializers.json import DjangoJSONEncoder
+
from authentik.lib.models import SerializerModel
from authentik.lib.sentry import SentryIgnoredException
@@ -11,7 +12,17 @@ from authentik.lib.sentry import SentryIgnoredException
def get_attrs(obj: SerializerModel) -> Dict[str, Any]:
"""Get object's attributes via their serializer, and covert it to a normal dict"""
data = dict(obj.serializer(obj).data)
- to_remove = ("policies", "stages", "pk", "background", "group", "user")
+ to_remove = (
+ "policies",
+ "stages",
+ "pk",
+ "background",
+ "group",
+ "user",
+ "verbose_name",
+ "verbose_name_plural",
+ "object_type",
+ )
for to_remove_name in to_remove:
if to_remove_name in data:
data.pop(to_remove_name)
@@ -53,7 +64,7 @@ class FlowBundle:
entries: List[FlowBundleEntry] = field(default_factory=list)
-class DataclassEncoder(JSONEncoder):
+class DataclassEncoder(DjangoJSONEncoder):
"""Convert FlowBundleEntry to json"""
def default(self, o):
diff --git a/authentik/flows/transfer/exporter.py b/authentik/flows/transfer/exporter.py
index 87ecd0f61..63faa33c0 100644
--- a/authentik/flows/transfer/exporter.py
+++ b/authentik/flows/transfer/exporter.py
@@ -98,4 +98,5 @@ class FlowExporter:
def export_to_string(self) -> str:
"""Call export and convert it to json"""
- return dumps(self.export(), cls=DataclassEncoder)
+ bundle = self.export()
+ return dumps(bundle, cls=DataclassEncoder)
diff --git a/authentik/policies/event_matcher/migrations/0003_auto_20210110_1907.py b/authentik/policies/event_matcher/migrations/0003_auto_20210110_1907.py
index 6444b413a..4ed20e27b 100644
--- a/authentik/policies/event_matcher/migrations/0003_auto_20210110_1907.py
+++ b/authentik/policies/event_matcher/migrations/0003_auto_20210110_1907.py
@@ -60,8 +60,11 @@ class Migration(migrations.Migration):
("authentik.stages.user_logout", "authentik Stages.User Logout"),
("authentik.stages.user_write", "authentik Stages.User Write"),
("authentik.stages.otp_static", "authentik OTP.Static"),
- ("authentik.stages.otp_time", "authentik OTP.Time"),
- ("authentik.stages.otp_validate", "authentik OTP.Validate"),
+ ("authentik.stages.authenticator_totp", "authentik OTP.Time"),
+ (
+ "authentik.stages.authenticator_validate",
+ "authentik OTP.Validate",
+ ),
("authentik.stages.password", "authentik Stages.Password"),
("authentik.core", "authentik Core"),
],
diff --git a/authentik/policies/event_matcher/migrations/0004_auto_20210112_2158.py b/authentik/policies/event_matcher/migrations/0004_auto_20210112_2158.py
index d4a6112f2..21d531790 100644
--- a/authentik/policies/event_matcher/migrations/0004_auto_20210112_2158.py
+++ b/authentik/policies/event_matcher/migrations/0004_auto_20210112_2158.py
@@ -67,8 +67,14 @@ class Migration(migrations.Migration):
("authentik.stages.user_logout", "authentik Stages.User Logout"),
("authentik.stages.user_write", "authentik Stages.User Write"),
("authentik.stages.otp_static", "authentik Stages.OTP.Static"),
- ("authentik.stages.otp_time", "authentik Stages.OTP.Time"),
- ("authentik.stages.otp_validate", "authentik Stages.OTP.Validate"),
+ (
+ "authentik.stages.authenticator_totp",
+ "authentik Stages.OTP.Time",
+ ),
+ (
+ "authentik.stages.authenticator_validate",
+ "authentik Stages.OTP.Validate",
+ ),
("authentik.stages.password", "authentik Stages.Password"),
("authentik.core", "authentik Core"),
],
diff --git a/authentik/policies/event_matcher/migrations/0006_auto_20210203_1134.py b/authentik/policies/event_matcher/migrations/0006_auto_20210203_1134.py
index 33b010f24..0a311504e 100644
--- a/authentik/policies/event_matcher/migrations/0006_auto_20210203_1134.py
+++ b/authentik/policies/event_matcher/migrations/0006_auto_20210203_1134.py
@@ -60,8 +60,14 @@ class Migration(migrations.Migration):
("authentik.stages.user_logout", "authentik Stages.User Logout"),
("authentik.stages.user_write", "authentik Stages.User Write"),
("authentik.stages.otp_static", "authentik Stages.OTP.Static"),
- ("authentik.stages.otp_time", "authentik Stages.OTP.Time"),
- ("authentik.stages.otp_validate", "authentik Stages.OTP.Validate"),
+ (
+ "authentik.stages.authenticator_totp",
+ "authentik Stages.OTP.Time",
+ ),
+ (
+ "authentik.stages.authenticator_validate",
+ "authentik Stages.OTP.Validate",
+ ),
("authentik.stages.password", "authentik Stages.Password"),
("authentik.managed", "authentik Managed"),
("authentik.core", "authentik Core"),
diff --git a/authentik/policies/event_matcher/migrations/0008_auto_20210213_1640.py b/authentik/policies/event_matcher/migrations/0008_auto_20210213_1640.py
new file mode 100644
index 000000000..a57931b9c
--- /dev/null
+++ b/authentik/policies/event_matcher/migrations/0008_auto_20210213_1640.py
@@ -0,0 +1,83 @@
+# Generated by Django 3.1.6 on 2021-02-13 16:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_policies_event_matcher", "0007_auto_20210209_1657"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="eventmatcherpolicy",
+ name="app",
+ field=models.TextField(
+ blank=True,
+ choices=[
+ ("authentik.admin", "authentik Admin"),
+ ("authentik.api", "authentik API"),
+ ("authentik.events", "authentik Events"),
+ ("authentik.crypto", "authentik Crypto"),
+ ("authentik.flows", "authentik Flows"),
+ ("authentik.outposts", "authentik Outpost"),
+ ("authentik.lib", "authentik lib"),
+ ("authentik.policies", "authentik Policies"),
+ ("authentik.policies.dummy", "authentik Policies.Dummy"),
+ (
+ "authentik.policies.event_matcher",
+ "authentik Policies.Event Matcher",
+ ),
+ ("authentik.policies.expiry", "authentik Policies.Expiry"),
+ ("authentik.policies.expression", "authentik Policies.Expression"),
+ (
+ "authentik.policies.group_membership",
+ "authentik Policies.Group Membership",
+ ),
+ ("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"),
+ ("authentik.policies.password", "authentik Policies.Password"),
+ ("authentik.policies.reputation", "authentik Policies.Reputation"),
+ ("authentik.providers.proxy", "authentik Providers.Proxy"),
+ ("authentik.providers.oauth2", "authentik Providers.OAuth2"),
+ ("authentik.providers.saml", "authentik Providers.SAML"),
+ ("authentik.recovery", "authentik Recovery"),
+ ("authentik.sources.ldap", "authentik Sources.LDAP"),
+ ("authentik.sources.oauth", "authentik Sources.OAuth"),
+ ("authentik.sources.saml", "authentik Sources.SAML"),
+ ("authentik.stages.captcha", "authentik Stages.Captcha"),
+ ("authentik.stages.consent", "authentik Stages.Consent"),
+ ("authentik.stages.dummy", "authentik Stages.Dummy"),
+ ("authentik.stages.email", "authentik Stages.Email"),
+ ("authentik.stages.prompt", "authentik Stages.Prompt"),
+ (
+ "authentik.stages.identification",
+ "authentik Stages.Identification",
+ ),
+ ("authentik.stages.invitation", "authentik Stages.User Invitation"),
+ ("authentik.stages.user_delete", "authentik Stages.User Delete"),
+ ("authentik.stages.user_login", "authentik Stages.User Login"),
+ ("authentik.stages.user_logout", "authentik Stages.User Logout"),
+ ("authentik.stages.user_write", "authentik Stages.User Write"),
+ ("authentik.stages.otp_static", "authentik Stages.OTP.Static"),
+ (
+ "authentik.stages.authenticator_totp",
+ "authentik Stages.OTP.Time",
+ ),
+ (
+ "authentik.stages.authenticator_validate",
+ "authentik Stages.OTP.Validate",
+ ),
+ ("authentik.stages.password", "authentik Stages.Password"),
+ (
+ "authentik.stages.authenticator_webauthn",
+ "authentik Stages.WebAuthn",
+ ),
+ ("authentik.managed", "authentik Managed"),
+ ("authentik.core", "authentik Core"),
+ ],
+ default="",
+ help_text="Match events created by selected application. When left empty, all applications are matched.",
+ ),
+ ),
+ ]
diff --git a/authentik/policies/event_matcher/migrations/0009_auto_20210215_2159.py b/authentik/policies/event_matcher/migrations/0009_auto_20210215_2159.py
new file mode 100644
index 000000000..99f0faecc
--- /dev/null
+++ b/authentik/policies/event_matcher/migrations/0009_auto_20210215_2159.py
@@ -0,0 +1,86 @@
+# Generated by Django 3.1.6 on 2021-02-15 21:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_policies_event_matcher", "0008_auto_20210213_1640"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="eventmatcherpolicy",
+ name="app",
+ field=models.TextField(
+ blank=True,
+ choices=[
+ ("authentik.admin", "authentik Admin"),
+ ("authentik.api", "authentik API"),
+ ("authentik.events", "authentik Events"),
+ ("authentik.crypto", "authentik Crypto"),
+ ("authentik.flows", "authentik Flows"),
+ ("authentik.outposts", "authentik Outpost"),
+ ("authentik.lib", "authentik lib"),
+ ("authentik.policies", "authentik Policies"),
+ ("authentik.policies.dummy", "authentik Policies.Dummy"),
+ (
+ "authentik.policies.event_matcher",
+ "authentik Policies.Event Matcher",
+ ),
+ ("authentik.policies.expiry", "authentik Policies.Expiry"),
+ ("authentik.policies.expression", "authentik Policies.Expression"),
+ (
+ "authentik.policies.group_membership",
+ "authentik Policies.Group Membership",
+ ),
+ ("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"),
+ ("authentik.policies.password", "authentik Policies.Password"),
+ ("authentik.policies.reputation", "authentik Policies.Reputation"),
+ ("authentik.providers.proxy", "authentik Providers.Proxy"),
+ ("authentik.providers.oauth2", "authentik Providers.OAuth2"),
+ ("authentik.providers.saml", "authentik Providers.SAML"),
+ ("authentik.recovery", "authentik Recovery"),
+ ("authentik.sources.ldap", "authentik Sources.LDAP"),
+ ("authentik.sources.oauth", "authentik Sources.OAuth"),
+ ("authentik.sources.saml", "authentik Sources.SAML"),
+ ("authentik.stages.captcha", "authentik Stages.Captcha"),
+ ("authentik.stages.consent", "authentik Stages.Consent"),
+ ("authentik.stages.dummy", "authentik Stages.Dummy"),
+ ("authentik.stages.email", "authentik Stages.Email"),
+ ("authentik.stages.prompt", "authentik Stages.Prompt"),
+ (
+ "authentik.stages.identification",
+ "authentik Stages.Identification",
+ ),
+ ("authentik.stages.invitation", "authentik Stages.User Invitation"),
+ ("authentik.stages.user_delete", "authentik Stages.User Delete"),
+ ("authentik.stages.user_login", "authentik Stages.User Login"),
+ ("authentik.stages.user_logout", "authentik Stages.User Logout"),
+ ("authentik.stages.user_write", "authentik Stages.User Write"),
+ (
+ "authentik.stages.authenticator_static",
+ "authentik Stages.Authenticator.Static",
+ ),
+ (
+ "authentik.stages.authenticator_totp",
+ "authentik Stages.Authenticator.TOTP",
+ ),
+ (
+ "authentik.stages.authenticator_validate",
+ "authentik Stages.Authenticator.Validate",
+ ),
+ (
+ "authentik.stages.authenticator_webauthn",
+ "authentik Stages.Authenticator.WebAuthn",
+ ),
+ ("authentik.stages.password", "authentik Stages.Password"),
+ ("authentik.managed", "authentik Managed"),
+ ("authentik.core", "authentik Core"),
+ ],
+ default="",
+ help_text="Match events created by selected application. When left empty, all applications are matched.",
+ ),
+ ),
+ ]
diff --git a/authentik/root/settings.py b/authentik/root/settings.py
index 665465782..8197138f7 100644
--- a/authentik/root/settings.py
+++ b/authentik/root/settings.py
@@ -119,9 +119,10 @@ INSTALLED_APPS = [
"authentik.stages.user_login.apps.AuthentikStageUserLoginConfig",
"authentik.stages.user_logout.apps.AuthentikStageUserLogoutConfig",
"authentik.stages.user_write.apps.AuthentikStageUserWriteConfig",
- "authentik.stages.otp_static.apps.AuthentikStageOTPStaticConfig",
- "authentik.stages.otp_time.apps.AuthentikStageOTPTimeConfig",
- "authentik.stages.otp_validate.apps.AuthentikStageOTPValidateConfig",
+ "authentik.stages.authenticator_static.apps.AuthentikStageAuthenticatorStaticConfig",
+ "authentik.stages.authenticator_totp.apps.AuthentikStageAuthenticatorTOTPConfig",
+ "authentik.stages.authenticator_validate.apps.AuthentikStageAuthenticatorValidateConfig",
+ "authentik.stages.authenticator_webauthn.apps.AuthentikStageAuthenticatorWebAuthnConfig",
"authentik.stages.password.apps.AuthentikStagePasswordConfig",
"rest_framework",
"django_filters",
diff --git a/authentik/stages/otp_static/__init__.py b/authentik/stages/authenticator_static/__init__.py
similarity index 100%
rename from authentik/stages/otp_static/__init__.py
rename to authentik/stages/authenticator_static/__init__.py
diff --git a/authentik/stages/authenticator_static/api.py b/authentik/stages/authenticator_static/api.py
new file mode 100644
index 000000000..0663863ca
--- /dev/null
+++ b/authentik/stages/authenticator_static/api.py
@@ -0,0 +1,21 @@
+"""AuthenticatorStaticStage API Views"""
+from rest_framework.viewsets import ModelViewSet
+
+from authentik.flows.api import StageSerializer
+from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
+
+
+class AuthenticatorStaticStageSerializer(StageSerializer):
+ """AuthenticatorStaticStage Serializer"""
+
+ class Meta:
+
+ model = AuthenticatorStaticStage
+ fields = StageSerializer.Meta.fields + ["configure_flow", "token_count"]
+
+
+class AuthenticatorStaticStageViewSet(ModelViewSet):
+ """AuthenticatorStaticStage Viewset"""
+
+ queryset = AuthenticatorStaticStage.objects.all()
+ serializer_class = AuthenticatorStaticStageSerializer
diff --git a/authentik/stages/authenticator_static/apps.py b/authentik/stages/authenticator_static/apps.py
new file mode 100644
index 000000000..e79ea32db
--- /dev/null
+++ b/authentik/stages/authenticator_static/apps.py
@@ -0,0 +1,11 @@
+"""Authenticator Static stage"""
+from django.apps import AppConfig
+
+
+class AuthentikStageAuthenticatorStaticConfig(AppConfig):
+ """Authenticator Static stage"""
+
+ name = "authentik.stages.authenticator_static"
+ label = "authentik_stages_authenticator_static"
+ verbose_name = "authentik Stages.Authenticator.Static"
+ mountpoint = "-/user/authenticator/static/"
diff --git a/authentik/stages/otp_static/forms.py b/authentik/stages/authenticator_static/forms.py
similarity index 77%
rename from authentik/stages/otp_static/forms.py
rename to authentik/stages/authenticator_static/forms.py
index 2864dfaea..9195c84fd 100644
--- a/authentik/stages/otp_static/forms.py
+++ b/authentik/stages/authenticator_static/forms.py
@@ -1,8 +1,8 @@
-"""OTP Static forms"""
+"""Static Authenticator forms"""
from django import forms
from django.utils.safestring import mark_safe
-from authentik.stages.otp_static.models import OTPStaticStage
+from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
class StaticTokenWidget(forms.widgets.Widget):
@@ -26,12 +26,12 @@ class SetupForm(forms.Form):
self.fields["tokens"].initial = tokens
-class OTPStaticStageForm(forms.ModelForm):
- """OTP Static Stage setup form"""
+class AuthenticatorStaticStageForm(forms.ModelForm):
+ """Static Authenticator Stage setup form"""
class Meta:
- model = OTPStaticStage
+ model = AuthenticatorStaticStage
fields = ["name", "configure_flow", "token_count"]
widgets = {
diff --git a/authentik/stages/otp_static/migrations/0001_initial.py b/authentik/stages/authenticator_static/migrations/0001_initial.py
similarity index 87%
rename from authentik/stages/otp_static/migrations/0001_initial.py
rename to authentik/stages/authenticator_static/migrations/0001_initial.py
index 4494d16ae..21f2bd60f 100644
--- a/authentik/stages/otp_static/migrations/0001_initial.py
+++ b/authentik/stages/authenticator_static/migrations/0001_initial.py
@@ -30,8 +30,8 @@ class Migration(migrations.Migration):
("token_count", models.IntegerField(default=6)),
],
options={
- "verbose_name": "OTP Static Setup Stage",
- "verbose_name_plural": "OTP Static Setup Stages",
+ "verbose_name": "Static Authenticator Setup Stage",
+ "verbose_name_plural": "Static Authenticator Setup Stages",
},
bases=("authentik_flows.stage",),
),
diff --git a/authentik/stages/otp_static/migrations/0002_otpstaticstage_configure_flow.py b/authentik/stages/authenticator_static/migrations/0002_otpstaticstage_configure_flow.py
similarity index 91%
rename from authentik/stages/otp_static/migrations/0002_otpstaticstage_configure_flow.py
rename to authentik/stages/authenticator_static/migrations/0002_otpstaticstage_configure_flow.py
index e6f27b206..73110bc72 100644
--- a/authentik/stages/otp_static/migrations/0002_otpstaticstage_configure_flow.py
+++ b/authentik/stages/authenticator_static/migrations/0002_otpstaticstage_configure_flow.py
@@ -8,7 +8,7 @@ class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0013_auto_20200924_1605"),
- ("authentik_stages_otp_static", "0001_initial"),
+ ("authentik_stages_authenticator_static", "0001_initial"),
]
operations = [
diff --git a/authentik/stages/authenticator_static/migrations/0003_default_setup_flow.py b/authentik/stages/authenticator_static/migrations/0003_default_setup_flow.py
new file mode 100644
index 000000000..f4d223d57
--- /dev/null
+++ b/authentik/stages/authenticator_static/migrations/0003_default_setup_flow.py
@@ -0,0 +1,15 @@
+# Generated by Django 3.1.1 on 2020-09-25 14:32
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ (
+ "authentik_stages_authenticator_static",
+ "0002_otpstaticstage_configure_flow",
+ ),
+ ]
+
+ operations = []
diff --git a/authentik/stages/authenticator_static/migrations/0004_auto_20210216_0838.py b/authentik/stages/authenticator_static/migrations/0004_auto_20210216_0838.py
new file mode 100644
index 000000000..92eada0ce
--- /dev/null
+++ b/authentik/stages/authenticator_static/migrations/0004_auto_20210216_0838.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.1.6 on 2021-02-16 08:38
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_flows", "0016_auto_20201202_1307"),
+ ("authentik_stages_authenticator_static", "0003_default_setup_flow"),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name="OTPStaticStage",
+ new_name="AuthenticatorStaticStage",
+ ),
+ migrations.AlterModelOptions(
+ name="authenticatorstaticstage",
+ options={
+ "verbose_name": "Static Authenticator Stage",
+ "verbose_name_plural": "Static Authenticator Stages",
+ },
+ ),
+ ]
diff --git a/authentik/stages/authenticator_static/migrations/0005_default_setup_flow.py b/authentik/stages/authenticator_static/migrations/0005_default_setup_flow.py
new file mode 100644
index 000000000..de787105e
--- /dev/null
+++ b/authentik/stages/authenticator_static/migrations/0005_default_setup_flow.py
@@ -0,0 +1,55 @@
+# Generated by Django 3.1.1 on 2020-09-25 14:32
+
+from django.apps.registry import Apps
+from django.db import migrations
+from django.db.backends.base.schema import BaseDatabaseSchemaEditor
+
+from authentik.flows.models import FlowDesignation
+
+
+def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
+ Flow = apps.get_model("authentik_flows", "Flow")
+ FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
+
+ AuthenticatorStaticStage = apps.get_model(
+ "authentik_stages_authenticator_static", "AuthenticatorStaticStage"
+ )
+
+ db_alias = schema_editor.connection.alias
+
+ flow, _ = Flow.objects.using(db_alias).update_or_create(
+ slug="default-authenticator-static-setup",
+ designation=FlowDesignation.STAGE_CONFIGURATION,
+ defaults={
+ "name": "default-authenticator-static-setup",
+ "title": "Setup Static OTP Tokens",
+ },
+ )
+
+ stage, _ = AuthenticatorStaticStage.objects.using(db_alias).update_or_create(
+ name="default-authenticator-static-setup", defaults={"token_count": 6}
+ )
+
+ FlowStageBinding.objects.using(db_alias).update_or_create(
+ target=flow, stage=stage, defaults={"order": 0}
+ )
+
+ for stage in AuthenticatorStaticStage.objects.using(db_alias).filter(
+ configure_flow=None
+ ):
+ stage.configure_flow = flow
+ stage.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ (
+ "authentik_stages_authenticator_static",
+ "0004_auto_20210216_0838",
+ ),
+ ]
+
+ operations = [
+ migrations.RunPython(create_default_setup_flow),
+ ]
diff --git a/authentik/stages/otp_static/migrations/__init__.py b/authentik/stages/authenticator_static/migrations/__init__.py
similarity index 100%
rename from authentik/stages/otp_static/migrations/__init__.py
rename to authentik/stages/authenticator_static/migrations/__init__.py
diff --git a/authentik/stages/authenticator_static/models.py b/authentik/stages/authenticator_static/models.py
new file mode 100644
index 000000000..aa76a955b
--- /dev/null
+++ b/authentik/stages/authenticator_static/models.py
@@ -0,0 +1,56 @@
+"""Static Authenticator models"""
+from typing import Optional, Type
+
+from django.db import models
+from django.forms import ModelForm
+from django.shortcuts import reverse
+from django.utils.translation import gettext_lazy as _
+from django.views import View
+from rest_framework.serializers import BaseSerializer
+
+from authentik.flows.models import ConfigurableStage, Stage
+
+
+class AuthenticatorStaticStage(ConfigurableStage, Stage):
+ """Generate static tokens for the user as a backup."""
+
+ token_count = models.IntegerField(default=6)
+
+ @property
+ def serializer(self) -> BaseSerializer:
+ from authentik.stages.authenticator_static.api import (
+ AuthenticatorStaticStageSerializer,
+ )
+
+ return AuthenticatorStaticStageSerializer
+
+ @property
+ def type(self) -> Type[View]:
+ from authentik.stages.authenticator_static.stage import (
+ AuthenticatorStaticStageView,
+ )
+
+ return AuthenticatorStaticStageView
+
+ @property
+ def form(self) -> Type[ModelForm]:
+ from authentik.stages.authenticator_static.forms import (
+ AuthenticatorStaticStageForm,
+ )
+
+ return AuthenticatorStaticStageForm
+
+ @property
+ def ui_user_settings(self) -> Optional[str]:
+ return reverse(
+ "authentik_stages_authenticator_static:user-settings",
+ kwargs={"stage_uuid": self.stage_uuid},
+ )
+
+ def __str__(self) -> str:
+ return f"Static Authenticator Stage {self.name}"
+
+ class Meta:
+
+ verbose_name = _("Static Authenticator Stage")
+ verbose_name_plural = _("Static Authenticator Stages")
diff --git a/authentik/stages/otp_static/settings.py b/authentik/stages/authenticator_static/settings.py
similarity index 62%
rename from authentik/stages/otp_static/settings.py
rename to authentik/stages/authenticator_static/settings.py
index 9c3cc4952..24b347ccc 100644
--- a/authentik/stages/otp_static/settings.py
+++ b/authentik/stages/authenticator_static/settings.py
@@ -1,4 +1,4 @@
-"""OTP Static settings"""
+"""Static Authenticator settings"""
INSTALLED_APPS = [
"django_otp.plugins.otp_static",
diff --git a/authentik/stages/otp_static/stage.py b/authentik/stages/authenticator_static/stage.py
similarity index 89%
rename from authentik/stages/otp_static/stage.py
rename to authentik/stages/authenticator_static/stage.py
index a51bb0af9..f02778297 100644
--- a/authentik/stages/otp_static/stage.py
+++ b/authentik/stages/authenticator_static/stage.py
@@ -8,15 +8,15 @@ from structlog.stdlib import get_logger
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import StageView
-from authentik.stages.otp_static.forms import SetupForm
-from authentik.stages.otp_static.models import OTPStaticStage
+from authentik.stages.authenticator_static.forms import SetupForm
+from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
LOGGER = get_logger()
SESSION_STATIC_DEVICE = "static_device"
SESSION_STATIC_TOKENS = "static_device_tokens"
-class OTPStaticStageView(FormView, StageView):
+class AuthenticatorStaticStageView(FormView, StageView):
"""Static OTP Setup stage"""
form_class = SetupForm
@@ -38,7 +38,7 @@ class OTPStaticStageView(FormView, StageView):
if StaticDevice.objects.filter(user=user).exists():
return self.executor.stage_ok()
- stage: OTPStaticStage = self.executor.current_stage
+ stage: AuthenticatorStaticStage = self.executor.current_stage
if SESSION_STATIC_DEVICE not in self.request.session:
device = StaticDevice(user=user, confirmed=True)
diff --git a/authentik/stages/otp_static/templates/stages/otp_static/user_settings.html b/authentik/stages/authenticator_static/templates/stages/authenticator_static/user_settings.html
similarity index 72%
rename from authentik/stages/otp_static/templates/stages/otp_static/user_settings.html
rename to authentik/stages/authenticator_static/templates/stages/authenticator_static/user_settings.html
index 1c723707d..b69f37225 100644
--- a/authentik/stages/otp_static/templates/stages/otp_static/user_settings.html
+++ b/authentik/stages/authenticator_static/templates/stages/authenticator_static/user_settings.html
@@ -22,10 +22,10 @@
{% if not state %}
{% if stage.configure_flow %}
- {% trans "Enable Static Tokens" %}
+ {% trans "Enable Static Tokens" %}
{% endif %}
{% else %}
- {% trans "Disable Static Tokens" %}
+ {% trans "Disable Static Tokens" %}
{% endif %}
diff --git a/authentik/stages/otp_static/urls.py b/authentik/stages/authenticator_static/urls.py
similarity index 66%
rename from authentik/stages/otp_static/urls.py
rename to authentik/stages/authenticator_static/urls.py
index 55ee7807c..505473312 100644
--- a/authentik/stages/otp_static/urls.py
+++ b/authentik/stages/authenticator_static/urls.py
@@ -1,7 +1,7 @@
-"""OTP static urls"""
+"""Static Authenticator urls"""
from django.urls import path
-from authentik.stages.otp_static.views import DisableView, UserSettingsView
+from authentik.stages.authenticator_static.views import DisableView, UserSettingsView
urlpatterns = [
path(
diff --git a/authentik/stages/otp_static/views.py b/authentik/stages/authenticator_static/views.py
similarity index 83%
rename from authentik/stages/otp_static/views.py
rename to authentik/stages/authenticator_static/views.py
index ccc495240..2d4fb048b 100644
--- a/authentik/stages/otp_static/views.py
+++ b/authentik/stages/authenticator_static/views.py
@@ -1,4 +1,4 @@
-"""otp Static view Tokens"""
+"""Static Authenticator view Tokens"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse
@@ -8,17 +8,19 @@ from django.views.generic import TemplateView
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
from authentik.events.models import Event
-from authentik.stages.otp_static.models import OTPStaticStage
+from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
class UserSettingsView(LoginRequiredMixin, TemplateView):
"""View for user settings to control OTP"""
- template_name = "stages/otp_static/user_settings.html"
+ template_name = "stages/authenticator_static/user_settings.html"
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
- stage = get_object_or_404(OTPStaticStage, pk=self.kwargs["stage_uuid"])
+ stage = get_object_or_404(
+ AuthenticatorStaticStage, pk=self.kwargs["stage_uuid"]
+ )
kwargs["stage"] = stage
static_devices = StaticDevice.objects.filter(
user=self.request.user, confirmed=True
diff --git a/authentik/stages/otp_time/__init__.py b/authentik/stages/authenticator_totp/__init__.py
similarity index 100%
rename from authentik/stages/otp_time/__init__.py
rename to authentik/stages/authenticator_totp/__init__.py
diff --git a/authentik/stages/authenticator_totp/api.py b/authentik/stages/authenticator_totp/api.py
new file mode 100644
index 000000000..6a8dab0d8
--- /dev/null
+++ b/authentik/stages/authenticator_totp/api.py
@@ -0,0 +1,21 @@
+"""AuthenticatorTOTPStage API Views"""
+from rest_framework.viewsets import ModelViewSet
+
+from authentik.flows.api import StageSerializer
+from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
+
+
+class AuthenticatorTOTPStageSerializer(StageSerializer):
+ """AuthenticatorTOTPStage Serializer"""
+
+ class Meta:
+
+ model = AuthenticatorTOTPStage
+ fields = StageSerializer.Meta.fields + ["configure_flow", "digits"]
+
+
+class AuthenticatorTOTPStageViewSet(ModelViewSet):
+ """AuthenticatorTOTPStage Viewset"""
+
+ queryset = AuthenticatorTOTPStage.objects.all()
+ serializer_class = AuthenticatorTOTPStageSerializer
diff --git a/authentik/stages/authenticator_totp/apps.py b/authentik/stages/authenticator_totp/apps.py
new file mode 100644
index 000000000..008351c29
--- /dev/null
+++ b/authentik/stages/authenticator_totp/apps.py
@@ -0,0 +1,11 @@
+"""OTP Time"""
+from django.apps import AppConfig
+
+
+class AuthentikStageAuthenticatorTOTPConfig(AppConfig):
+ """TOTP App config"""
+
+ name = "authentik.stages.authenticator_totp"
+ label = "authentik_stages_authenticator_totp"
+ verbose_name = "authentik Stages.Authenticator.TOTP"
+ mountpoint = "-/user/authenticator/totp/"
diff --git a/authentik/stages/otp_time/forms.py b/authentik/stages/authenticator_totp/forms.py
similarity index 90%
rename from authentik/stages/otp_time/forms.py
rename to authentik/stages/authenticator_totp/forms.py
index eb40521b8..91d635c92 100644
--- a/authentik/stages/otp_time/forms.py
+++ b/authentik/stages/authenticator_totp/forms.py
@@ -4,7 +4,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django_otp.models import Device
-from authentik.stages.otp_time.models import OTPTimeStage
+from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
class PictureWidget(forms.widgets.Widget):
@@ -49,12 +49,12 @@ class SetupForm(forms.Form):
return self.cleaned_data.get("code")
-class OTPTimeStageForm(forms.ModelForm):
+class AuthenticatorTOTPStageForm(forms.ModelForm):
"""OTP Time-based Stage setup form"""
class Meta:
- model = OTPTimeStage
+ model = AuthenticatorTOTPStage
fields = ["name", "configure_flow", "digits"]
widgets = {
diff --git a/authentik/stages/otp_time/migrations/0001_initial.py b/authentik/stages/authenticator_totp/migrations/0001_initial.py
similarity index 100%
rename from authentik/stages/otp_time/migrations/0001_initial.py
rename to authentik/stages/authenticator_totp/migrations/0001_initial.py
diff --git a/authentik/stages/otp_time/migrations/0002_auto_20200701_1900.py b/authentik/stages/authenticator_totp/migrations/0002_auto_20200701_1900.py
similarity index 89%
rename from authentik/stages/otp_time/migrations/0002_auto_20200701_1900.py
rename to authentik/stages/authenticator_totp/migrations/0002_auto_20200701_1900.py
index 9dca4752f..edcd23104 100644
--- a/authentik/stages/otp_time/migrations/0002_auto_20200701_1900.py
+++ b/authentik/stages/authenticator_totp/migrations/0002_auto_20200701_1900.py
@@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ("authentik_stages_otp_time", "0001_initial"),
+ ("authentik_stages_authenticator_totp", "0001_initial"),
]
operations = [
diff --git a/authentik/stages/otp_time/migrations/0003_otptimestage_configure_flow.py b/authentik/stages/authenticator_totp/migrations/0003_otptimestage_configure_flow.py
similarity index 90%
rename from authentik/stages/otp_time/migrations/0003_otptimestage_configure_flow.py
rename to authentik/stages/authenticator_totp/migrations/0003_otptimestage_configure_flow.py
index 64d09b8b8..a692ec9ca 100644
--- a/authentik/stages/otp_time/migrations/0003_otptimestage_configure_flow.py
+++ b/authentik/stages/authenticator_totp/migrations/0003_otptimestage_configure_flow.py
@@ -8,7 +8,7 @@ class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0013_auto_20200924_1605"),
- ("authentik_stages_otp_time", "0002_auto_20200701_1900"),
+ ("authentik_stages_authenticator_totp", "0002_auto_20200701_1900"),
]
operations = [
diff --git a/authentik/stages/authenticator_totp/migrations/0004_default_setup_flow.py b/authentik/stages/authenticator_totp/migrations/0004_default_setup_flow.py
new file mode 100644
index 000000000..0bc3c705c
--- /dev/null
+++ b/authentik/stages/authenticator_totp/migrations/0004_default_setup_flow.py
@@ -0,0 +1,15 @@
+# Generated by Django 3.1.1 on 2020-09-25 15:36
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ (
+ "authentik_stages_authenticator_totp",
+ "0003_otptimestage_configure_flow",
+ ),
+ ]
+
+ operations = []
diff --git a/authentik/stages/authenticator_totp/migrations/0005_auto_20210216_0838.py b/authentik/stages/authenticator_totp/migrations/0005_auto_20210216_0838.py
new file mode 100644
index 000000000..3ba742263
--- /dev/null
+++ b/authentik/stages/authenticator_totp/migrations/0005_auto_20210216_0838.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.1.6 on 2021-02-16 08:38
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_flows", "0016_auto_20201202_1307"),
+ ("authentik_stages_authenticator_totp", "0004_default_setup_flow"),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name="OTPTimeStage",
+ new_name="AuthenticatorTOTPStage",
+ ),
+ migrations.AlterModelOptions(
+ name="authenticatortotpstage",
+ options={
+ "verbose_name": "TOTP Authenticator Setup Stage",
+ "verbose_name_plural": "TOTP Authenticator Setup Stages",
+ },
+ ),
+ ]
diff --git a/authentik/stages/otp_time/migrations/0004_default_setup_flow.py b/authentik/stages/authenticator_totp/migrations/0006_default_setup_flow.py
similarity index 60%
rename from authentik/stages/otp_time/migrations/0004_default_setup_flow.py
rename to authentik/stages/authenticator_totp/migrations/0006_default_setup_flow.py
index 9b5df159b..85788c348 100644
--- a/authentik/stages/otp_time/migrations/0004_default_setup_flow.py
+++ b/authentik/stages/authenticator_totp/migrations/0006_default_setup_flow.py
@@ -5,35 +5,39 @@ from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.flows.models import FlowDesignation
-from authentik.stages.otp_time.models import TOTPDigits
+from authentik.stages.authenticator_totp.models import TOTPDigits
def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Flow = apps.get_model("authentik_flows", "Flow")
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
- OTPTimeStage = apps.get_model("authentik_stages_otp_time", "OTPTimeStage")
+ AuthenticatorTOTPStage = apps.get_model(
+ "authentik_stages_authenticator_totp", "AuthenticatorTOTPStage"
+ )
db_alias = schema_editor.connection.alias
flow, _ = Flow.objects.using(db_alias).update_or_create(
- slug="default-otp-time-configure",
+ slug="default-authenticator-totp-setup",
designation=FlowDesignation.STAGE_CONFIGURATION,
defaults={
- "name": "default-otp-time-configure",
+ "name": "default-authenticator-totp-setup",
"title": "Setup Two-Factor authentication",
},
)
- stage, _ = OTPTimeStage.objects.using(db_alias).update_or_create(
- name="default-otp-time-configure", defaults={"digits": TOTPDigits.SIX}
+ stage, _ = AuthenticatorTOTPStage.objects.using(db_alias).update_or_create(
+ name="default-authenticator-totp-setup", defaults={"digits": TOTPDigits.SIX}
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow, stage=stage, defaults={"order": 0}
)
- for stage in OTPTimeStage.objects.using(db_alias).filter(configure_flow=None):
+ for stage in AuthenticatorTOTPStage.objects.using(db_alias).filter(
+ configure_flow=None
+ ):
stage.configure_flow = flow
stage.save()
@@ -41,7 +45,10 @@ def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
class Migration(migrations.Migration):
dependencies = [
- ("authentik_stages_otp_time", "0003_otptimestage_configure_flow"),
+ (
+ "authentik_stages_authenticator_totp",
+ "0005_auto_20210216_0838",
+ ),
]
operations = [
diff --git a/authentik/stages/otp_time/migrations/__init__.py b/authentik/stages/authenticator_totp/migrations/__init__.py
similarity index 100%
rename from authentik/stages/otp_time/migrations/__init__.py
rename to authentik/stages/authenticator_totp/migrations/__init__.py
diff --git a/authentik/stages/otp_time/models.py b/authentik/stages/authenticator_totp/models.py
similarity index 58%
rename from authentik/stages/otp_time/models.py
rename to authentik/stages/authenticator_totp/models.py
index 08a54e394..010a1fd97 100644
--- a/authentik/stages/otp_time/models.py
+++ b/authentik/stages/authenticator_totp/models.py
@@ -18,40 +18,42 @@ class TOTPDigits(models.IntegerChoices):
EIGHT = 8, _("8 digits, not compatible with apps like Google Authenticator")
-class OTPTimeStage(ConfigurableStage, Stage):
+class AuthenticatorTOTPStage(ConfigurableStage, Stage):
"""Enroll a user's device into Time-based OTP."""
digits = models.IntegerField(choices=TOTPDigits.choices)
@property
def serializer(self) -> BaseSerializer:
- from authentik.stages.otp_time.api import OTPTimeStageSerializer
+ from authentik.stages.authenticator_totp.api import (
+ AuthenticatorTOTPStageSerializer,
+ )
- return OTPTimeStageSerializer
+ return AuthenticatorTOTPStageSerializer
@property
def type(self) -> Type[View]:
- from authentik.stages.otp_time.stage import OTPTimeStageView
+ from authentik.stages.authenticator_totp.stage import AuthenticatorTOTPStageView
- return OTPTimeStageView
+ return AuthenticatorTOTPStageView
@property
def form(self) -> Type[ModelForm]:
- from authentik.stages.otp_time.forms import OTPTimeStageForm
+ from authentik.stages.authenticator_totp.forms import AuthenticatorTOTPStageForm
- return OTPTimeStageForm
+ return AuthenticatorTOTPStageForm
@property
def ui_user_settings(self) -> Optional[str]:
return reverse(
- "authentik_stages_otp_time:user-settings",
+ "authentik_stages_authenticator_totp:user-settings",
kwargs={"stage_uuid": self.stage_uuid},
)
def __str__(self) -> str:
- return f"OTP Time (TOTP) Stage {self.name}"
+ return f"TOTP Authenticator Setup Stage {self.name}"
class Meta:
- verbose_name = _("OTP Time (TOTP) Setup Stage")
- verbose_name_plural = _("OTP Time (TOTP) Setup Stages")
+ verbose_name = _("TOTP Authenticator Setup Stage")
+ verbose_name_plural = _("TOTP Authenticator Setup Stages")
diff --git a/authentik/stages/otp_time/settings.py b/authentik/stages/authenticator_totp/settings.py
similarity index 100%
rename from authentik/stages/otp_time/settings.py
rename to authentik/stages/authenticator_totp/settings.py
diff --git a/authentik/stages/otp_time/stage.py b/authentik/stages/authenticator_totp/stage.py
similarity index 89%
rename from authentik/stages/otp_time/stage.py
rename to authentik/stages/authenticator_totp/stage.py
index 08984ece2..631b24cf6 100644
--- a/authentik/stages/otp_time/stage.py
+++ b/authentik/stages/authenticator_totp/stage.py
@@ -12,14 +12,14 @@ from structlog.stdlib import get_logger
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import StageView
-from authentik.stages.otp_time.forms import SetupForm
-from authentik.stages.otp_time.models import OTPTimeStage
+from authentik.stages.authenticator_totp.forms import SetupForm
+from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
LOGGER = get_logger()
SESSION_TOTP_DEVICE = "totp_device"
-class OTPTimeStageView(FormView, StageView):
+class AuthenticatorTOTPStageView(FormView, StageView):
"""OTP totp Setup stage"""
form_class = SetupForm
@@ -50,7 +50,7 @@ class OTPTimeStageView(FormView, StageView):
if TOTPDevice.objects.filter(user=user).exists():
return self.executor.stage_ok()
- stage: OTPTimeStage = self.executor.current_stage
+ stage: AuthenticatorTOTPStage = self.executor.current_stage
if SESSION_TOTP_DEVICE not in self.request.session:
device = TOTPDevice(user=user, confirmed=True, digits=stage.digits)
diff --git a/authentik/stages/otp_time/templates/stages/otp_time/user_settings.html b/authentik/stages/authenticator_totp/templates/stages/authenticator_totp/user_settings.html
similarity index 69%
rename from authentik/stages/otp_time/templates/stages/otp_time/user_settings.html
rename to authentik/stages/authenticator_totp/templates/stages/authenticator_totp/user_settings.html
index c6e24146e..54bc16e14 100644
--- a/authentik/stages/otp_time/templates/stages/otp_time/user_settings.html
+++ b/authentik/stages/authenticator_totp/templates/stages/authenticator_totp/user_settings.html
@@ -18,10 +18,10 @@
{% if not state %}
{% if stage.configure_flow %}
- {% trans "Enable Time-based OTP" %}
+ {% trans "Enable Time-based OTP" %}
{% endif %}
{% else %}
- {% trans "Disable Time-based OTP" %}
+ {% trans "Disable Time-based OTP" %}
{% endif %}
diff --git a/authentik/stages/otp_time/urls.py b/authentik/stages/authenticator_totp/urls.py
similarity index 75%
rename from authentik/stages/otp_time/urls.py
rename to authentik/stages/authenticator_totp/urls.py
index 3570fc8d4..f5ba850e8 100644
--- a/authentik/stages/otp_time/urls.py
+++ b/authentik/stages/authenticator_totp/urls.py
@@ -1,7 +1,7 @@
"""OTP Time urls"""
from django.urls import path
-from authentik.stages.otp_time.views import DisableView, UserSettingsView
+from authentik.stages.authenticator_totp.views import DisableView, UserSettingsView
urlpatterns = [
path(
diff --git a/authentik/stages/otp_time/views.py b/authentik/stages/authenticator_totp/views.py
similarity index 85%
rename from authentik/stages/otp_time/views.py
rename to authentik/stages/authenticator_totp/views.py
index f1322cadf..338350d71 100644
--- a/authentik/stages/otp_time/views.py
+++ b/authentik/stages/authenticator_totp/views.py
@@ -8,17 +8,17 @@ from django.views.generic import TemplateView
from django_otp.plugins.otp_totp.models import TOTPDevice
from authentik.events.models import Event
-from authentik.stages.otp_time.models import OTPTimeStage
+from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
class UserSettingsView(LoginRequiredMixin, TemplateView):
"""View for user settings to control OTP"""
- template_name = "stages/otp_time/user_settings.html"
+ template_name = "stages/authenticator_totp/user_settings.html"
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
- stage = get_object_or_404(OTPTimeStage, pk=self.kwargs["stage_uuid"])
+ stage = get_object_or_404(AuthenticatorTOTPStage, pk=self.kwargs["stage_uuid"])
kwargs["stage"] = stage
totp_devices = TOTPDevice.objects.filter(user=self.request.user, confirmed=True)
diff --git a/authentik/stages/otp_validate/__init__.py b/authentik/stages/authenticator_validate/__init__.py
similarity index 100%
rename from authentik/stages/otp_validate/__init__.py
rename to authentik/stages/authenticator_validate/__init__.py
diff --git a/authentik/stages/authenticator_validate/api.py b/authentik/stages/authenticator_validate/api.py
new file mode 100644
index 000000000..54bfa25b0
--- /dev/null
+++ b/authentik/stages/authenticator_validate/api.py
@@ -0,0 +1,24 @@
+"""AuthenticatorValidateStage API Views"""
+from rest_framework.viewsets import ModelViewSet
+
+from authentik.flows.api import StageSerializer
+from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
+
+
+class AuthenticatorValidateStageSerializer(StageSerializer):
+ """AuthenticatorValidateStage Serializer"""
+
+ class Meta:
+
+ model = AuthenticatorValidateStage
+ fields = StageSerializer.Meta.fields + [
+ "not_configured_action",
+ "device_classes",
+ ]
+
+
+class AuthenticatorValidateStageViewSet(ModelViewSet):
+ """AuthenticatorValidateStage Viewset"""
+
+ queryset = AuthenticatorValidateStage.objects.all()
+ serializer_class = AuthenticatorValidateStageSerializer
diff --git a/authentik/stages/authenticator_validate/apps.py b/authentik/stages/authenticator_validate/apps.py
new file mode 100644
index 000000000..e50715bf8
--- /dev/null
+++ b/authentik/stages/authenticator_validate/apps.py
@@ -0,0 +1,10 @@
+"""Authenticator Validation Stage"""
+from django.apps import AppConfig
+
+
+class AuthentikStageAuthenticatorValidateConfig(AppConfig):
+ """Authenticator Validation Stage"""
+
+ name = "authentik.stages.authenticator_validate"
+ label = "authentik_stages_authenticator_validate"
+ verbose_name = "authentik Stages.Authenticator.Validate"
diff --git a/authentik/stages/otp_validate/forms.py b/authentik/stages/authenticator_validate/forms.py
similarity index 76%
rename from authentik/stages/otp_validate/forms.py
rename to authentik/stages/authenticator_validate/forms.py
index a8d1f04b8..23e87fba0 100644
--- a/authentik/stages/otp_validate/forms.py
+++ b/authentik/stages/authenticator_validate/forms.py
@@ -4,7 +4,10 @@ from django.utils.translation import gettext_lazy as _
from django_otp import match_token
from authentik.core.models import User
-from authentik.stages.otp_validate.models import OTPValidateStage
+from authentik.stages.authenticator_validate.models import (
+ AuthenticatorValidateStage,
+ DeviceClasses,
+)
class ValidationForm(forms.Form):
@@ -36,14 +39,15 @@ class ValidationForm(forms.Form):
return code
-class OTPValidateStageForm(forms.ModelForm):
+class AuthenticatorValidateStageForm(forms.ModelForm):
"""OTP Validate stage forms"""
class Meta:
- model = OTPValidateStage
- fields = ["name"]
+ model = AuthenticatorValidateStage
+ fields = ["name", "device_classes"]
widgets = {
"name": forms.TextInput(),
+ "device_classes": forms.SelectMultiple(choices=DeviceClasses.choices),
}
diff --git a/authentik/stages/otp_validate/migrations/0001_initial.py b/authentik/stages/authenticator_validate/migrations/0001_initial.py
similarity index 100%
rename from authentik/stages/otp_validate/migrations/0001_initial.py
rename to authentik/stages/authenticator_validate/migrations/0001_initial.py
diff --git a/authentik/stages/authenticator_validate/migrations/0002_auto_20210216_0838.py b/authentik/stages/authenticator_validate/migrations/0002_auto_20210216_0838.py
new file mode 100644
index 000000000..c6a53fb25
--- /dev/null
+++ b/authentik/stages/authenticator_validate/migrations/0002_auto_20210216_0838.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.1.6 on 2021-02-16 08:38
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_flows", "0016_auto_20201202_1307"),
+ ("authentik_stages_authenticator_validate", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name="OTPValidateStage",
+ new_name="AuthenticatorValidateStage",
+ ),
+ migrations.AlterModelOptions(
+ name="authenticatorvalidatestage",
+ options={
+ "verbose_name": "Authenticator Validation Stage",
+ "verbose_name_plural": "Authenticator Validation Stages",
+ },
+ ),
+ ]
diff --git a/authentik/stages/authenticator_validate/migrations/0003_authenticatorvalidatestage_device_classes.py b/authentik/stages/authenticator_validate/migrations/0003_authenticatorvalidatestage_device_classes.py
new file mode 100644
index 000000000..8b865bbff
--- /dev/null
+++ b/authentik/stages/authenticator_validate/migrations/0003_authenticatorvalidatestage_device_classes.py
@@ -0,0 +1,26 @@
+# Generated by Django 3.1.6 on 2021-02-16 13:03
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+import authentik.stages.authenticator_validate.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_stages_authenticator_validate", "0002_auto_20210216_0838"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="authenticatorvalidatestage",
+ name="device_classes",
+ field=django.contrib.postgres.fields.ArrayField(
+ base_field=models.TextField(),
+ default=authentik.stages.authenticator_validate.models.default_device_classes,
+ help_text="Device classes which can be used to authenticate",
+ size=None,
+ ),
+ ),
+ ]
diff --git a/authentik/stages/otp_validate/migrations/__init__.py b/authentik/stages/authenticator_validate/migrations/__init__.py
similarity index 100%
rename from authentik/stages/otp_validate/migrations/__init__.py
rename to authentik/stages/authenticator_validate/migrations/__init__.py
diff --git a/authentik/stages/authenticator_validate/models.py b/authentik/stages/authenticator_validate/models.py
new file mode 100644
index 000000000..688cd0e1a
--- /dev/null
+++ b/authentik/stages/authenticator_validate/models.py
@@ -0,0 +1,74 @@
+"""Authenticator Validation Stage"""
+from typing import Type
+
+from django.contrib.postgres.fields.array import ArrayField
+from django.db import models
+from django.forms import ModelForm
+from django.utils.translation import gettext_lazy as _
+from django.views import View
+from rest_framework.serializers import BaseSerializer
+
+from authentik.flows.models import NotConfiguredAction, Stage
+
+
+class DeviceClasses(models.TextChoices):
+ """Device classes this stage can validate"""
+
+ STATIC = "static"
+ TOTP = "totp", _("TOTP")
+ WEBAUTHN = "webauthn", _("WebAuthn")
+
+
+def default_device_classes() -> list:
+ """By default, accept all device classes"""
+ return [
+ DeviceClasses.STATIC,
+ DeviceClasses.TOTP,
+ DeviceClasses.WEBAUTHN,
+ ]
+
+
+class AuthenticatorValidateStage(Stage):
+ """Validate user's configured OTP Device."""
+
+ not_configured_action = models.TextField(
+ choices=NotConfiguredAction.choices, default=NotConfiguredAction.SKIP
+ )
+
+ device_classes = ArrayField(
+ models.TextField(),
+ help_text=_("Device classes which can be used to authenticate"),
+ default=default_device_classes,
+ )
+
+ @property
+ def serializer(self) -> BaseSerializer:
+ from authentik.stages.authenticator_validate.api import (
+ AuthenticatorValidateStageSerializer,
+ )
+
+ return AuthenticatorValidateStageSerializer
+
+ @property
+ def type(self) -> Type[View]:
+ from authentik.stages.authenticator_validate.stage import (
+ AuthenticatorValidateStageView,
+ )
+
+ return AuthenticatorValidateStageView
+
+ @property
+ def form(self) -> Type[ModelForm]:
+ from authentik.stages.authenticator_validate.forms import (
+ AuthenticatorValidateStageForm,
+ )
+
+ return AuthenticatorValidateStageForm
+
+ def __str__(self) -> str:
+ return f"Authenticator Validation Stage {self.name}"
+
+ class Meta:
+
+ verbose_name = _("Authenticator Validation Stage")
+ verbose_name_plural = _("Authenticator Validation Stages")
diff --git a/authentik/stages/otp_validate/settings.py b/authentik/stages/authenticator_validate/settings.py
similarity index 100%
rename from authentik/stages/otp_validate/settings.py
rename to authentik/stages/authenticator_validate/settings.py
diff --git a/authentik/stages/otp_validate/stage.py b/authentik/stages/authenticator_validate/stage.py
similarity index 79%
rename from authentik/stages/otp_validate/stage.py
rename to authentik/stages/authenticator_validate/stage.py
index fae6086f4..04c3018b2 100644
--- a/authentik/stages/otp_validate/stage.py
+++ b/authentik/stages/authenticator_validate/stage.py
@@ -9,13 +9,13 @@ from structlog.stdlib import get_logger
from authentik.flows.models import NotConfiguredAction
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import StageView
-from authentik.stages.otp_validate.forms import ValidationForm
-from authentik.stages.otp_validate.models import OTPValidateStage
+from authentik.stages.authenticator_validate.forms import ValidationForm
+from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
LOGGER = get_logger()
-class OTPValidateStageView(FormView, StageView):
+class AuthenticatorValidateStageView(FormView, StageView):
"""OTP Validation"""
form_class = ValidationForm
@@ -31,11 +31,11 @@ class OTPValidateStageView(FormView, StageView):
LOGGER.debug("No pending user, continuing")
return self.executor.stage_ok()
has_devices = user_has_device(user)
- stage: OTPValidateStage = self.executor.current_stage
+ stage: AuthenticatorValidateStage = self.executor.current_stage
if not has_devices:
if stage.not_configured_action == NotConfiguredAction.SKIP:
- LOGGER.debug("OTP not configured, skipping stage")
+ LOGGER.debug("Authenticator not configured, skipping stage")
return self.executor.stage_ok()
return super().get(request, *args, **kwargs)
diff --git a/authentik/stages/authenticator_webauthn/__init__.py b/authentik/stages/authenticator_webauthn/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/authentik/stages/authenticator_webauthn/api.py b/authentik/stages/authenticator_webauthn/api.py
new file mode 100644
index 000000000..d3ecaed79
--- /dev/null
+++ b/authentik/stages/authenticator_webauthn/api.py
@@ -0,0 +1,21 @@
+"""AuthenticateWebAuthnStage API Views"""
+from rest_framework.viewsets import ModelViewSet
+
+from authentik.flows.api import StageSerializer
+from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage
+
+
+class AuthenticateWebAuthnStageSerializer(StageSerializer):
+ """AuthenticateWebAuthnStage Serializer"""
+
+ class Meta:
+
+ model = AuthenticateWebAuthnStage
+ fields = StageSerializer.Meta.fields
+
+
+class AuthenticateWebAuthnStageViewSet(ModelViewSet):
+ """AuthenticateWebAuthnStage Viewset"""
+
+ queryset = AuthenticateWebAuthnStage.objects.all()
+ serializer_class = AuthenticateWebAuthnStageSerializer
diff --git a/authentik/stages/authenticator_webauthn/apps.py b/authentik/stages/authenticator_webauthn/apps.py
new file mode 100644
index 000000000..ec8736525
--- /dev/null
+++ b/authentik/stages/authenticator_webauthn/apps.py
@@ -0,0 +1,11 @@
+"""authentik webauthn app config"""
+from django.apps import AppConfig
+
+
+class AuthentikStageAuthenticatorWebAuthnConfig(AppConfig):
+ """authentik webauthn config"""
+
+ name = "authentik.stages.authenticator_webauthn"
+ label = "authentik_stages_authenticator_webauthn"
+ verbose_name = "authentik Stages.Authenticator.WebAuthn"
+ mountpoint = "-/user/authenticator/webauthn/"
diff --git a/authentik/stages/authenticator_webauthn/forms.py b/authentik/stages/authenticator_webauthn/forms.py
new file mode 100644
index 000000000..78368c190
--- /dev/null
+++ b/authentik/stages/authenticator_webauthn/forms.py
@@ -0,0 +1,17 @@
+"""Webauthn stage forms"""
+from django import forms
+
+from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage
+
+
+class AuthenticateWebAuthnStageForm(forms.ModelForm):
+ """OTP Time-based Stage setup form"""
+
+ class Meta:
+
+ model = AuthenticateWebAuthnStage
+ fields = ["name"]
+
+ widgets = {
+ "name": forms.TextInput(),
+ }
diff --git a/authentik/stages/authenticator_webauthn/migrations/0001_initial.py b/authentik/stages/authenticator_webauthn/migrations/0001_initial.py
new file mode 100644
index 000000000..7d097bd6a
--- /dev/null
+++ b/authentik/stages/authenticator_webauthn/migrations/0001_initial.py
@@ -0,0 +1,81 @@
+# Generated by Django 3.1.6 on 2021-02-17 10:48
+
+import django.db.models.deletion
+import django.utils.timezone
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("authentik_flows", "0016_auto_20201202_1307"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="WebAuthnDevice",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.TextField(max_length=200)),
+ ("credential_id", models.CharField(max_length=300, unique=True)),
+ ("public_key", models.TextField()),
+ ("sign_count", models.IntegerField(default=0)),
+ ("rp_id", models.CharField(max_length=253)),
+ ("created_on", models.DateTimeField(auto_now_add=True)),
+ (
+ "last_used_on",
+ models.DateTimeField(default=django.utils.timezone.now),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="AuthenticateWebAuthnStage",
+ fields=[
+ (
+ "stage_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="authentik_flows.stage",
+ ),
+ ),
+ (
+ "configure_flow",
+ models.ForeignKey(
+ blank=True,
+ help_text="Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.",
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="authentik_flows.flow",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "WebAuthn Authenticator Setup Stage",
+ "verbose_name_plural": "WebAuthn Authenticator Setup Stages",
+ },
+ bases=("authentik_flows.stage", models.Model),
+ ),
+ ]
diff --git a/authentik/stages/otp_static/migrations/0003_default_setup_flow.py b/authentik/stages/authenticator_webauthn/migrations/0002_default_setup_flow.py
similarity index 63%
rename from authentik/stages/otp_static/migrations/0003_default_setup_flow.py
rename to authentik/stages/authenticator_webauthn/migrations/0002_default_setup_flow.py
index 4bf6dac47..6be74a728 100644
--- a/authentik/stages/otp_static/migrations/0003_default_setup_flow.py
+++ b/authentik/stages/authenticator_webauthn/migrations/0002_default_setup_flow.py
@@ -11,28 +11,32 @@ def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
Flow = apps.get_model("authentik_flows", "Flow")
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
- OTPStaticStage = apps.get_model("authentik_stages_otp_static", "OTPStaticStage")
+ AuthenticateWebAuthnStage = apps.get_model(
+ "authentik_stages_authenticator_webauthn", "AuthenticateWebAuthnStage"
+ )
db_alias = schema_editor.connection.alias
flow, _ = Flow.objects.using(db_alias).update_or_create(
- slug="default-otp-static-configure",
+ slug="default-authenticator-webuahtn-setup",
designation=FlowDesignation.STAGE_CONFIGURATION,
defaults={
- "name": "default-otp-static-configure",
+ "name": "default-authenticator-webuahtn-setup",
"title": "Setup Static OTP Tokens",
},
)
- stage, _ = OTPStaticStage.objects.using(db_alias).update_or_create(
- name="default-otp-static-configure", defaults={"token_count": 6}
+ stage, _ = AuthenticateWebAuthnStage.objects.using(db_alias).update_or_create(
+ name="default-authenticator-webuahtn-setup"
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow, stage=stage, defaults={"order": 0}
)
- for stage in OTPStaticStage.objects.using(db_alias).filter(configure_flow=None):
+ for stage in AuthenticateWebAuthnStage.objects.using(db_alias).filter(
+ configure_flow=None
+ ):
stage.configure_flow = flow
stage.save()
@@ -40,7 +44,10 @@ def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
class Migration(migrations.Migration):
dependencies = [
- ("authentik_stages_otp_static", "0002_otpstaticstage_configure_flow"),
+ (
+ "authentik_stages_authenticator_webauthn",
+ "0001_initial",
+ ),
]
operations = [
diff --git a/authentik/stages/authenticator_webauthn/migrations/__init__.py b/authentik/stages/authenticator_webauthn/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/authentik/stages/authenticator_webauthn/models.py b/authentik/stages/authenticator_webauthn/models.py
new file mode 100644
index 000000000..a3f5c409c
--- /dev/null
+++ b/authentik/stages/authenticator_webauthn/models.py
@@ -0,0 +1,80 @@
+"""WebAuthn stage"""
+from typing import Optional, Type
+
+from django.contrib.auth import get_user_model
+from django.db import models
+from django.forms import ModelForm
+from django.shortcuts import reverse
+from django.utils.timezone import now
+from django.utils.translation import gettext_lazy as _
+from django.views import View
+from rest_framework.serializers import BaseSerializer
+
+from authentik.flows.models import ConfigurableStage, Stage
+
+
+class AuthenticateWebAuthnStage(ConfigurableStage, Stage):
+ """WebAuthn stage"""
+
+ @property
+ def serializer(self) -> BaseSerializer:
+ from authentik.stages.authenticator_webauthn.api import (
+ AuthenticateWebAuthnStageSerializer,
+ )
+
+ return AuthenticateWebAuthnStageSerializer
+
+ @property
+ def type(self) -> Type[View]:
+ from authentik.stages.authenticator_webauthn.stage import (
+ AuthenticateWebAuthnStageView,
+ )
+
+ return AuthenticateWebAuthnStageView
+
+ @property
+ def form(self) -> Type[ModelForm]:
+ from authentik.stages.authenticator_webauthn.forms import (
+ AuthenticateWebAuthnStageForm,
+ )
+
+ return AuthenticateWebAuthnStageForm
+
+ @property
+ def ui_user_settings(self) -> Optional[str]:
+ return reverse(
+ "authentik_stages_authenticator_webauthn:user-settings",
+ kwargs={"stage_uuid": self.stage_uuid},
+ )
+
+ def __str__(self) -> str:
+ return f"WebAuthn Authenticator Setup Stage {self.name}"
+
+ class Meta:
+
+ verbose_name = _("WebAuthn Authenticator Setup Stage")
+ verbose_name_plural = _("WebAuthn Authenticator Setup Stages")
+
+
+class WebAuthnDevice(models.Model):
+ """WebAuthn Device for a single user"""
+
+ user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
+
+ name = models.TextField(max_length=200)
+ credential_id = models.CharField(max_length=300, unique=True)
+ public_key = models.TextField()
+ sign_count = models.IntegerField(default=0)
+ rp_id = models.CharField(max_length=253)
+
+ created_on = models.DateTimeField(auto_now_add=True)
+ last_used_on = models.DateTimeField(default=now)
+
+ def set_sign_count(self, sign_count: int) -> None:
+ """Set the sign_count and update the last_used_on datetime."""
+ self.sign_count = sign_count
+ self.last_used_on = now()
+ self.save()
+
+ def __str__(self):
+ return self.name or str(self.user)
diff --git a/authentik/stages/authenticator_webauthn/stage.py b/authentik/stages/authenticator_webauthn/stage.py
new file mode 100644
index 000000000..3116dc161
--- /dev/null
+++ b/authentik/stages/authenticator_webauthn/stage.py
@@ -0,0 +1,44 @@
+"""WebAuthn stage"""
+
+from django.http import HttpRequest, HttpResponse
+from django.shortcuts import render
+from django.views.generic import FormView
+from structlog.stdlib import get_logger
+
+from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
+from authentik.flows.stage import StageView
+from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
+
+LOGGER = get_logger()
+
+SESSION_KEY_WEBAUTHN_AUTHENTICATED = (
+ "authentik_stages_authenticator_webauthn_authenticated"
+)
+
+
+class AuthenticateWebAuthnStageView(FormView, StageView):
+ """WebAuthn stage"""
+
+ def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
+ user = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER)
+ if not user:
+ LOGGER.debug("No pending user, continuing")
+ return self.executor.stage_ok()
+ devices = WebAuthnDevice.objects.filter(user=user)
+ # If the current user is logged in already, or the pending user
+ # has no devices, show setup
+ if self.request.user == user:
+ # Because the user is already authenticated, skip the later check
+ self.request.session[SESSION_KEY_WEBAUTHN_AUTHENTICATED] = True
+ return render(request, "stages/authenticator_webauthn/setup.html")
+ if not devices.exists():
+ return self.executor.stage_ok()
+ self.request.session[SESSION_KEY_WEBAUTHN_AUTHENTICATED] = False
+ return render(request, "stages/authenticator_webauthn/auth.html")
+
+ def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
+ """Since the client can't directly indicate when a stage is done,
+ we use the post handler for this"""
+ if request.session.pop(SESSION_KEY_WEBAUTHN_AUTHENTICATED, False):
+ return self.executor.stage_ok()
+ return self.executor.stage_invalid()
diff --git a/authentik/stages/authenticator_webauthn/templates/stages/authenticator_webauthn/auth.html b/authentik/stages/authenticator_webauthn/templates/stages/authenticator_webauthn/auth.html
new file mode 100644
index 000000000..f35ee05ed
--- /dev/null
+++ b/authentik/stages/authenticator_webauthn/templates/stages/authenticator_webauthn/auth.html
@@ -0,0 +1,15 @@
+{% load i18n %}
+
+
+
+ {% trans 'WebAuthn' %}
+
+
+
+ {% block card %}
+
+ {% endblock %}
+
diff --git a/authentik/stages/authenticator_webauthn/templates/stages/authenticator_webauthn/setup.html b/authentik/stages/authenticator_webauthn/templates/stages/authenticator_webauthn/setup.html
new file mode 100644
index 000000000..6a47a71e9
--- /dev/null
+++ b/authentik/stages/authenticator_webauthn/templates/stages/authenticator_webauthn/setup.html
@@ -0,0 +1,16 @@
+{% load i18n %}
+
+
+
+ {% trans 'Configure WebAuthn' %}
+
+
+
+ {% block card %}
+
+ {% endblock %}
+
+
diff --git a/authentik/stages/authenticator_webauthn/templates/stages/authenticator_webauthn/user_settings.html b/authentik/stages/authenticator_webauthn/templates/stages/authenticator_webauthn/user_settings.html
new file mode 100644
index 000000000..89a26a8a3
--- /dev/null
+++ b/authentik/stages/authenticator_webauthn/templates/stages/authenticator_webauthn/user_settings.html
@@ -0,0 +1,33 @@
+{% load i18n %}
+{% load humanize %}
+
+
+
+
+
+ {% for device in devices %}
+ -
+
+
+ {{ device.name|default:"-" }}
+
+ {% blocktrans with created_on=device.created_on|naturaltime %}
+ Created {{ created_on }}
+ {% endblocktrans %}
+
+
+
+
+ {% endfor %}
+
+
+
+
diff --git a/authentik/stages/authenticator_webauthn/urls.py b/authentik/stages/authenticator_webauthn/urls.py
new file mode 100644
index 000000000..d2d7e5c91
--- /dev/null
+++ b/authentik/stages/authenticator_webauthn/urls.py
@@ -0,0 +1,38 @@
+"""WebAuthn urls"""
+from django.urls import path
+from django.views.decorators.csrf import csrf_exempt
+
+from authentik.stages.authenticator_webauthn.views import (
+ BeginActivateView,
+ BeginAssertion,
+ UserSettingsView,
+ VerifyAssertion,
+ VerifyCredentialInfo,
+)
+
+# TODO: Move to API views so we don't need csrf_exempt
+urlpatterns = [
+ path(
+ "begin-activate/",
+ csrf_exempt(BeginActivateView.as_view()),
+ name="activate-begin",
+ ),
+ path(
+ "begin-assertion/",
+ csrf_exempt(BeginAssertion.as_view()),
+ name="assertion-begin",
+ ),
+ path(
+ "verify-credential-info/",
+ csrf_exempt(VerifyCredentialInfo.as_view()),
+ name="credential-info-verify",
+ ),
+ path(
+ "verify-assertion/",
+ csrf_exempt(VerifyAssertion.as_view()),
+ name="assertion-verify",
+ ),
+ path(
+ "/settings/", UserSettingsView.as_view(), name="user-settings"
+ ),
+]
diff --git a/authentik/stages/authenticator_webauthn/utils.py b/authentik/stages/authenticator_webauthn/utils.py
new file mode 100644
index 000000000..217f685e4
--- /dev/null
+++ b/authentik/stages/authenticator_webauthn/utils.py
@@ -0,0 +1,23 @@
+"""webauthn utils"""
+import base64
+import os
+
+CHALLENGE_DEFAULT_BYTE_LEN = 32
+
+
+def generate_challenge(challenge_len=CHALLENGE_DEFAULT_BYTE_LEN):
+ """Generate a challenge of challenge_len bytes, Base64-encoded.
+ We use URL-safe base64, but we *don't* strip the padding, so that
+ the browser can decode it without too much hassle.
+ Note that if we are doing byte comparisons with the challenge in collectedClientData
+ later on, that value will not have padding, so we must remove the padding
+ before storing the value in the session.
+ """
+ # If we know Python 3.6 or greater is available, we could replace this with one
+ # call to secrets.token_urlsafe
+ challenge_bytes = os.urandom(challenge_len)
+ challenge_base64 = base64.urlsafe_b64encode(challenge_bytes)
+ # Python 2/3 compatibility: b64encode returns bytes only in newer Python versions
+ if not isinstance(challenge_base64, str):
+ challenge_base64 = challenge_base64.decode("utf-8")
+ return challenge_base64
diff --git a/authentik/stages/authenticator_webauthn/views.py b/authentik/stages/authenticator_webauthn/views.py
new file mode 100644
index 000000000..b9a4a4c57
--- /dev/null
+++ b/authentik/stages/authenticator_webauthn/views.py
@@ -0,0 +1,241 @@
+"""webauthn views"""
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.http import HttpRequest, HttpResponse, JsonResponse
+from django.http.response import HttpResponseBadRequest
+from django.shortcuts import get_object_or_404
+from django.views import View
+from django.views.generic import TemplateView
+from structlog.stdlib import get_logger
+from webauthn import (
+ WebAuthnAssertionOptions,
+ WebAuthnAssertionResponse,
+ WebAuthnMakeCredentialOptions,
+ WebAuthnRegistrationResponse,
+ WebAuthnUser,
+)
+from webauthn.webauthn import (
+ AuthenticationRejectedException,
+ RegistrationRejectedException,
+ WebAuthnUserDataMissing,
+)
+
+from authentik.core.models import User
+from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
+from authentik.flows.views import SESSION_KEY_PLAN
+from authentik.lib.templatetags.authentik_utils import avatar
+from authentik.stages.authenticator_webauthn.models import (
+ AuthenticateWebAuthnStage,
+ WebAuthnDevice,
+)
+from authentik.stages.authenticator_webauthn.stage import (
+ SESSION_KEY_WEBAUTHN_AUTHENTICATED,
+)
+from authentik.stages.authenticator_webauthn.utils import generate_challenge
+
+LOGGER = get_logger()
+RP_ID = "localhost"
+RP_NAME = "authentik"
+ORIGIN = "http://localhost:8000"
+
+
+class FlowUserRequiredView(View):
+ """Base class for views which can only be called in the context of a flow."""
+
+ user: User
+
+ def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
+ plan = request.session.get(SESSION_KEY_PLAN, None)
+ if not plan:
+ return HttpResponseBadRequest()
+ self.user = plan.context.get(PLAN_CONTEXT_PENDING_USER)
+ if not self.user:
+ return HttpResponseBadRequest()
+ return super().dispatch(request, *args, **kwargs)
+
+
+class BeginActivateView(FlowUserRequiredView):
+ """Initial device registration view"""
+
+ def post(self, request: HttpRequest) -> HttpResponse:
+ """Initial device registration view"""
+ # clear session variables prior to starting a new registration
+ request.session.pop("challenge", None)
+
+ challenge = generate_challenge(32)
+
+ # We strip the saved challenge of padding, so that we can do a byte
+ # comparison on the URL-safe-without-padding challenge we get back
+ # from the browser.
+ # We will still pass the padded version down to the browser so that the JS
+ # can decode the challenge into binary without too much trouble.
+ request.session["challenge"] = challenge.rstrip("=")
+
+ make_credential_options = WebAuthnMakeCredentialOptions(
+ challenge,
+ RP_NAME,
+ RP_ID,
+ self.user.uid,
+ self.user.username,
+ self.user.name,
+ avatar(self.user),
+ )
+
+ return JsonResponse(make_credential_options.registration_dict)
+
+
+class VerifyCredentialInfo(FlowUserRequiredView):
+ """Finish device registration"""
+
+ def post(self, request: HttpRequest) -> HttpResponse:
+ """Finish device registration"""
+ challenge = request.session["challenge"]
+
+ registration_response = request.POST
+ trusted_attestation_cert_required = True
+ self_attestation_permitted = True
+ none_attestation_permitted = True
+
+ webauthn_registration_response = WebAuthnRegistrationResponse(
+ RP_ID,
+ ORIGIN,
+ registration_response,
+ challenge,
+ trusted_attestation_cert_required=trusted_attestation_cert_required,
+ self_attestation_permitted=self_attestation_permitted,
+ none_attestation_permitted=none_attestation_permitted,
+ uv_required=False,
+ ) # User Verification
+
+ try:
+ webauthn_credential = webauthn_registration_response.verify()
+ except RegistrationRejectedException as exc:
+ LOGGER.warning("registration failed", exc=exc)
+ return JsonResponse({"fail": "Registration failed. Error: {}".format(exc)})
+
+ # Step 17.
+ #
+ # Check that the credentialId is not yet registered to any other user.
+ # If registration is requested for a credential that is already registered
+ # to a different user, the Relying Party SHOULD fail this registration
+ # ceremony, or it MAY decide to accept the registration, e.g. while deleting
+ # the older registration.
+ credential_id_exists = WebAuthnDevice.objects.filter(
+ credential_id=webauthn_credential.credential_id
+ ).first()
+ if credential_id_exists:
+ return JsonResponse({"fail": "Credential ID already exists."}, status=401)
+
+ webauthn_credential.credential_id = str(
+ webauthn_credential.credential_id, "utf-8"
+ )
+ webauthn_credential.public_key = str(webauthn_credential.public_key, "utf-8")
+ existing_device = WebAuthnDevice.objects.filter(
+ credential_id=webauthn_credential.credential_id
+ ).first()
+ if not existing_device:
+ user = WebAuthnDevice.objects.create(
+ user=self.user,
+ public_key=webauthn_credential.public_key,
+ credential_id=webauthn_credential.credential_id,
+ sign_count=webauthn_credential.sign_count,
+ rp_id=RP_ID,
+ )
+ else:
+ return JsonResponse({"fail": "User already exists."}, status=401)
+
+ LOGGER.debug("Successfully registered.", user=user)
+
+ return JsonResponse({"success": "User successfully registered."})
+
+
+class BeginAssertion(FlowUserRequiredView):
+ """Send the client a challenge that we'll check later"""
+
+ def post(self, request: HttpRequest) -> HttpResponse:
+ """Send the client a challenge that we'll check later"""
+ request.session.pop("challenge", None)
+
+ challenge = generate_challenge(32)
+
+ # We strip the padding from the challenge stored in the session
+ # for the reasons outlined in the comment in webauthn_begin_activate.
+ request.session["challenge"] = challenge.rstrip("=")
+
+ devices = WebAuthnDevice.objects.filter(user=self.user)
+ if not devices.exists():
+ return HttpResponseBadRequest()
+ device: WebAuthnDevice = devices.first()
+
+ webauthn_user = WebAuthnUser(
+ self.user.uid,
+ self.user.username,
+ self.user.name,
+ avatar(self.user),
+ device.credential_id,
+ device.public_key,
+ device.sign_count,
+ device.rp_id,
+ )
+
+ webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge)
+
+ return JsonResponse(webauthn_assertion_options.assertion_dict)
+
+
+class VerifyAssertion(FlowUserRequiredView):
+ """Verify assertion result that we've sent to the client"""
+
+ def post(self, request: HttpRequest) -> HttpResponse:
+ """Verify assertion result that we've sent to the client"""
+ challenge = request.session.get("challenge")
+ assertion_response = request.POST
+ credential_id = assertion_response.get("id")
+
+ device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
+ if not device:
+ return JsonResponse({"fail": "Device does not exist."}, status=401)
+
+ webauthn_user = WebAuthnUser(
+ self.user.uid,
+ self.user.username,
+ self.user.name,
+ avatar(self.user),
+ device.credential_id,
+ device.public_key,
+ device.sign_count,
+ device.rp_id,
+ )
+
+ webauthn_assertion_response = WebAuthnAssertionResponse(
+ webauthn_user, assertion_response, challenge, ORIGIN, uv_required=False
+ ) # User Verification
+
+ try:
+ sign_count = webauthn_assertion_response.verify()
+ except (
+ AuthenticationRejectedException,
+ WebAuthnUserDataMissing,
+ RegistrationRejectedException,
+ ) as exc:
+ return JsonResponse({"fail": "Assertion failed. Error: {}".format(exc)})
+
+ device.set_sign_count(sign_count)
+ request.session[SESSION_KEY_WEBAUTHN_AUTHENTICATED] = True
+ return JsonResponse(
+ {"success": "Successfully authenticated as {}".format(self.user.username)}
+ )
+
+
+class UserSettingsView(LoginRequiredMixin, TemplateView):
+ """View for user settings to control WebAuthn devices"""
+
+ template_name = "stages/authenticator_webauthn/user_settings.html"
+
+ def get_context_data(self, **kwargs):
+ kwargs = super().get_context_data(**kwargs)
+ kwargs["devices"] = WebAuthnDevice.objects.filter(user=self.request.user)
+ stage = get_object_or_404(
+ AuthenticateWebAuthnStage, pk=self.kwargs["stage_uuid"]
+ )
+ kwargs["stage"] = stage
+ return kwargs
diff --git a/authentik/stages/captcha/api.py b/authentik/stages/captcha/api.py
index f3389a78b..a60103666 100644
--- a/authentik/stages/captcha/api.py
+++ b/authentik/stages/captcha/api.py
@@ -1,17 +1,17 @@
"""CaptchaStage API Views"""
-from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
+from authentik.flows.api import StageSerializer
from authentik.stages.captcha.models import CaptchaStage
-class CaptchaStageSerializer(ModelSerializer):
+class CaptchaStageSerializer(StageSerializer):
"""CaptchaStage Serializer"""
class Meta:
model = CaptchaStage
- fields = ["pk", "name", "public_key", "private_key"]
+ fields = StageSerializer.Meta.fields + ["public_key", "private_key"]
class CaptchaStageViewSet(ModelViewSet):
diff --git a/authentik/stages/consent/api.py b/authentik/stages/consent/api.py
index 3c7f1415d..3e106c937 100644
--- a/authentik/stages/consent/api.py
+++ b/authentik/stages/consent/api.py
@@ -1,17 +1,17 @@
"""ConsentStage API Views"""
-from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
+from authentik.flows.api import StageSerializer
from authentik.stages.consent.models import ConsentStage
-class ConsentStageSerializer(ModelSerializer):
+class ConsentStageSerializer(StageSerializer):
"""ConsentStage Serializer"""
class Meta:
model = ConsentStage
- fields = ["pk", "name", "mode", "consent_expire_in"]
+ fields = StageSerializer.Meta.fields + ["mode", "consent_expire_in"]
class ConsentStageViewSet(ModelViewSet):
diff --git a/authentik/stages/dummy/api.py b/authentik/stages/dummy/api.py
index fa875a9fa..3b677bbac 100644
--- a/authentik/stages/dummy/api.py
+++ b/authentik/stages/dummy/api.py
@@ -1,17 +1,17 @@
"""DummyStage API Views"""
-from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
+from authentik.flows.api import StageSerializer
from authentik.stages.dummy.models import DummyStage
-class DummyStageSerializer(ModelSerializer):
+class DummyStageSerializer(StageSerializer):
"""DummyStage Serializer"""
class Meta:
model = DummyStage
- fields = ["pk", "name"]
+ fields = StageSerializer.Meta.fields
class DummyStageViewSet(ModelViewSet):
diff --git a/authentik/stages/email/api.py b/authentik/stages/email/api.py
index 01cdf3d2b..ffe5d6089 100644
--- a/authentik/stages/email/api.py
+++ b/authentik/stages/email/api.py
@@ -1,23 +1,21 @@
"""EmailStage API Views"""
-from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
+from authentik.flows.api import StageSerializer
from authentik.stages.email.models import EmailStage, get_template_choices
-class EmailStageSerializer(ModelSerializer):
+class EmailStageSerializer(StageSerializer):
"""EmailStage Serializer"""
- def __init__(self, *args, **kwrags):
- super().__init__(*args, **kwrags)
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
self.fields["template"].choices = get_template_choices()
class Meta:
model = EmailStage
- fields = [
- "pk",
- "name",
+ fields = StageSerializer.Meta.fields + [
"use_global_settings",
"host",
"port",
diff --git a/authentik/stages/identification/api.py b/authentik/stages/identification/api.py
index be08013fc..aba1629ec 100644
--- a/authentik/stages/identification/api.py
+++ b/authentik/stages/identification/api.py
@@ -1,19 +1,17 @@
"""Identification Stage API Views"""
-from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
+from authentik.flows.api import StageSerializer
from authentik.stages.identification.models import IdentificationStage
-class IdentificationStageSerializer(ModelSerializer):
+class IdentificationStageSerializer(StageSerializer):
"""IdentificationStage Serializer"""
class Meta:
model = IdentificationStage
- fields = [
- "pk",
- "name",
+ fields = StageSerializer.Meta.fields + [
"user_fields",
"case_insensitive_matching",
"show_matched_user",
diff --git a/authentik/stages/invitation/api.py b/authentik/stages/invitation/api.py
index 9a4402453..e8aadea19 100644
--- a/authentik/stages/invitation/api.py
+++ b/authentik/stages/invitation/api.py
@@ -2,18 +2,17 @@
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
+from authentik.flows.api import StageSerializer
from authentik.stages.invitation.models import Invitation, InvitationStage
-class InvitationStageSerializer(ModelSerializer):
+class InvitationStageSerializer(StageSerializer):
"""InvitationStage Serializer"""
class Meta:
model = InvitationStage
- fields = [
- "pk",
- "name",
+ fields = StageSerializer.Meta.fields + [
"continue_flow_without_invitation",
]
diff --git a/authentik/stages/otp_static/api.py b/authentik/stages/otp_static/api.py
deleted file mode 100644
index 120ab2f18..000000000
--- a/authentik/stages/otp_static/api.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""OTPStaticStage API Views"""
-from rest_framework.serializers import ModelSerializer
-from rest_framework.viewsets import ModelViewSet
-
-from authentik.stages.otp_static.models import OTPStaticStage
-
-
-class OTPStaticStageSerializer(ModelSerializer):
- """OTPStaticStage Serializer"""
-
- class Meta:
-
- model = OTPStaticStage
- fields = ["pk", "name", "configure_flow", "token_count"]
-
-
-class OTPStaticStageViewSet(ModelViewSet):
- """OTPStaticStage Viewset"""
-
- queryset = OTPStaticStage.objects.all()
- serializer_class = OTPStaticStageSerializer
diff --git a/authentik/stages/otp_static/apps.py b/authentik/stages/otp_static/apps.py
deleted file mode 100644
index 3dcb8dd63..000000000
--- a/authentik/stages/otp_static/apps.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""OTP Static stage"""
-from django.apps import AppConfig
-
-
-class AuthentikStageOTPStaticConfig(AppConfig):
- """OTP Static stage"""
-
- name = "authentik.stages.otp_static"
- label = "authentik_stages_otp_static"
- verbose_name = "authentik Stages.OTP.Static"
- mountpoint = "-/user/otp/static/"
diff --git a/authentik/stages/otp_static/models.py b/authentik/stages/otp_static/models.py
deleted file mode 100644
index c91ba317e..000000000
--- a/authentik/stages/otp_static/models.py
+++ /dev/null
@@ -1,50 +0,0 @@
-"""OTP Static models"""
-from typing import Optional, Type
-
-from django.db import models
-from django.forms import ModelForm
-from django.shortcuts import reverse
-from django.utils.translation import gettext_lazy as _
-from django.views import View
-from rest_framework.serializers import BaseSerializer
-
-from authentik.flows.models import ConfigurableStage, Stage
-
-
-class OTPStaticStage(ConfigurableStage, Stage):
- """Generate static tokens for the user as a backup."""
-
- token_count = models.IntegerField(default=6)
-
- @property
- def serializer(self) -> BaseSerializer:
- from authentik.stages.otp_static.api import OTPStaticStageSerializer
-
- return OTPStaticStageSerializer
-
- @property
- def type(self) -> Type[View]:
- from authentik.stages.otp_static.stage import OTPStaticStageView
-
- return OTPStaticStageView
-
- @property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.otp_static.forms import OTPStaticStageForm
-
- return OTPStaticStageForm
-
- @property
- def ui_user_settings(self) -> Optional[str]:
- return reverse(
- "authentik_stages_otp_static:user-settings",
- kwargs={"stage_uuid": self.stage_uuid},
- )
-
- def __str__(self) -> str:
- return f"OTP Static Stage {self.name}"
-
- class Meta:
-
- verbose_name = _("OTP Static Setup Stage")
- verbose_name_plural = _("OTP Static Setup Stages")
diff --git a/authentik/stages/otp_time/api.py b/authentik/stages/otp_time/api.py
deleted file mode 100644
index 7670477d7..000000000
--- a/authentik/stages/otp_time/api.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""OTPTimeStage API Views"""
-from rest_framework.serializers import ModelSerializer
-from rest_framework.viewsets import ModelViewSet
-
-from authentik.stages.otp_time.models import OTPTimeStage
-
-
-class OTPTimeStageSerializer(ModelSerializer):
- """OTPTimeStage Serializer"""
-
- class Meta:
-
- model = OTPTimeStage
- fields = ["pk", "name", "configure_flow", "digits"]
-
-
-class OTPTimeStageViewSet(ModelViewSet):
- """OTPTimeStage Viewset"""
-
- queryset = OTPTimeStage.objects.all()
- serializer_class = OTPTimeStageSerializer
diff --git a/authentik/stages/otp_time/apps.py b/authentik/stages/otp_time/apps.py
deleted file mode 100644
index a8e8cf362..000000000
--- a/authentik/stages/otp_time/apps.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""OTP Time"""
-from django.apps import AppConfig
-
-
-class AuthentikStageOTPTimeConfig(AppConfig):
- """OTP time App config"""
-
- name = "authentik.stages.otp_time"
- label = "authentik_stages_otp_time"
- verbose_name = "authentik Stages.OTP.Time"
- mountpoint = "-/user/otp/time/"
diff --git a/authentik/stages/otp_validate/api.py b/authentik/stages/otp_validate/api.py
deleted file mode 100644
index 6d9222939..000000000
--- a/authentik/stages/otp_validate/api.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""OTPValidateStage API Views"""
-from rest_framework.serializers import ModelSerializer
-from rest_framework.viewsets import ModelViewSet
-
-from authentik.stages.otp_validate.models import OTPValidateStage
-
-
-class OTPValidateStageSerializer(ModelSerializer):
- """OTPValidateStage Serializer"""
-
- class Meta:
-
- model = OTPValidateStage
- fields = [
- "pk",
- "name",
- ]
-
-
-class OTPValidateStageViewSet(ModelViewSet):
- """OTPValidateStage Viewset"""
-
- queryset = OTPValidateStage.objects.all()
- serializer_class = OTPValidateStageSerializer
diff --git a/authentik/stages/otp_validate/apps.py b/authentik/stages/otp_validate/apps.py
deleted file mode 100644
index 92b558cf0..000000000
--- a/authentik/stages/otp_validate/apps.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""OTP Validation Stage"""
-from django.apps import AppConfig
-
-
-class AuthentikStageOTPValidateConfig(AppConfig):
- """OTP Validation Stage"""
-
- name = "authentik.stages.otp_validate"
- label = "authentik_stages_otp_validate"
- verbose_name = "authentik Stages.OTP.Validate"
diff --git a/authentik/stages/otp_validate/models.py b/authentik/stages/otp_validate/models.py
deleted file mode 100644
index 33e58988f..000000000
--- a/authentik/stages/otp_validate/models.py
+++ /dev/null
@@ -1,44 +0,0 @@
-"""OTP Validation Stage"""
-from typing import Type
-
-from django.db import models
-from django.forms import ModelForm
-from django.utils.translation import gettext_lazy as _
-from django.views import View
-from rest_framework.serializers import BaseSerializer
-
-from authentik.flows.models import NotConfiguredAction, Stage
-
-
-class OTPValidateStage(Stage):
- """Validate user's configured OTP Device."""
-
- not_configured_action = models.TextField(
- choices=NotConfiguredAction.choices, default=NotConfiguredAction.SKIP
- )
-
- @property
- def serializer(self) -> BaseSerializer:
- from authentik.stages.otp_validate.api import OTPValidateStageSerializer
-
- return OTPValidateStageSerializer
-
- @property
- def type(self) -> Type[View]:
- from authentik.stages.otp_validate.stage import OTPValidateStageView
-
- return OTPValidateStageView
-
- @property
- def form(self) -> Type[ModelForm]:
- from authentik.stages.otp_validate.forms import OTPValidateStageForm
-
- return OTPValidateStageForm
-
- def __str__(self) -> str:
- return f"OTP Validation Stage {self.name}"
-
- class Meta:
-
- verbose_name = _("OTP Validation Stage")
- verbose_name_plural = _("OTP Validation Stages")
diff --git a/authentik/stages/password/api.py b/authentik/stages/password/api.py
index edce69f3e..e5ca7dc22 100644
--- a/authentik/stages/password/api.py
+++ b/authentik/stages/password/api.py
@@ -1,19 +1,17 @@
"""PasswordStage API Views"""
-from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
+from authentik.flows.api import StageSerializer
from authentik.stages.password.models import PasswordStage
-class PasswordStageSerializer(ModelSerializer):
+class PasswordStageSerializer(StageSerializer):
"""PasswordStage Serializer"""
class Meta:
model = PasswordStage
- fields = [
- "pk",
- "name",
+ fields = StageSerializer.Meta.fields + [
"backends",
"configure_flow",
"failed_attempts_before_cancel",
diff --git a/authentik/stages/prompt/api.py b/authentik/stages/prompt/api.py
index d5618c9b1..a04f021a8 100644
--- a/authentik/stages/prompt/api.py
+++ b/authentik/stages/prompt/api.py
@@ -3,10 +3,11 @@ from rest_framework.serializers import CharField, ModelSerializer
from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
+from authentik.flows.api import StageSerializer
from authentik.stages.prompt.models import Prompt, PromptStage
-class PromptStageSerializer(ModelSerializer):
+class PromptStageSerializer(StageSerializer):
"""PromptStage Serializer"""
name = CharField(validators=[UniqueValidator(queryset=PromptStage.objects.all())])
@@ -14,9 +15,7 @@ class PromptStageSerializer(ModelSerializer):
class Meta:
model = PromptStage
- fields = [
- "pk",
- "name",
+ fields = StageSerializer.Meta.fields + [
"fields",
"validation_policies",
]
diff --git a/authentik/stages/user_delete/api.py b/authentik/stages/user_delete/api.py
index ef283ae38..0f60f1479 100644
--- a/authentik/stages/user_delete/api.py
+++ b/authentik/stages/user_delete/api.py
@@ -1,20 +1,17 @@
"""User Delete Stage API Views"""
-from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
+from authentik.flows.api import StageSerializer
from authentik.stages.user_delete.models import UserDeleteStage
-class UserDeleteStageSerializer(ModelSerializer):
+class UserDeleteStageSerializer(StageSerializer):
"""UserDeleteStage Serializer"""
class Meta:
model = UserDeleteStage
- fields = [
- "pk",
- "name",
- ]
+ fields = StageSerializer.Meta.fields
class UserDeleteStageViewSet(ModelViewSet):
diff --git a/authentik/stages/user_login/api.py b/authentik/stages/user_login/api.py
index 43e29f105..406267fb0 100644
--- a/authentik/stages/user_login/api.py
+++ b/authentik/stages/user_login/api.py
@@ -1,19 +1,17 @@
"""Login Stage API Views"""
-from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
+from authentik.flows.api import StageSerializer
from authentik.stages.user_login.models import UserLoginStage
-class UserLoginStageSerializer(ModelSerializer):
+class UserLoginStageSerializer(StageSerializer):
"""UserLoginStage Serializer"""
class Meta:
model = UserLoginStage
- fields = [
- "pk",
- "name",
+ fields = StageSerializer.Meta.fields + [
"session_duration",
]
diff --git a/authentik/stages/user_logout/api.py b/authentik/stages/user_logout/api.py
index 8467b7a70..8d67c7d99 100644
--- a/authentik/stages/user_logout/api.py
+++ b/authentik/stages/user_logout/api.py
@@ -1,20 +1,17 @@
"""Logout Stage API Views"""
-from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
+from authentik.flows.api import StageSerializer
from authentik.stages.user_logout.models import UserLogoutStage
-class UserLogoutStageSerializer(ModelSerializer):
+class UserLogoutStageSerializer(StageSerializer):
"""UserLogoutStage Serializer"""
class Meta:
model = UserLogoutStage
- fields = [
- "pk",
- "name",
- ]
+ fields = StageSerializer.Meta.fields
class UserLogoutStageViewSet(ModelViewSet):
diff --git a/authentik/stages/user_write/api.py b/authentik/stages/user_write/api.py
index 0e2833e02..121285eb6 100644
--- a/authentik/stages/user_write/api.py
+++ b/authentik/stages/user_write/api.py
@@ -1,20 +1,17 @@
"""User Write Stage API Views"""
-from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
+from authentik.flows.api import StageSerializer
from authentik.stages.user_write.models import UserWriteStage
-class UserWriteStageSerializer(ModelSerializer):
+class UserWriteStageSerializer(StageSerializer):
"""UserWriteStage Serializer"""
class Meta:
model = UserWriteStage
- fields = [
- "pk",
- "name",
- ]
+ fields = StageSerializer.Meta.fields
class UserWriteStageViewSet(ModelViewSet):
diff --git a/lifecycle/system_migrations/to_2021_3_authenticator.py b/lifecycle/system_migrations/to_2021_3_authenticator.py
new file mode 100644
index 000000000..607d2e2c4
--- /dev/null
+++ b/lifecycle/system_migrations/to_2021_3_authenticator.py
@@ -0,0 +1,29 @@
+# flake8: noqa
+from lifecycle.migrate import BaseMigration
+
+SQL_STATEMENT = """BEGIN TRANSACTION;
+ALTER TABLE authentik_stages_otp_static_otpstaticstage RENAME TO authentik_stages_authenticator_static_otpstaticstage;
+UPDATE django_migrations SET app = replace(app, 'authentik_stages_otp_static', 'authentik_stages_authenticator_static');
+UPDATE django_content_type SET app_label = replace(app_label, 'authentik_stages_otp_static', 'authentik_stages_authenticator_static');
+
+ALTER TABLE authentik_stages_otp_time_otptimestage RENAME TO authentik_stages_authenticator_totp_otptimestage;
+UPDATE django_migrations SET app = replace(app, 'authentik_stages_otp_time', 'authentik_stages_authenticator_totp');
+UPDATE django_content_type SET app_label = replace(app_label, 'authentik_stages_otp_time', 'authentik_stages_authenticator_totp');
+
+ALTER TABLE authentik_stages_otp_validate_otpvalidatestage RENAME TO authentik_stages_authenticator_validate_otpvalidatestage;
+UPDATE django_migrations SET app = replace(app, 'authentik_stages_otp_validate', 'authentik_stages_authenticator_validate');
+UPDATE django_content_type SET app_label = replace(app_label, 'authentik_stages_otp_validate', 'authentik_stages_authenticator_validate');
+
+END TRANSACTION;"""
+
+
+class Migration(BaseMigration):
+ def needs_migration(self) -> bool:
+ self.cur.execute(
+ "select * from information_schema.tables where table_name = 'authentik_stages_otp_static_otpstaticstage';"
+ )
+ return bool(self.cur.rowcount)
+
+ def run(self):
+ self.cur.execute(SQL_STATEMENT)
+ self.con.commit()
diff --git a/swagger.yaml b/swagger.yaml
index ea37c08f0..e6b8005c1 100755
--- a/swagger.yaml
+++ b/swagger.yaml
@@ -5420,6 +5420,42 @@ paths:
tags:
- stages
parameters: []
+ /stages/all/types/:
+ get:
+ operationId: stages_all_types
+ description: Get all creatable stage types
+ parameters:
+ - name: ordering
+ in: query
+ description: Which field to use when ordering the results.
+ required: false
+ type: string
+ - name: search
+ in: query
+ description: A search term.
+ required: false
+ type: string
+ - name: page
+ in: query
+ description: A page number within the paginated result set.
+ required: false
+ type: integer
+ - name: page_size
+ in: query
+ description: Number of results to return per page.
+ required: false
+ type: integer
+ responses:
+ '200':
+ description: Types of an object that can be created
+ schema:
+ description: ''
+ type: array
+ items:
+ $ref: '#/definitions/TypeCreate'
+ tags:
+ - stages
+ parameters: []
/stages/all/{stage_uuid}/:
get:
operationId: stages_all_read
@@ -5439,6 +5475,514 @@ paths:
required: true
type: string
format: uuid
+ /stages/authenticator/static/:
+ get:
+ operationId: stages_authenticator_static_list
+ description: AuthenticatorStaticStage Viewset
+ parameters:
+ - name: ordering
+ in: query
+ description: Which field to use when ordering the results.
+ required: false
+ type: string
+ - name: search
+ in: query
+ description: A search term.
+ required: false
+ type: string
+ - name: page
+ in: query
+ description: A page number within the paginated result set.
+ required: false
+ type: integer
+ - name: page_size
+ in: query
+ description: Number of results to return per page.
+ required: false
+ type: integer
+ responses:
+ '200':
+ description: ''
+ schema:
+ required:
+ - count
+ - results
+ type: object
+ properties:
+ count:
+ type: integer
+ next:
+ type: string
+ format: uri
+ x-nullable: true
+ previous:
+ type: string
+ format: uri
+ x-nullable: true
+ results:
+ type: array
+ items:
+ $ref: '#/definitions/AuthenticatorStaticStage'
+ tags:
+ - stages
+ post:
+ operationId: stages_authenticator_static_create
+ description: AuthenticatorStaticStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/AuthenticatorStaticStage'
+ responses:
+ '201':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticatorStaticStage'
+ tags:
+ - stages
+ parameters: []
+ /stages/authenticator/static/{stage_uuid}/:
+ get:
+ operationId: stages_authenticator_static_read
+ description: AuthenticatorStaticStage Viewset
+ parameters: []
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticatorStaticStage'
+ tags:
+ - stages
+ put:
+ operationId: stages_authenticator_static_update
+ description: AuthenticatorStaticStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/AuthenticatorStaticStage'
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticatorStaticStage'
+ tags:
+ - stages
+ patch:
+ operationId: stages_authenticator_static_partial_update
+ description: AuthenticatorStaticStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/AuthenticatorStaticStage'
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticatorStaticStage'
+ tags:
+ - stages
+ delete:
+ operationId: stages_authenticator_static_delete
+ description: AuthenticatorStaticStage Viewset
+ parameters: []
+ responses:
+ '204':
+ description: ''
+ tags:
+ - stages
+ parameters:
+ - name: stage_uuid
+ in: path
+ description: A UUID string identifying this Static Authenticator Stage.
+ required: true
+ type: string
+ format: uuid
+ /stages/authenticator/totp/:
+ get:
+ operationId: stages_authenticator_totp_list
+ description: AuthenticatorTOTPStage Viewset
+ parameters:
+ - name: ordering
+ in: query
+ description: Which field to use when ordering the results.
+ required: false
+ type: string
+ - name: search
+ in: query
+ description: A search term.
+ required: false
+ type: string
+ - name: page
+ in: query
+ description: A page number within the paginated result set.
+ required: false
+ type: integer
+ - name: page_size
+ in: query
+ description: Number of results to return per page.
+ required: false
+ type: integer
+ responses:
+ '200':
+ description: ''
+ schema:
+ required:
+ - count
+ - results
+ type: object
+ properties:
+ count:
+ type: integer
+ next:
+ type: string
+ format: uri
+ x-nullable: true
+ previous:
+ type: string
+ format: uri
+ x-nullable: true
+ results:
+ type: array
+ items:
+ $ref: '#/definitions/AuthenticatorTOTPStage'
+ tags:
+ - stages
+ post:
+ operationId: stages_authenticator_totp_create
+ description: AuthenticatorTOTPStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/AuthenticatorTOTPStage'
+ responses:
+ '201':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticatorTOTPStage'
+ tags:
+ - stages
+ parameters: []
+ /stages/authenticator/totp/{stage_uuid}/:
+ get:
+ operationId: stages_authenticator_totp_read
+ description: AuthenticatorTOTPStage Viewset
+ parameters: []
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticatorTOTPStage'
+ tags:
+ - stages
+ put:
+ operationId: stages_authenticator_totp_update
+ description: AuthenticatorTOTPStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/AuthenticatorTOTPStage'
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticatorTOTPStage'
+ tags:
+ - stages
+ patch:
+ operationId: stages_authenticator_totp_partial_update
+ description: AuthenticatorTOTPStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/AuthenticatorTOTPStage'
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticatorTOTPStage'
+ tags:
+ - stages
+ delete:
+ operationId: stages_authenticator_totp_delete
+ description: AuthenticatorTOTPStage Viewset
+ parameters: []
+ responses:
+ '204':
+ description: ''
+ tags:
+ - stages
+ parameters:
+ - name: stage_uuid
+ in: path
+ description: A UUID string identifying this TOTP Authenticator Setup Stage.
+ required: true
+ type: string
+ format: uuid
+ /stages/authenticator/validate/:
+ get:
+ operationId: stages_authenticator_validate_list
+ description: AuthenticatorValidateStage Viewset
+ parameters:
+ - name: ordering
+ in: query
+ description: Which field to use when ordering the results.
+ required: false
+ type: string
+ - name: search
+ in: query
+ description: A search term.
+ required: false
+ type: string
+ - name: page
+ in: query
+ description: A page number within the paginated result set.
+ required: false
+ type: integer
+ - name: page_size
+ in: query
+ description: Number of results to return per page.
+ required: false
+ type: integer
+ responses:
+ '200':
+ description: ''
+ schema:
+ required:
+ - count
+ - results
+ type: object
+ properties:
+ count:
+ type: integer
+ next:
+ type: string
+ format: uri
+ x-nullable: true
+ previous:
+ type: string
+ format: uri
+ x-nullable: true
+ results:
+ type: array
+ items:
+ $ref: '#/definitions/AuthenticatorValidateStage'
+ tags:
+ - stages
+ post:
+ operationId: stages_authenticator_validate_create
+ description: AuthenticatorValidateStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/AuthenticatorValidateStage'
+ responses:
+ '201':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticatorValidateStage'
+ tags:
+ - stages
+ parameters: []
+ /stages/authenticator/validate/{stage_uuid}/:
+ get:
+ operationId: stages_authenticator_validate_read
+ description: AuthenticatorValidateStage Viewset
+ parameters: []
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticatorValidateStage'
+ tags:
+ - stages
+ put:
+ operationId: stages_authenticator_validate_update
+ description: AuthenticatorValidateStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/AuthenticatorValidateStage'
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticatorValidateStage'
+ tags:
+ - stages
+ patch:
+ operationId: stages_authenticator_validate_partial_update
+ description: AuthenticatorValidateStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/AuthenticatorValidateStage'
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticatorValidateStage'
+ tags:
+ - stages
+ delete:
+ operationId: stages_authenticator_validate_delete
+ description: AuthenticatorValidateStage Viewset
+ parameters: []
+ responses:
+ '204':
+ description: ''
+ tags:
+ - stages
+ parameters:
+ - name: stage_uuid
+ in: path
+ description: A UUID string identifying this Authenticator Validation Stage.
+ required: true
+ type: string
+ format: uuid
+ /stages/authenticator/webauthn/:
+ get:
+ operationId: stages_authenticator_webauthn_list
+ description: AuthenticateWebAuthnStage Viewset
+ parameters:
+ - name: ordering
+ in: query
+ description: Which field to use when ordering the results.
+ required: false
+ type: string
+ - name: search
+ in: query
+ description: A search term.
+ required: false
+ type: string
+ - name: page
+ in: query
+ description: A page number within the paginated result set.
+ required: false
+ type: integer
+ - name: page_size
+ in: query
+ description: Number of results to return per page.
+ required: false
+ type: integer
+ responses:
+ '200':
+ description: ''
+ schema:
+ required:
+ - count
+ - results
+ type: object
+ properties:
+ count:
+ type: integer
+ next:
+ type: string
+ format: uri
+ x-nullable: true
+ previous:
+ type: string
+ format: uri
+ x-nullable: true
+ results:
+ type: array
+ items:
+ $ref: '#/definitions/AuthenticateWebAuthnStage'
+ tags:
+ - stages
+ post:
+ operationId: stages_authenticator_webauthn_create
+ description: AuthenticateWebAuthnStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/AuthenticateWebAuthnStage'
+ responses:
+ '201':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticateWebAuthnStage'
+ tags:
+ - stages
+ parameters: []
+ /stages/authenticator/webauthn/{stage_uuid}/:
+ get:
+ operationId: stages_authenticator_webauthn_read
+ description: AuthenticateWebAuthnStage Viewset
+ parameters: []
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticateWebAuthnStage'
+ tags:
+ - stages
+ put:
+ operationId: stages_authenticator_webauthn_update
+ description: AuthenticateWebAuthnStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/AuthenticateWebAuthnStage'
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticateWebAuthnStage'
+ tags:
+ - stages
+ patch:
+ operationId: stages_authenticator_webauthn_partial_update
+ description: AuthenticateWebAuthnStage Viewset
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/AuthenticateWebAuthnStage'
+ responses:
+ '200':
+ description: ''
+ schema:
+ $ref: '#/definitions/AuthenticateWebAuthnStage'
+ tags:
+ - stages
+ delete:
+ operationId: stages_authenticator_webauthn_delete
+ description: AuthenticateWebAuthnStage Viewset
+ parameters: []
+ responses:
+ '204':
+ description: ''
+ tags:
+ - stages
+ parameters:
+ - name: stage_uuid
+ in: path
+ description: A UUID string identifying this WebAuthn Authenticator Setup Stage.
+ required: true
+ type: string
+ format: uuid
/stages/captcha/:
get:
operationId: stages_captcha_list
@@ -6328,387 +6872,6 @@ paths:
required: true
type: string
format: uuid
- /stages/otp_static/:
- get:
- operationId: stages_otp_static_list
- description: OTPStaticStage Viewset
- parameters:
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: A page number within the paginated result set.
- required: false
- type: integer
- - name: page_size
- in: query
- description: Number of results to return per page.
- required: false
- type: integer
- responses:
- '200':
- description: ''
- schema:
- required:
- - count
- - results
- type: object
- properties:
- count:
- type: integer
- next:
- type: string
- format: uri
- x-nullable: true
- previous:
- type: string
- format: uri
- x-nullable: true
- results:
- type: array
- items:
- $ref: '#/definitions/OTPStaticStage'
- tags:
- - stages
- post:
- operationId: stages_otp_static_create
- description: OTPStaticStage Viewset
- parameters:
- - name: data
- in: body
- required: true
- schema:
- $ref: '#/definitions/OTPStaticStage'
- responses:
- '201':
- description: ''
- schema:
- $ref: '#/definitions/OTPStaticStage'
- tags:
- - stages
- parameters: []
- /stages/otp_static/{stage_uuid}/:
- get:
- operationId: stages_otp_static_read
- description: OTPStaticStage Viewset
- parameters: []
- responses:
- '200':
- description: ''
- schema:
- $ref: '#/definitions/OTPStaticStage'
- tags:
- - stages
- put:
- operationId: stages_otp_static_update
- description: OTPStaticStage Viewset
- parameters:
- - name: data
- in: body
- required: true
- schema:
- $ref: '#/definitions/OTPStaticStage'
- responses:
- '200':
- description: ''
- schema:
- $ref: '#/definitions/OTPStaticStage'
- tags:
- - stages
- patch:
- operationId: stages_otp_static_partial_update
- description: OTPStaticStage Viewset
- parameters:
- - name: data
- in: body
- required: true
- schema:
- $ref: '#/definitions/OTPStaticStage'
- responses:
- '200':
- description: ''
- schema:
- $ref: '#/definitions/OTPStaticStage'
- tags:
- - stages
- delete:
- operationId: stages_otp_static_delete
- description: OTPStaticStage Viewset
- parameters: []
- responses:
- '204':
- description: ''
- tags:
- - stages
- parameters:
- - name: stage_uuid
- in: path
- description: A UUID string identifying this OTP Static Setup Stage.
- required: true
- type: string
- format: uuid
- /stages/otp_time/:
- get:
- operationId: stages_otp_time_list
- description: OTPTimeStage Viewset
- parameters:
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: A page number within the paginated result set.
- required: false
- type: integer
- - name: page_size
- in: query
- description: Number of results to return per page.
- required: false
- type: integer
- responses:
- '200':
- description: ''
- schema:
- required:
- - count
- - results
- type: object
- properties:
- count:
- type: integer
- next:
- type: string
- format: uri
- x-nullable: true
- previous:
- type: string
- format: uri
- x-nullable: true
- results:
- type: array
- items:
- $ref: '#/definitions/OTPTimeStage'
- tags:
- - stages
- post:
- operationId: stages_otp_time_create
- description: OTPTimeStage Viewset
- parameters:
- - name: data
- in: body
- required: true
- schema:
- $ref: '#/definitions/OTPTimeStage'
- responses:
- '201':
- description: ''
- schema:
- $ref: '#/definitions/OTPTimeStage'
- tags:
- - stages
- parameters: []
- /stages/otp_time/{stage_uuid}/:
- get:
- operationId: stages_otp_time_read
- description: OTPTimeStage Viewset
- parameters: []
- responses:
- '200':
- description: ''
- schema:
- $ref: '#/definitions/OTPTimeStage'
- tags:
- - stages
- put:
- operationId: stages_otp_time_update
- description: OTPTimeStage Viewset
- parameters:
- - name: data
- in: body
- required: true
- schema:
- $ref: '#/definitions/OTPTimeStage'
- responses:
- '200':
- description: ''
- schema:
- $ref: '#/definitions/OTPTimeStage'
- tags:
- - stages
- patch:
- operationId: stages_otp_time_partial_update
- description: OTPTimeStage Viewset
- parameters:
- - name: data
- in: body
- required: true
- schema:
- $ref: '#/definitions/OTPTimeStage'
- responses:
- '200':
- description: ''
- schema:
- $ref: '#/definitions/OTPTimeStage'
- tags:
- - stages
- delete:
- operationId: stages_otp_time_delete
- description: OTPTimeStage Viewset
- parameters: []
- responses:
- '204':
- description: ''
- tags:
- - stages
- parameters:
- - name: stage_uuid
- in: path
- description: A UUID string identifying this OTP Time (TOTP) Setup Stage.
- required: true
- type: string
- format: uuid
- /stages/otp_validate/:
- get:
- operationId: stages_otp_validate_list
- description: OTPValidateStage Viewset
- parameters:
- - name: ordering
- in: query
- description: Which field to use when ordering the results.
- required: false
- type: string
- - name: search
- in: query
- description: A search term.
- required: false
- type: string
- - name: page
- in: query
- description: A page number within the paginated result set.
- required: false
- type: integer
- - name: page_size
- in: query
- description: Number of results to return per page.
- required: false
- type: integer
- responses:
- '200':
- description: ''
- schema:
- required:
- - count
- - results
- type: object
- properties:
- count:
- type: integer
- next:
- type: string
- format: uri
- x-nullable: true
- previous:
- type: string
- format: uri
- x-nullable: true
- results:
- type: array
- items:
- $ref: '#/definitions/OTPValidateStage'
- tags:
- - stages
- post:
- operationId: stages_otp_validate_create
- description: OTPValidateStage Viewset
- parameters:
- - name: data
- in: body
- required: true
- schema:
- $ref: '#/definitions/OTPValidateStage'
- responses:
- '201':
- description: ''
- schema:
- $ref: '#/definitions/OTPValidateStage'
- tags:
- - stages
- parameters: []
- /stages/otp_validate/{stage_uuid}/:
- get:
- operationId: stages_otp_validate_read
- description: OTPValidateStage Viewset
- parameters: []
- responses:
- '200':
- description: ''
- schema:
- $ref: '#/definitions/OTPValidateStage'
- tags:
- - stages
- put:
- operationId: stages_otp_validate_update
- description: OTPValidateStage Viewset
- parameters:
- - name: data
- in: body
- required: true
- schema:
- $ref: '#/definitions/OTPValidateStage'
- responses:
- '200':
- description: ''
- schema:
- $ref: '#/definitions/OTPValidateStage'
- tags:
- - stages
- patch:
- operationId: stages_otp_validate_partial_update
- description: OTPValidateStage Viewset
- parameters:
- - name: data
- in: body
- required: true
- schema:
- $ref: '#/definitions/OTPValidateStage'
- responses:
- '200':
- description: ''
- schema:
- $ref: '#/definitions/OTPValidateStage'
- tags:
- - stages
- delete:
- operationId: stages_otp_validate_delete
- description: OTPValidateStage Viewset
- parameters: []
- responses:
- '204':
- description: ''
- tags:
- - stages
- parameters:
- - name: stage_uuid
- in: path
- description: A UUID string identifying this OTP Validation Stage.
- required: true
- type: string
- format: uuid
/stages/password/:
get:
operationId: stages_password_list
@@ -8191,14 +8354,18 @@ definitions:
title: Name
type: string
minLength: 1
- __type__:
- title: 'type '
+ object_type:
+ title: Object type
type: string
readOnly: true
verbose_name:
title: Verbose name
type: string
readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
FlowStageBinding:
description: FlowStageBinding Serializer
required:
@@ -9117,9 +9284,10 @@ definitions:
- authentik.stages.user_login
- authentik.stages.user_logout
- authentik.stages.user_write
- - authentik.stages.otp_static
- - authentik.stages.otp_time
- - authentik.stages.otp_validate
+ - authentik.stages.authenticator_static
+ - authentik.stages.authenticator_totp
+ - authentik.stages.authenticator_validate
+ - authentik.stages.authenticator_webauthn
- authentik.stages.password
- authentik.managed
- authentik.core
@@ -10316,6 +10484,152 @@ definitions:
\ log out manually. (Format: hours=1;minutes=2;seconds=3)."
type: string
minLength: 1
+ AuthenticatorStaticStage:
+ description: AuthenticatorStaticStage Serializer
+ required:
+ - name
+ type: object
+ properties:
+ pk:
+ title: Stage uuid
+ type: string
+ format: uuid
+ readOnly: true
+ name:
+ title: Name
+ type: string
+ minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
+ configure_flow:
+ title: Configure flow
+ description: Flow used by an authenticated user to configure this Stage. If
+ empty, user will not be able to configure this stage.
+ type: string
+ format: uuid
+ x-nullable: true
+ token_count:
+ title: Token count
+ type: integer
+ maximum: 2147483647
+ minimum: -2147483648
+ AuthenticatorTOTPStage:
+ description: AuthenticatorTOTPStage Serializer
+ required:
+ - name
+ - digits
+ type: object
+ properties:
+ pk:
+ title: Stage uuid
+ type: string
+ format: uuid
+ readOnly: true
+ name:
+ title: Name
+ type: string
+ minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
+ configure_flow:
+ title: Configure flow
+ description: Flow used by an authenticated user to configure this Stage. If
+ empty, user will not be able to configure this stage.
+ type: string
+ format: uuid
+ x-nullable: true
+ digits:
+ title: Digits
+ type: integer
+ enum:
+ - 6
+ - 8
+ AuthenticatorValidateStage:
+ description: AuthenticatorValidateStage Serializer
+ required:
+ - name
+ type: object
+ properties:
+ pk:
+ title: Stage uuid
+ type: string
+ format: uuid
+ readOnly: true
+ name:
+ title: Name
+ type: string
+ minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
+ not_configured_action:
+ title: Not configured action
+ type: string
+ enum:
+ - skip
+ device_classes:
+ description: ''
+ type: array
+ items:
+ title: Device classes
+ type: string
+ minLength: 1
+ AuthenticateWebAuthnStage:
+ description: AuthenticateWebAuthnStage Serializer
+ required:
+ - name
+ type: object
+ properties:
+ pk:
+ title: Stage uuid
+ type: string
+ format: uuid
+ readOnly: true
+ name:
+ title: Name
+ type: string
+ minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
CaptchaStage:
description: CaptchaStage Serializer
required:
@@ -10333,6 +10647,18 @@ definitions:
title: Name
type: string
minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
public_key:
title: Public key
description: Public key, acquired from https://www.google.com/recaptcha/intro/v3.html
@@ -10358,6 +10684,18 @@ definitions:
title: Name
type: string
minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
mode:
title: Mode
type: string
@@ -10385,6 +10723,18 @@ definitions:
title: Name
type: string
minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
EmailStage:
description: EmailStage Serializer
required:
@@ -10400,6 +10750,18 @@ definitions:
title: Name
type: string
minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
use_global_settings:
title: Use global settings
description: When enabled, global Email connection settings will be used and
@@ -10470,6 +10832,18 @@ definitions:
title: Name
type: string
minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
user_fields:
description: ''
type: array
@@ -10524,6 +10898,18 @@ definitions:
title: Name
type: string
minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
continue_flow_without_invitation:
title: Continue flow without invitation
description: If this flag is set, this Stage will jump to the next Stage when
@@ -10548,77 +10934,6 @@ definitions:
title: Fixed data
description: Optional fixed data to enforce on user enrollment.
type: object
- OTPStaticStage:
- description: OTPStaticStage Serializer
- required:
- - name
- type: object
- properties:
- pk:
- title: Stage uuid
- type: string
- format: uuid
- readOnly: true
- name:
- title: Name
- type: string
- minLength: 1
- configure_flow:
- title: Configure flow
- description: Flow used by an authenticated user to configure this Stage. If
- empty, user will not be able to configure this stage.
- type: string
- format: uuid
- x-nullable: true
- token_count:
- title: Token count
- type: integer
- maximum: 2147483647
- minimum: -2147483648
- OTPTimeStage:
- description: OTPTimeStage Serializer
- required:
- - name
- - digits
- type: object
- properties:
- pk:
- title: Stage uuid
- type: string
- format: uuid
- readOnly: true
- name:
- title: Name
- type: string
- minLength: 1
- configure_flow:
- title: Configure flow
- description: Flow used by an authenticated user to configure this Stage. If
- empty, user will not be able to configure this stage.
- type: string
- format: uuid
- x-nullable: true
- digits:
- title: Digits
- type: integer
- enum:
- - 6
- - 8
- OTPValidateStage:
- description: OTPValidateStage Serializer
- required:
- - name
- type: object
- properties:
- pk:
- title: Stage uuid
- type: string
- format: uuid
- readOnly: true
- name:
- title: Name
- type: string
- minLength: 1
PasswordStage:
description: PasswordStage Serializer
required:
@@ -10635,6 +10950,18 @@ definitions:
title: Name
type: string
minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
backends:
description: ''
type: array
@@ -10723,6 +11050,18 @@ definitions:
title: Name
type: string
minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
fields:
type: array
items:
@@ -10750,6 +11089,18 @@ definitions:
title: Name
type: string
minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
UserLoginStage:
description: UserLoginStage Serializer
required:
@@ -10765,6 +11116,18 @@ definitions:
title: Name
type: string
minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
session_duration:
title: Session duration
description: 'Determines how long a session lasts. Default of 0 means that
@@ -10786,6 +11149,18 @@ definitions:
title: Name
type: string
minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
UserWriteStage:
description: UserWriteStage Serializer
required:
@@ -10801,3 +11176,15 @@ definitions:
title: Name
type: string
minLength: 1
+ object_type:
+ title: Object type
+ type: string
+ readOnly: true
+ verbose_name:
+ title: Verbose name
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ title: Verbose name plural
+ type: string
+ readOnly: true
diff --git a/tests/e2e/test_flows_otp.py b/tests/e2e/test_flows_authenticators.py
similarity index 88%
rename from tests/e2e/test_flows_otp.py
rename to tests/e2e/test_flows_authenticators.py
index 113acc33e..632454932 100644
--- a/tests/e2e/test_flows_otp.py
+++ b/tests/e2e/test_flows_authenticators.py
@@ -13,18 +13,18 @@ from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec
from authentik.flows.models import Flow, FlowStageBinding
-from authentik.stages.otp_static.models import OTPStaticStage
-from authentik.stages.otp_time.models import OTPTimeStage
-from authentik.stages.otp_validate.models import OTPValidateStage
+from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
+from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
+from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
from tests.e2e.utils import USER, SeleniumTestCase, retry
@skipUnless(platform.startswith("linux"), "requires local docker")
-class TestFlowsOTP(SeleniumTestCase):
+class TestFlowsAuthenticator(SeleniumTestCase):
"""test flow with otp stages"""
@retry()
- def test_otp_validate(self):
+ def test_totp_validate(self):
"""test flow with otp stages"""
sleep(1)
# Setup TOTP Device
@@ -32,10 +32,8 @@ class TestFlowsOTP(SeleniumTestCase):
device = TOTPDevice.objects.create(user=user, confirmed=True, digits=6)
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
- # Move the user_login stage to order 3
- FlowStageBinding.objects.filter(target=flow, order=2).update(order=3)
FlowStageBinding.objects.create(
- target=flow, order=2, stage=OTPValidateStage.objects.create()
+ target=flow, order=30, stage=AuthenticatorValidateStage.objects.create()
)
self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
@@ -53,7 +51,7 @@ class TestFlowsOTP(SeleniumTestCase):
self.assert_user(USER())
@retry()
- def test_otp_totp_setup(self):
+ def test_totp_setup(self):
"""test TOTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
@@ -69,7 +67,7 @@ class TestFlowsOTP(SeleniumTestCase):
self.driver.get(
self.url(
"authentik_flows:configure",
- stage_uuid=OTPTimeStage.objects.first().stage_uuid,
+ stage_uuid=AuthenticatorTOTPStage.objects.first().stage_uuid,
)
)
@@ -96,7 +94,7 @@ class TestFlowsOTP(SeleniumTestCase):
self.assertTrue(TOTPDevice.objects.filter(user=USER(), confirmed=True).exists())
@retry()
- def test_otp_static_setup(self):
+ def test_static_setup(self):
"""test Static OTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
@@ -112,7 +110,7 @@ class TestFlowsOTP(SeleniumTestCase):
self.driver.get(
self.url(
"authentik_flows:configure",
- stage_uuid=OTPStaticStage.objects.first().stage_uuid,
+ stage_uuid=AuthenticatorStaticStage.objects.first().stage_uuid,
)
)
diff --git a/web/package-lock.json b/web/package-lock.json
index b985d8ff9..0b1c8e89d 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -725,6 +725,11 @@
}
}
},
+ "base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
+ },
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
diff --git a/web/package.json b/web/package.json
index be0abbcae..20edbb62e 100644
--- a/web/package.json
+++ b/web/package.json
@@ -16,6 +16,7 @@
"@sentry/tracing": "^6.1.0",
"@types/chart.js": "^2.9.30",
"@types/codemirror": "0.0.108",
+ "base64-js": "^1.5.1",
"chart.js": "^2.9.4",
"codemirror": "^5.59.2",
"construct-style-sheets-polyfill": "^2.4.16",
diff --git a/web/rollup.config.js b/web/rollup.config.js
index 2386494d8..3e366b3f1 100644
--- a/web/rollup.config.js
+++ b/web/rollup.config.js
@@ -1,6 +1,5 @@
import resolve from "rollup-plugin-node-resolve";
import commonjs from "rollup-plugin-commonjs";
-import minifyHTML from "rollup-plugin-minify-html-literals";
import { terser } from "rollup-plugin-terser";
import sourcemaps from "rollup-plugin-sourcemaps";
import typescript from "@rollup/plugin-typescript";
@@ -38,7 +37,6 @@ export default [
resolve({ browser: true }),
commonjs(),
sourcemaps(),
- minifyHTML(),
terser(),
copy({
targets: [...resources],
diff --git a/web/src/api/Client.ts b/web/src/api/Client.ts
index 581e94732..dfb74c42f 100644
--- a/web/src/api/Client.ts
+++ b/web/src/api/Client.ts
@@ -49,7 +49,7 @@ export class Client {
.then((r) => r);
}
- update(url: string[], body: T, query?: QueryArguments): Promise {
+ private writeRequest(url: string[], body: T, method: string, query?: QueryArguments): Promise {
const finalUrl = this.makeUrl(url, query);
const csrftoken = getCookie("authentik_csrf");
const request = new Request(finalUrl, {
@@ -57,10 +57,10 @@ export class Client {
"Accept": "application/json",
"Content-Type": "application/json",
"X-CSRFToken": csrftoken,
- },
+ },
});
return fetch(request, {
- method: "PATCH",
+ method: method,
mode: "same-origin",
body: JSON.stringify(body),
})
@@ -78,6 +78,10 @@ export class Client {
.then((r) => r.json())
.then((r) => r);
}
+
+ update(url: string[], body: T, query?: QueryArguments): Promise {
+ return this.writeRequest(url, body, "PATCH", query);
+ }
}
export const DefaultClient = new Client();
diff --git a/web/src/api/Flows.ts b/web/src/api/Flows.ts
index 0084f36a9..18f604e2b 100644
--- a/web/src/api/Flows.ts
+++ b/web/src/api/Flows.ts
@@ -1,4 +1,5 @@
import { DefaultClient, AKResponse, QueryArguments } from "./Client";
+import { TypeCreate } from "./Providers";
export enum FlowDesignation {
Authentication = "authentication",
@@ -57,6 +58,14 @@ export class Stage {
constructor() {
throw Error();
}
+
+ static getTypes(): Promise {
+ return DefaultClient.fetch(["stages", "all", "types"]);
+ }
+
+ static adminUrl(rest: string): string {
+ return `/administration/stages/${rest}`;
+ }
}
export class FlowStageBinding {
diff --git a/web/src/authentik.css b/web/src/authentik.css
index 5fda3c5dd..c052df2bf 100644
--- a/web/src/authentik.css
+++ b/web/src/authentik.css
@@ -250,4 +250,10 @@ select[multiple] {
.pf-c-notification-drawer__header {
background-color: var(--ak-dark-background-lighter);
}
+ /* data list */
+ .pf-c-data-list__item {
+ --pf-c-data-list__item--BackgroundColor: var(--ak-dark-background-light);
+ --pf-c-data-list__item--BorderBottomColor: var(--ak-dark-background-lighter);
+ color: var(--ak-dark-foreground);
+ }
}
diff --git a/web/src/elements/policies/BoundPoliciesList.ts b/web/src/elements/policies/BoundPoliciesList.ts
index 4b5798194..1e33101e8 100644
--- a/web/src/elements/policies/BoundPoliciesList.ts
+++ b/web/src/elements/policies/BoundPoliciesList.ts
@@ -56,7 +56,7 @@ export class BoundPoliciesList extends Table {
${gettext("Edit")}
-
+
${gettext("Delete")}
diff --git a/web/src/elements/stages/authenticator_webauthn/WebAuthnAuth.ts b/web/src/elements/stages/authenticator_webauthn/WebAuthnAuth.ts
new file mode 100644
index 000000000..ce3266947
--- /dev/null
+++ b/web/src/elements/stages/authenticator_webauthn/WebAuthnAuth.ts
@@ -0,0 +1,106 @@
+import { gettext } from "django";
+import { customElement, html, LitElement, property, TemplateResult } from "lit-element";
+import { SpinnerSize } from "../../Spinner";
+import { getCredentialRequestOptionsFromServer, postAssertionToServer, transformAssertionForServer, transformCredentialRequestOptions } from "./utils";
+
+@customElement("ak-stage-webauthn-auth")
+export class WebAuthnAuth extends LitElement {
+
+ @property({ type: Boolean })
+ authenticateRunning = false;
+
+ @property()
+ authenticateMessage = "";
+
+ async authenticate(): Promise {
+ // post the login data to the server to retrieve the PublicKeyCredentialRequestOptions
+ let credentialRequestOptionsFromServer;
+ try {
+ credentialRequestOptionsFromServer = await getCredentialRequestOptionsFromServer();
+ } catch (err) {
+ throw new Error(gettext(`Error when getting request options from server: ${err}`));
+ }
+
+ // convert certain members of the PublicKeyCredentialRequestOptions into
+ // byte arrays as expected by the spec.
+ const transformedCredentialRequestOptions = transformCredentialRequestOptions(
+ credentialRequestOptionsFromServer);
+
+ // request the authenticator to create an assertion signature using the
+ // credential private key
+ let assertion;
+ try {
+ assertion = await navigator.credentials.get({
+ publicKey: transformedCredentialRequestOptions,
+ });
+ if (!assertion) {
+ throw new Error(gettext("Assertions is empty"));
+ }
+ } catch (err) {
+ throw new Error(gettext(`Error when creating credential: ${err}`));
+ }
+
+ // we now have an authentication assertion! encode the byte arrays contained
+ // in the assertion data as strings for posting to the server
+ const transformedAssertionForServer = transformAssertionForServer(assertion);
+
+ // post the assertion to the server for verification.
+ try {
+ await postAssertionToServer(transformedAssertionForServer);
+ } catch (err) {
+ throw new Error(gettext(`Error when validating assertion on server: ${err}`));
+ }
+
+ this.finishStage();
+ }
+
+ finishStage(): void {
+ // Mark this stage as done
+ this.dispatchEvent(
+ new CustomEvent("ak-flow-submit", {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+
+ firstUpdated(): void {
+ this.authenticateWrapper();
+ }
+
+ async authenticateWrapper(): Promise {
+ if (this.authenticateRunning) {
+ return;
+ }
+ this.authenticateRunning = true;
+ this.authenticate().catch((e) => {
+ console.error(gettext(e));
+ this.authenticateMessage = e.toString();
+ }).finally(() => {
+ this.authenticateRunning = false;
+ });
+ }
+
+ render(): TemplateResult {
+ return html`
+ ${this.authenticateRunning ?
+ html` `:
+ html`
+ `}
+ `;
+ }
+
+}
diff --git a/web/src/elements/stages/authenticator_webauthn/WebAuthnRegister.ts b/web/src/elements/stages/authenticator_webauthn/WebAuthnRegister.ts
new file mode 100644
index 000000000..393bccdf2
--- /dev/null
+++ b/web/src/elements/stages/authenticator_webauthn/WebAuthnRegister.ts
@@ -0,0 +1,113 @@
+import { gettext } from "django";
+import { customElement, html, LitElement, property, TemplateResult } from "lit-element";
+import { SpinnerSize } from "../../Spinner";
+import { getCredentialCreateOptionsFromServer, postNewAssertionToServer, transformCredentialCreateOptions, transformNewAssertionForServer } from "./utils";
+
+@customElement("ak-stage-webauthn-register")
+export class WebAuthnRegister extends LitElement {
+
+ @property({type: Boolean})
+ registerRunning = false;
+
+ @property()
+ registerMessage = "";
+
+ createRenderRoot(): Element | ShadowRoot {
+ return this;
+ }
+
+ async register(): Promise {
+ // post the data to the server to generate the PublicKeyCredentialCreateOptions
+ let credentialCreateOptionsFromServer;
+ try {
+ credentialCreateOptionsFromServer = await getCredentialCreateOptionsFromServer();
+ } catch (err) {
+ throw new Error(gettext(`Failed to generate credential request options: ${err}`));
+ }
+
+ // convert certain members of the PublicKeyCredentialCreateOptions into
+ // byte arrays as expected by the spec.
+ const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(credentialCreateOptionsFromServer);
+
+ // request the authenticator(s) to create a new credential keypair.
+ let credential;
+ try {
+ credential = await navigator.credentials.create({
+ publicKey: publicKeyCredentialCreateOptions
+ });
+ if (!credential) {
+ throw new Error("Credential is empty");
+ }
+ } catch (err) {
+ throw new Error(gettext(`Error creating credential: ${err}`));
+ }
+
+ // we now have a new credential! We now need to encode the byte arrays
+ // in the credential into strings, for posting to our server.
+ const newAssertionForServer = transformNewAssertionForServer(credential);
+
+ // post the transformed credential data to the server for validation
+ // and storing the public key
+ try {
+ await postNewAssertionToServer(newAssertionForServer);
+ } catch (err) {
+ throw new Error(gettext(`Server validation of credential failed: ${err}`));
+ }
+ this.finishStage();
+ }
+
+ async registerWrapper(): Promise {
+ if (this.registerRunning) {
+ return;
+ }
+ this.registerRunning = true;
+ this.register().catch((e) => {
+ console.error(e);
+ this.registerMessage = e.toString();
+ }).finally(() => {
+ this.registerRunning = false;
+ });
+ }
+
+ finishStage(): void {
+ // Mark this stage as done
+ this.dispatchEvent(
+ new CustomEvent("ak-flow-submit", {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+
+ firstUpdated(): void {
+ this.registerWrapper();
+ }
+
+ render(): TemplateResult {
+ return html`
+ ${this.registerRunning ?
+ html` `:
+ html`
+ `}
+ `;
+ }
+
+}
diff --git a/web/src/elements/stages/authenticator_webauthn/utils.ts b/web/src/elements/stages/authenticator_webauthn/utils.ts
new file mode 100644
index 000000000..686f0e441
--- /dev/null
+++ b/web/src/elements/stages/authenticator_webauthn/utils.ts
@@ -0,0 +1,201 @@
+import * as base64js from "base64-js";
+
+export function b64enc(buf: Uint8Array): string {
+ return base64js.fromByteArray(buf)
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_")
+ .replace(/=/g, "");
+}
+
+export function b64RawEnc(buf: Uint8Array): string {
+ return base64js.fromByteArray(buf)
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_");
+}
+
+export function hexEncode(buf: Uint8Array): string {
+ return Array.from(buf)
+ .map(function (x) {
+ return ("0" + x.toString(16)).substr(-2);
+ })
+ .join("");
+}
+
+export interface GenericResponse {
+ fail?: string;
+ success?: string;
+ [key: string]: string | number | GenericResponse | undefined;
+}
+
+async function fetchJSON(url: string, options: RequestInit): Promise {
+ const response = await fetch(url, options);
+ const body = await response.json();
+ if (body.fail)
+ throw body.fail;
+ return body;
+}
+
+/**
+ * Transforms items in the credentialCreateOptions generated on the server
+ * into byte arrays expected by the navigator.credentials.create() call
+ */
+export function transformCredentialCreateOptions(credentialCreateOptions: PublicKeyCredentialCreationOptions): PublicKeyCredentialCreationOptions {
+ const user = credentialCreateOptions.user;
+ user.id = u8arr(credentialCreateOptions.user.id.toString());
+ const challenge = u8arr(credentialCreateOptions.challenge.toString());
+
+ const transformedCredentialCreateOptions = Object.assign(
+ {}, credentialCreateOptions,
+ { challenge, user });
+
+ return transformedCredentialCreateOptions;
+}
+
+export interface Assertion {
+ id: string;
+ rawId: string;
+ type: string;
+ attObj: string;
+ clientData: string;
+ registrationClientExtensions: string;
+}
+
+/**
+ * Transforms the binary data in the credential into base64 strings
+ * for posting to the server.
+ * @param {PublicKeyCredential} newAssertion
+ */
+export function transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
+ const attObj = new Uint8Array(
+ (newAssertion.response).attestationObject);
+ const clientDataJSON = new Uint8Array(
+ newAssertion.response.clientDataJSON);
+ const rawId = new Uint8Array(
+ newAssertion.rawId);
+
+ const registrationClientExtensions = newAssertion.getClientExtensionResults();
+ return {
+ id: newAssertion.id,
+ rawId: b64enc(rawId),
+ type: newAssertion.type,
+ attObj: b64enc(attObj),
+ clientData: b64enc(clientDataJSON),
+ registrationClientExtensions: JSON.stringify(registrationClientExtensions)
+ };
+}
+
+/**
+ * Post the assertion to the server for validation and logging the user in.
+ * @param {Object} assertionDataForServer
+ */
+export async function postNewAssertionToServer(assertionDataForServer: Assertion): Promise {
+ const formData = new FormData();
+ Object.entries(assertionDataForServer).forEach(([key, value]) => {
+ formData.set(key, value);
+ });
+
+ return await fetchJSON(
+ "/-/user/authenticator/webauthn/verify-credential-info/", {
+ method: "POST",
+ body: formData
+ });
+}
+
+/**
+ * Get PublicKeyCredentialRequestOptions for this user from the server
+ * formData of the registration form
+ * @param {FormData} formData
+ */
+export async function getCredentialCreateOptionsFromServer(): Promise {
+ return await fetchJSON(
+ "/-/user/authenticator/webauthn/begin-activate/",
+ {
+ method: "POST",
+ }
+ );
+}
+
+
+/**
+ * Get PublicKeyCredentialRequestOptions for this user from the server
+ * formData of the registration form
+ * @param {FormData} formData
+ */
+export async function getCredentialRequestOptionsFromServer(): Promise {
+ return await fetchJSON(
+ "/-/user/authenticator/webauthn/begin-assertion/",
+ {
+ method: "POST",
+ }
+ );
+}
+
+function u8arr(input: string): Uint8Array {
+ return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), c => c.charCodeAt(0));
+}
+
+export function transformCredentialRequestOptions(credentialRequestOptions: PublicKeyCredentialRequestOptions): PublicKeyCredentialRequestOptions {
+ const challenge = u8arr(credentialRequestOptions.challenge.toString());
+
+ const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(credentialDescriptor => {
+ const id = u8arr(credentialDescriptor.id.toString());
+ return Object.assign({}, credentialDescriptor, { id });
+ });
+
+ const transformedCredentialRequestOptions = Object.assign(
+ {},
+ credentialRequestOptions,
+ { challenge, allowCredentials });
+
+ return transformedCredentialRequestOptions;
+}
+
+export interface AuthAssertion {
+ id: string;
+ rawId: string;
+ type: string;
+ clientData: string;
+ authData: string;
+ signature: string;
+ assertionClientExtensions: string;
+}
+
+/**
+ * Encodes the binary data in the assertion into strings for posting to the server.
+ * @param {PublicKeyCredential} newAssertion
+ */
+export function transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion{
+ const response = newAssertion.response;
+ const authData = new Uint8Array(response.authenticatorData);
+ const clientDataJSON = new Uint8Array(response.clientDataJSON);
+ const rawId = new Uint8Array(newAssertion.rawId);
+ const sig = new Uint8Array(response.signature);
+ const assertionClientExtensions = newAssertion.getClientExtensionResults();
+
+ return {
+ id: newAssertion.id,
+ rawId: b64enc(rawId),
+ type: newAssertion.type,
+ authData: b64RawEnc(authData),
+ clientData: b64RawEnc(clientDataJSON),
+ signature: hexEncode(sig),
+ assertionClientExtensions: JSON.stringify(assertionClientExtensions)
+ };
+}
+
+/**
+ * Post the assertion to the server for validation and logging the user in.
+ * @param {Object} assertionDataForServer
+ */
+export async function postAssertionToServer(assertionDataForServer: Assertion): Promise {
+ const formData = new FormData();
+ Object.entries(assertionDataForServer).forEach(([key, value]) => {
+ formData.set(key, value);
+ });
+
+ return await fetchJSON(
+ "/-/user/authenticator/webauthn/verify-assertion/", {
+ method: "POST",
+ body: formData
+ });
+}
diff --git a/web/src/elements/table/Table.ts b/web/src/elements/table/Table.ts
index 38b0798a7..f8e59a146 100644
--- a/web/src/elements/table/Table.ts
+++ b/web/src/elements/table/Table.ts
@@ -194,11 +194,11 @@ export abstract class Table extends LitElement {
}
renderToolbar(): TemplateResult {
- return html` `;
+ `;
}
renderToolbarAfter(): TemplateResult {
@@ -216,10 +216,10 @@ export abstract class Table extends LitElement {
renderTable(): TemplateResult {
return html`
diff --git a/web/src/pages/sources/SourcesListPage.ts b/web/src/pages/sources/SourcesListPage.ts
index 7266e9e22..30511aa4a 100644
--- a/web/src/pages/sources/SourcesListPage.ts
+++ b/web/src/pages/sources/SourcesListPage.ts
@@ -57,7 +57,7 @@ export class SourceListPage extends TablePage
-
+
${gettext("Delete")}
diff --git a/website/docs/flow/stages/otp_static/index.md b/website/docs/flow/stages/authenticator_static/index.md
similarity index 83%
rename from website/docs/flow/stages/otp_static/index.md
rename to website/docs/flow/stages/authenticator_static/index.md
index 91a8eca8c..ee43781d6 100644
--- a/website/docs/flow/stages/otp_static/index.md
+++ b/website/docs/flow/stages/authenticator_static/index.md
@@ -1,5 +1,5 @@
---
-title: OTP Static stage
+title: Static Authenticator stage
---
This stage configures static OTP Tokens, which can be used as a backup method to time-based OTP tokens.
diff --git a/website/docs/flow/stages/otp_time/index.md b/website/docs/flow/stages/authenticator_totp/index.md
similarity index 88%
rename from website/docs/flow/stages/otp_time/index.md
rename to website/docs/flow/stages/authenticator_totp/index.md
index 105bcde60..6d4961144 100644
--- a/website/docs/flow/stages/otp_time/index.md
+++ b/website/docs/flow/stages/authenticator_totp/index.md
@@ -1,5 +1,5 @@
---
-title: OTP Time stage
+title: TOTP stage
---
This stage configures a time-based OTP Device, such as Google Authenticator or Authy.
diff --git a/website/docs/flow/stages/authenticator_validate/index.md b/website/docs/flow/stages/authenticator_validate/index.md
new file mode 100644
index 000000000..9dffa3dc1
--- /dev/null
+++ b/website/docs/flow/stages/authenticator_validate/index.md
@@ -0,0 +1,8 @@
+---
+title: Authenticator Validation Stage
+---
+
+This stage validates an already configured OTP Device. This device has to be configured using any of the other authenticator stages:
+
+- [TOTP authenticator stage](../authenticator_totp/index.md)
+- [Static authenticator stage](../authenticator_static/index.md).
diff --git a/website/docs/flow/stages/otp_validation/index.md b/website/docs/flow/stages/otp_validation/index.md
deleted file mode 100644
index 5e3af1b68..000000000
--- a/website/docs/flow/stages/otp_validation/index.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: OTP Validation stage
----
-
-This stage validates an already configured OTP Device. This device has to be configured using an [OTP Time stage](../otp_time/index.md) or [OTP Static stage](../otp_static/index.md).
diff --git a/website/sidebars.js b/website/sidebars.js
index 87fde5e28..2037f2be0 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -46,14 +46,14 @@ module.exports = {
type: "category",
label: "Stages",
items: [
+ "flow/stages/authenticator_static/index",
+ "flow/stages/authenticator_totp/index",
+ "flow/stages/authenticator_validate/index",
"flow/stages/captcha/index",
"flow/stages/dummy/index",
"flow/stages/email/index",
"flow/stages/identification/index",
"flow/stages/invitation/index",
- "flow/stages/otp_static/index",
- "flow/stages/otp_time/index",
- "flow/stages/otp_validation/index",
"flow/stages/password/index",
"flow/stages/prompt/index",
"flow/stages/prompt/validation",
diff --git a/website/static/flows/login-2fa.pbflow b/website/static/flows/login-2fa.pbflow
index 00b384cd1..ada439967 100644
--- a/website/static/flows/login-2fa.pbflow
+++ b/website/static/flows/login-2fa.pbflow
@@ -41,7 +41,7 @@
"pk": "37f709c3-8817-45e8-9a93-80a925d293c2",
"name": "default-authentication-flow-totp"
},
- "model": "authentik_stages_otp_validate.otpvalidatestage",
+ "model": "authentik_stages_authenticator_validate.AuthenticatorValidateStage",
"attrs": {}
},
{
|