diff --git a/Pipfile b/Pipfile
index 6ca7b5f3d..a6627d069 100644
--- a/Pipfile
+++ b/Pipfile
@@ -44,6 +44,7 @@ urllib3 = {extras = ["secure"],version = "*"}
uvicorn = {extras = ["standard"],version = "*"}
webauthn = "*"
xmlsec = "*"
+duo-client = "*"
[requires]
python_version = "3.9"
diff --git a/Pipfile.lock b/Pipfile.lock
index 6fd177258..226dd0c02 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "61354b75aa954ea0a995ee1909b861092a4be5c1af66d3c00c7c7845e056d064"
+ "sha256": "eb043e24ba05d5d78459a973fe0cd7c37dad1cca90431f68b6df773247c58cbb"
},
"pipfile-spec": 6,
"requires": {
@@ -56,6 +56,7 @@
"sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a",
"sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"
],
+ "markers": "python_version >= '3.6'",
"version": "==3.7.4.post0"
},
"aioredis": {
@@ -70,6 +71,7 @@
"sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2",
"sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"
],
+ "markers": "python_version >= '3.6'",
"version": "==5.0.6"
},
"asgiref": {
@@ -77,6 +79,7 @@
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
],
+ "markers": "python_version >= '3.6'",
"version": "==3.3.4"
},
"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:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.2.0"
},
"autobahn": {
@@ -98,6 +103,7 @@
"sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac",
"sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03"
],
+ "markers": "python_version >= '3.7'",
"version": "==21.3.1"
},
"automat": {
@@ -116,24 +122,26 @@
},
"boto3": {
"hashes": [
- "sha256:02835bcad77a5fda1fc376a824323779301ddf88f04a0ac16044d980f350c4a3",
- "sha256:b434170484348b870e3624069ca577d38e52ace0229d0619d8368454bb66ad3b"
+ "sha256:1a87855123df1f18081a5fb8c1abde28d0096a03f6f3ebb06bcfb77cdffdae5e",
+ "sha256:2a5caee63d45fbdcc85e710c7f4146112f5d10b22fd0176643d2f2914cce54df"
],
"index": "pypi",
- "version": "==1.17.77"
+ "version": "==1.17.78"
},
"botocore": {
"hashes": [
- "sha256:466ab5eac5e5735d573e83e84194585cac4e804d2b91b7bbe0351bcaff10df32",
- "sha256:b2a71043378687dc891997669830e8b61eaea656981059dbd4898825659df639"
+ "sha256:37105b9434d73f9c4d4960ee54c8eb129120f4c6681eb16edf483f03c5e2326d",
+ "sha256:e74775f9e64e975787d76390fc5ac5aba875d726bb9ece3b7bd900205b430389"
],
- "version": "==1.20.77"
+ "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.78"
},
"cachetools": {
"hashes": [
"sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001",
"sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"
],
+ "markers": "python_version ~= '3.5'",
"version": "==4.2.2"
},
"cbor2": {
@@ -152,15 +160,16 @@
"sha256:f0058d33b5eaffb176d6190d175a5391f13362f165881deea2b99e63b66ecf55",
"sha256:f5df0ad8c16f7992bf24e5c9a53f03a11a990fd18253c3c335315bd25a34f832"
],
+ "markers": "python_version >= '3.6'",
"version": "==5.3.0"
},
"celery": {
"hashes": [
- "sha256:5e8d364e058554e83bbb116e8377d90c79be254785f357cb2cec026e79febe13",
- "sha256:f4efebe6f8629b0da2b8e529424de376494f5b7a743c321c8a2ddc2b1414921c"
+ "sha256:1329de1edeaf734ef859e630cb42df2c116d53e59d2f46433b13aed196e85620",
+ "sha256:65f061c04578cf189cd7352c192e1a79fdeb370b916bff792bcc769560e81184"
],
"index": "pypi",
- "version": "==5.0.5"
+ "version": "==5.1.0"
},
"certifi": {
"hashes": [
@@ -244,6 +253,7 @@
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.0.0"
},
"click": {
@@ -251,6 +261,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": {
@@ -310,6 +321,7 @@
"sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f",
"sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"
],
+ "markers": "python_version >= '3.6'",
"version": "==3.0.2"
},
"defusedxml": {
@@ -420,6 +432,14 @@
"index": "pypi",
"version": "==0.16.0"
},
+ "duo-client": {
+ "hashes": [
+ "sha256:790de9573e2a0a85cc10cdb671b37759ee5f6c8557fe9972a8837bb07d71f69b",
+ "sha256:a5c9282cba3a02ae2ffbb16552e66379c19272664561baec86ad3a661f26ebf2"
+ ],
+ "index": "pypi",
+ "version": "==4.3.1"
+ },
"facebook-sdk": {
"hashes": [
"sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e",
@@ -432,6 +452,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": {
@@ -447,6 +468,7 @@
"sha256:588bdb03a41ecb4978472b847881e5518b5d9ec6153d3d679aa127a55e13b39f",
"sha256:9ad25fba07f46a628ad4d0ca09f38dcb262830df2ac95b217f9b0129c9e42206"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.30.0"
},
"gunicorn": {
@@ -462,6 +484,7 @@
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
],
+ "markers": "python_version >= '3.6'",
"version": "==0.12.0"
},
"hiredis": {
@@ -508,6 +531,7 @@
"sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0",
"sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"
],
+ "markers": "python_version >= '3.6'",
"version": "==2.0.0"
},
"httptools": {
@@ -556,6 +580,7 @@
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
],
+ "markers": "python_version >= '3.5'",
"version": "==0.5.1"
},
"jmespath": {
@@ -563,6 +588,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": {
@@ -574,10 +600,11 @@
},
"kombu": {
"hashes": [
- "sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006",
- "sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c"
+ "sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d",
+ "sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a"
],
- "version": "==5.0.2"
+ "markers": "python_version >= '3.6'",
+ "version": "==5.1.0"
},
"kubernetes": {
"hashes": [
@@ -589,8 +616,11 @@
},
"ldap3": {
"hashes": [
+ "sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57",
+ "sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c",
+ "sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59",
"sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91",
- "sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57"
+ "sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056"
],
"index": "pypi",
"version": "==2.9"
@@ -651,6 +681,7 @@
"hashes": [
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
],
+ "markers": "python_version >= '3.6'",
"version": "==2.0.3"
},
"msgpack": {
@@ -726,6 +757,7 @@
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
],
+ "markers": "python_version >= '3.6'",
"version": "==5.1.0"
},
"oauthlib": {
@@ -733,6 +765,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": {
@@ -748,6 +781,7 @@
"sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa",
"sha256:b6c5a9643e3545bcbfd9451766cbaa5d9c67e7303c7bc32c750b6fa70ecb107d"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.1"
},
"prompt-toolkit": {
@@ -755,6 +789,7 @@
"sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04",
"sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc"
],
+ "markers": "python_full_version >= '3.6.1'",
"version": "==3.0.18"
},
"psycopg2-binary": {
@@ -800,15 +835,37 @@
},
"pyasn1": {
"hashes": [
+ "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
+ "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
+ "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
+ "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
+ "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
+ "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3",
+ "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
+ "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
+ "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
+ "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
+ "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
- "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
+ "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"
],
"version": "==0.4.8"
},
"pyasn1-modules": {
"hashes": [
+ "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0",
+ "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
+ "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405",
+ "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74",
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
- "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"
+ "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d",
+ "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8",
+ "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb",
+ "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811",
+ "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199",
+ "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4",
+ "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed",
+ "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"
],
"version": "==0.2.8"
},
@@ -817,6 +874,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": {
@@ -860,6 +918,7 @@
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
],
+ "markers": "python_version >= '3.5'",
"version": "==2.0.2"
},
"pyjwt": {
@@ -882,12 +941,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": {
@@ -895,6 +956,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": {
@@ -951,6 +1013,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": {
@@ -958,12 +1021,14 @@
"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:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
- "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
+ "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
],
"index": "pypi",
"version": "==1.3.0"
@@ -1004,6 +1069,7 @@
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"sqlparse": {
@@ -1011,6 +1077,7 @@
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
],
+ "markers": "python_version >= '3.5'",
"version": "==0.4.1"
},
"structlog": {
@@ -1066,6 +1133,7 @@
"sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8",
"sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb"
],
+ "markers": "python_version >= '3.6'",
"version": "==21.2.1"
},
"typing-extensions": {
@@ -1081,6 +1149,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": {
@@ -1125,6 +1194,7 @@
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
],
+ "markers": "python_version >= '3.6'",
"version": "==5.0.0"
},
"watchgod": {
@@ -1151,10 +1221,11 @@
},
"websocket-client": {
"hashes": [
- "sha256:5051b38a2f4c27fbd7ca077ebb23ec6965a626ded5a95637f36be1b35b6c4f81",
- "sha256:57f876f1af4731cacb806cf54d02f5fbf75dee796053b9a5b94fd7c1d9621db9"
+ "sha256:3e2bf58191d4619b161389a95bdce84ce9e0b24eb8107e7e590db682c2d0ca81",
+ "sha256:abf306dc6351dcef07f4d40453037e51cc5d9da2ef60d0fc5d0fe3bcda255372"
],
- "version": "==1.0.0"
+ "markers": "python_version >= '3.6'",
+ "version": "==1.0.1"
},
"websockets": {
"hashes": [
@@ -1240,6 +1311,7 @@
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
],
+ "markers": "python_version >= '3.6'",
"version": "==1.6.3"
},
"zope.interface": {
@@ -1296,6 +1368,7 @@
"sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4",
"sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==5.4.0"
}
},
@@ -1312,6 +1385,7 @@
"sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e",
"sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"
],
+ "markers": "python_version ~= '3.6'",
"version": "==2.5.6"
},
"attrs": {
@@ -1319,6 +1393,7 @@
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.2.0"
},
"bandit": {
@@ -1357,6 +1432,7 @@
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.0.0"
},
"click": {
@@ -1364,6 +1440,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": {
@@ -1437,6 +1514,7 @@
"sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0",
"sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"
],
+ "markers": "python_version >= '3.4'",
"version": "==4.0.7"
},
"gitpython": {
@@ -1444,6 +1522,7 @@
"sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135",
"sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"
],
+ "markers": "python_version >= '3.5'",
"version": "==3.1.17"
},
"idna": {
@@ -1465,6 +1544,7 @@
"sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6",
"sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"
],
+ "markers": "python_version >= '3.6' and python_version < '4.0'",
"version": "==5.8.0"
},
"lazy-object-proxy": {
@@ -1492,6 +1572,7 @@
"sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93",
"sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.6.0"
},
"mccabe": {
@@ -1528,6 +1609,7 @@
"sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd",
"sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"
],
+ "markers": "python_version >= '2.6'",
"version": "==5.6.0"
},
"pluggy": {
@@ -1535,6 +1617,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"
},
"py": {
@@ -1542,6 +1625,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"
},
"pylint": {
@@ -1572,6 +1656,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": {
@@ -1676,6 +1761,7 @@
"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-mock": {
@@ -1699,6 +1785,7 @@
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"smmap": {
@@ -1706,6 +1793,7 @@
"sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182",
"sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"
],
+ "markers": "python_version >= '3.5'",
"version": "==4.0.0"
},
"stevedore": {
@@ -1713,6 +1801,7 @@
"sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee",
"sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"
],
+ "markers": "python_version >= '3.6'",
"version": "==3.3.0"
},
"toml": {
@@ -1720,6 +1809,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"
},
"urllib3": {
diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py
index 6c7971836..51fd9bbb8 100644
--- a/authentik/api/v2/urls.py
+++ b/authentik/api/v2/urls.py
@@ -64,6 +64,7 @@ from authentik.sources.oauth.api.source_connection import (
)
from authentik.sources.plex.api import PlexSourceViewSet
from authentik.sources.saml.api import SAMLSourceViewSet
+from authentik.stages.authenticator_duo.api import AuthenticatorDuoStageViewSet
from authentik.stages.authenticator_static.api import (
AuthenticatorStaticStageViewSet,
StaticAdminDeviceViewSet,
@@ -176,6 +177,7 @@ router.register(
)
router.register("stages/all", StageViewSet)
+router.register("stages/authenticator/duo", AuthenticatorDuoStageViewSet)
router.register("stages/authenticator/static", AuthenticatorStaticStageViewSet)
router.register("stages/authenticator/totp", AuthenticatorTOTPStageViewSet)
router.register("stages/authenticator/validate", AuthenticatorValidateStageViewSet)
diff --git a/authentik/root/settings.py b/authentik/root/settings.py
index 039e6bef6..57fe98c23 100644
--- a/authentik/root/settings.py
+++ b/authentik/root/settings.py
@@ -109,6 +109,7 @@ INSTALLED_APPS = [
"authentik.sources.oauth",
"authentik.sources.plex",
"authentik.sources.saml",
+ "authentik.stages.authenticator_duo",
"authentik.stages.authenticator_static",
"authentik.stages.authenticator_totp",
"authentik.stages.authenticator_validate",
diff --git a/authentik/stages/authenticator_duo/__init__.py b/authentik/stages/authenticator_duo/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/authentik/stages/authenticator_duo/api.py b/authentik/stages/authenticator_duo/api.py
new file mode 100644
index 000000000..8af02e185
--- /dev/null
+++ b/authentik/stages/authenticator_duo/api.py
@@ -0,0 +1,102 @@
+"""AuthenticatorDuoStage API Views"""
+from django_filters.rest_framework.backends import DjangoFilterBackend
+from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import OpenApiResponse, extend_schema
+from rest_framework import mixins
+from rest_framework.decorators import action
+from rest_framework.filters import OrderingFilter, SearchFilter
+from rest_framework.permissions import IsAdminUser
+from rest_framework.request import Request
+from rest_framework.response import Response
+from rest_framework.serializers import ModelSerializer
+from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
+
+from authentik.api.authorization import OwnerFilter, OwnerPermissions
+from authentik.flows.api.stages import StageSerializer
+from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
+from authentik.stages.authenticator_duo.stage import (
+ SESSION_KEY_DUO_ACTIVATION_CODE,
+ SESSION_KEY_DUO_USER_ID,
+)
+
+
+class AuthenticatorDuoStageSerializer(StageSerializer):
+ """AuthenticatorDuoStage Serializer"""
+
+ class Meta:
+
+ model = AuthenticatorDuoStage
+ fields = StageSerializer.Meta.fields + [
+ "configure_flow",
+ "client_id",
+ "client_secret",
+ "api_hostname",
+ ]
+ extra_kwargs = {
+ "client_secret": {"write_only": True},
+ }
+
+
+class AuthenticatorDuoStageViewSet(ModelViewSet):
+ """AuthenticatorDuoStage Viewset"""
+
+ queryset = AuthenticatorDuoStage.objects.all()
+ serializer_class = AuthenticatorDuoStageSerializer
+
+ @extend_schema(
+ request=OpenApiTypes.NONE,
+ responses={
+ 204: OpenApiResponse(description="Enrollment successful"),
+ 400: OpenApiResponse(description="Enrollment pending/failed"),
+ },
+ )
+ @action(methods=["POST"], detail=True, permission_classes=[])
+ def enrollment_status(self, request: Request, pk: str) -> Response:
+ stage: AuthenticatorDuoStage = self.get_object()
+ client = stage.client
+ user_id = self.request.session.get(SESSION_KEY_DUO_USER_ID)
+ activation_code = self.request.session.get(SESSION_KEY_DUO_ACTIVATION_CODE)
+ status = client.enroll_status(user_id, activation_code)
+ print(status)
+ if status["response"] == "success":
+ return Response(status=204)
+ return Response(status=400)
+
+
+class DuoDeviceSerializer(ModelSerializer):
+ """Serializer for Duo authenticator devices"""
+
+ class Meta:
+
+ model = DuoDevice
+ fields = ["pk", "name"]
+ depth = 2
+
+
+class DuoDeviceViewSet(
+ mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ mixins.DestroyModelMixin,
+ mixins.ListModelMixin,
+ GenericViewSet,
+):
+ """Viewset for Duo authenticator devices"""
+
+ queryset = DuoDevice.objects.all()
+ serializer_class = DuoDeviceSerializer
+ search_fields = ["name"]
+ filterset_fields = ["name"]
+ ordering = ["name"]
+ permission_classes = [OwnerPermissions]
+ filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
+
+
+class DuoAdminDeviceViewSet(ReadOnlyModelViewSet):
+ """Viewset for Duo authenticator devices (for admins)"""
+
+ permission_classes = [IsAdminUser]
+ queryset = DuoDevice.objects.all()
+ serializer_class = DuoDeviceSerializer
+ search_fields = ["name"]
+ filterset_fields = ["name"]
+ ordering = ["name"]
diff --git a/authentik/stages/authenticator_duo/apps.py b/authentik/stages/authenticator_duo/apps.py
new file mode 100644
index 000000000..a97979865
--- /dev/null
+++ b/authentik/stages/authenticator_duo/apps.py
@@ -0,0 +1,10 @@
+"""authentik duo app config"""
+from django.apps import AppConfig
+
+
+class AuthentikStageAuthenticatorDuoConfig(AppConfig):
+ """authentik duo config"""
+
+ name = "authentik.stages.authenticator_duo"
+ label = "authentik_stages_authenticator_duo"
+ verbose_name = "authentik Stages.Authenticator.Duo"
diff --git a/authentik/stages/authenticator_duo/migrations/0001_initial.py b/authentik/stages/authenticator_duo/migrations/0001_initial.py
new file mode 100644
index 000000000..eebee53ca
--- /dev/null
+++ b/authentik/stages/authenticator_duo/migrations/0001_initial.py
@@ -0,0 +1,91 @@
+# Generated by Django 3.2.3 on 2021-05-23 17:54
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ("authentik_flows", "0018_oob_flows"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="DuoDevice",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "name",
+ models.CharField(
+ help_text="The human-readable name of this device.",
+ max_length=64,
+ ),
+ ),
+ (
+ "confirmed",
+ models.BooleanField(
+ default=True, help_text="Is this device ready for use?"
+ ),
+ ),
+ ("duo_user_id", models.TextField()),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "Duo Device",
+ "verbose_name_plural": "Duo Devices",
+ },
+ ),
+ migrations.CreateModel(
+ name="AuthenticatorDuoStage",
+ 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",
+ ),
+ ),
+ ("client_id", models.TextField()),
+ ("client_secret", models.TextField()),
+ ("api_hostname", models.TextField()),
+ (
+ "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": "Duo Authenticator Setup Stage",
+ "verbose_name_plural": "Duo Authenticator Setup Stages",
+ },
+ bases=("authentik_flows.stage", models.Model),
+ ),
+ ]
diff --git a/authentik/stages/authenticator_duo/migrations/__init__.py b/authentik/stages/authenticator_duo/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/authentik/stages/authenticator_duo/models.py b/authentik/stages/authenticator_duo/models.py
new file mode 100644
index 000000000..baa7e6c84
--- /dev/null
+++ b/authentik/stages/authenticator_duo/models.py
@@ -0,0 +1,93 @@
+"""Duo stage"""
+from typing import Optional, Type
+
+from django.contrib.auth import get_user_model
+from django.db import models
+from django.utils.timezone import now
+from django.utils.translation import gettext_lazy as _
+from django.views import View
+from django_otp.models import Device
+from duo_client.auth import Auth
+from rest_framework.serializers import BaseSerializer
+
+from authentik import __version__
+from authentik.core.types import UserSettingSerializer
+from authentik.flows.models import ConfigurableStage, Stage
+
+
+class AuthenticatorDuoStage(ConfigurableStage, Stage):
+ """Duo stage"""
+
+ client_id = models.TextField()
+ client_secret = models.TextField()
+ api_hostname = models.TextField()
+
+ @property
+ def serializer(self) -> BaseSerializer:
+ from authentik.stages.authenticator_duo.api import (
+ AuthenticatorDuoStageSerializer,
+ )
+
+ return AuthenticatorDuoStageSerializer
+
+ @property
+ def type(self) -> Type[View]:
+ from authentik.stages.authenticator_duo.stage import AuthenticatorDuoStageView
+
+ return AuthenticatorDuoStageView
+
+ _client: Optional[Auth] = None
+
+ @property
+ def client(self) -> Auth:
+ if not self._client:
+ self._client = Auth(
+ self.client_id,
+ self.client_secret,
+ self.api_hostname,
+ user_agent=f"authentik {__version__}",
+ )
+ try:
+ self._client.ping()
+ except RuntimeError:
+ # Either allow login without 2FA, or abort the login process
+ # TODO: Define action when duo unavailable
+ raise
+ return self._client
+
+ @property
+ def component(self) -> str:
+ return "ak-stage-authenticator-duo-form"
+
+ @property
+ def ui_user_settings(self) -> Optional[UserSettingSerializer]:
+ return UserSettingSerializer(
+ data={
+ "title": str(self._meta.verbose_name),
+ "component": "ak-user-settings-authenticator-duo",
+ }
+ )
+
+ def __str__(self) -> str:
+ return f"Duo Authenticator Setup Stage {self.name}"
+
+ class Meta:
+
+ verbose_name = _("Duo Authenticator Setup Stage")
+ verbose_name_plural = _("Duo Authenticator Setup Stages")
+
+
+class DuoDevice(Device):
+ """Duo Device for a single user"""
+
+ user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
+
+ duo_user_id = models.TextField()
+
+ def __str__(self):
+ return self.name or str(self.user)
+
+ class Meta:
+
+ verbose_name = _("Duo Device")
+ verbose_name_plural = _("Duo Devices")
diff --git a/authentik/stages/authenticator_duo/stage.py b/authentik/stages/authenticator_duo/stage.py
new file mode 100644
index 000000000..b3719e62c
--- /dev/null
+++ b/authentik/stages/authenticator_duo/stage.py
@@ -0,0 +1,79 @@
+"""Duo stage"""
+from django.http import HttpRequest, HttpResponse
+from django.http.request import QueryDict
+from duo_client.auth import Auth
+from rest_framework.fields import CharField, JSONField
+from rest_framework.serializers import ValidationError
+from structlog.stdlib import get_logger
+
+from authentik.core.models import User
+from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
+from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
+from authentik.flows.stage import ChallengeStageView
+from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
+
+LOGGER = get_logger()
+
+SESSION_KEY_DUO_USER_ID = "authentik_stages_authenticator_duo_user_id"
+SESSION_KEY_DUO_ACTIVATION_CODE = "authentik_stages_authenticator_duo_activation_code"
+
+
+class AuthenticatorDuoChallenge(Challenge):
+ """Duo Challenge"""
+
+ activation_barcode = CharField()
+ activation_code = CharField()
+ stage_uuid = CharField()
+
+
+class AuthenticatorDuoStageView(ChallengeStageView):
+ """Duo stage"""
+
+ def get_challenge(self, *args, **kwargs) -> Challenge:
+ user = self.get_pending_user()
+ stage: AuthenticatorDuoStage = self.executor.current_stage
+ enroll = stage.client.enroll(user.username)
+ user_id = enroll["user_id"]
+ self.request.session[SESSION_KEY_DUO_USER_ID] = user_id
+ self.request.session[SESSION_KEY_DUO_ACTIVATION_CODE] = enroll[
+ "activation_code"
+ ]
+ return AuthenticatorDuoChallenge(
+ data={
+ "type": ChallengeTypes.NATIVE.value,
+ "component": "ak-stage-authenticator-duo",
+ "activation_barcode": enroll["activation_barcode"],
+ "activation_code": enroll["activation_code"],
+ "stage_uuid": stage.stage_uuid,
+ }
+ )
+
+ 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()
+ return super().get(request, *args, **kwargs)
+
+ def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
+ # Duo Challenge has already been validated
+ stage: AuthenticatorDuoStage = self.executor.current_stage
+ user_id = self.request.session.get(SESSION_KEY_DUO_USER_ID)
+ activation_code = self.request.session.get(SESSION_KEY_DUO_ACTIVATION_CODE)
+ enroll_status = stage.client.enroll_status(user_id, activation_code).get(
+ "response"
+ )
+ if enroll_status != "success":
+ # TODO: Find a better response
+ return HttpResponse(status=503)
+ existing_device = DuoDevice.objects.filter(duo_user_id=user_id).first()
+ if not existing_device:
+ DuoDevice.objects.create(
+ user=self.get_pending_user(),
+ duo_user_id=user_id,
+ )
+ else:
+ return self.executor.stage_invalid(
+ "Device with Credential ID already exists."
+ )
+ return self.executor.stage_ok()
diff --git a/schema.yml b/schema.yml
index a1446dc34..a6658f48a 100644
--- a/schema.yml
+++ b/schema.yml
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
- version: 2021.5.3
+ version: 2021.5.4
description: Making authentication simple.
contact:
email: hello@beryju.org
@@ -10759,6 +10759,234 @@ paths:
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
+ /api/v2beta/stages/authenticator/duo/:
+ get:
+ operationId: stages_authenticator_duo_list
+ description: AuthenticatorDuoStage Viewset
+ parameters:
+ - name: ordering
+ required: false
+ in: query
+ description: Which field to use when ordering the results.
+ schema:
+ type: string
+ - name: page
+ required: false
+ in: query
+ description: A page number within the paginated result set.
+ schema:
+ type: integer
+ - name: page_size
+ required: false
+ in: query
+ description: Number of results to return per page.
+ schema:
+ type: integer
+ - name: search
+ required: false
+ in: query
+ description: A search term.
+ schema:
+ type: string
+ tags:
+ - stages
+ security:
+ - authentik: []
+ - cookieAuth: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PaginatedAuthenticatorDuoStageList'
+ description: ''
+ '400':
+ $ref: '#/components/schemas/ValidationError'
+ '403':
+ $ref: '#/components/schemas/GenericError'
+ post:
+ operationId: stages_authenticator_duo_create
+ description: AuthenticatorDuoStage Viewset
+ tags:
+ - stages
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthenticatorDuoStageRequest'
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/AuthenticatorDuoStageRequest'
+ multipart/form-data:
+ schema:
+ $ref: '#/components/schemas/AuthenticatorDuoStageRequest'
+ required: true
+ security:
+ - authentik: []
+ - cookieAuth: []
+ responses:
+ '201':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthenticatorDuoStage'
+ description: ''
+ '400':
+ $ref: '#/components/schemas/ValidationError'
+ '403':
+ $ref: '#/components/schemas/GenericError'
+ /api/v2beta/stages/authenticator/duo/{stage_uuid}/:
+ get:
+ operationId: stages_authenticator_duo_retrieve
+ description: AuthenticatorDuoStage Viewset
+ parameters:
+ - in: path
+ name: stage_uuid
+ schema:
+ type: string
+ format: uuid
+ description: A UUID string identifying this Duo Authenticator Setup Stage.
+ required: true
+ tags:
+ - stages
+ security:
+ - authentik: []
+ - cookieAuth: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthenticatorDuoStage'
+ description: ''
+ '400':
+ $ref: '#/components/schemas/ValidationError'
+ '403':
+ $ref: '#/components/schemas/GenericError'
+ put:
+ operationId: stages_authenticator_duo_update
+ description: AuthenticatorDuoStage Viewset
+ parameters:
+ - in: path
+ name: stage_uuid
+ schema:
+ type: string
+ format: uuid
+ description: A UUID string identifying this Duo Authenticator Setup Stage.
+ required: true
+ tags:
+ - stages
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthenticatorDuoStageRequest'
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/AuthenticatorDuoStageRequest'
+ multipart/form-data:
+ schema:
+ $ref: '#/components/schemas/AuthenticatorDuoStageRequest'
+ required: true
+ security:
+ - authentik: []
+ - cookieAuth: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthenticatorDuoStage'
+ description: ''
+ '400':
+ $ref: '#/components/schemas/ValidationError'
+ '403':
+ $ref: '#/components/schemas/GenericError'
+ patch:
+ operationId: stages_authenticator_duo_partial_update
+ description: AuthenticatorDuoStage Viewset
+ parameters:
+ - in: path
+ name: stage_uuid
+ schema:
+ type: string
+ format: uuid
+ description: A UUID string identifying this Duo Authenticator Setup Stage.
+ required: true
+ tags:
+ - stages
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PatchedAuthenticatorDuoStageRequest'
+ application/x-www-form-urlencoded:
+ schema:
+ $ref: '#/components/schemas/PatchedAuthenticatorDuoStageRequest'
+ multipart/form-data:
+ schema:
+ $ref: '#/components/schemas/PatchedAuthenticatorDuoStageRequest'
+ security:
+ - authentik: []
+ - cookieAuth: []
+ responses:
+ '200':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthenticatorDuoStage'
+ description: ''
+ '400':
+ $ref: '#/components/schemas/ValidationError'
+ '403':
+ $ref: '#/components/schemas/GenericError'
+ delete:
+ operationId: stages_authenticator_duo_destroy
+ description: AuthenticatorDuoStage Viewset
+ parameters:
+ - in: path
+ name: stage_uuid
+ schema:
+ type: string
+ format: uuid
+ description: A UUID string identifying this Duo Authenticator Setup Stage.
+ required: true
+ tags:
+ - stages
+ security:
+ - authentik: []
+ - cookieAuth: []
+ responses:
+ '204':
+ description: No response body
+ '400':
+ $ref: '#/components/schemas/ValidationError'
+ '403':
+ $ref: '#/components/schemas/GenericError'
+ /api/v2beta/stages/authenticator/duo/{stage_uuid}/enrollment_status/:
+ post:
+ operationId: stages_authenticator_duo_enrollment_status_create
+ description: AuthenticatorDuoStage Viewset
+ parameters:
+ - in: path
+ name: stage_uuid
+ schema:
+ type: string
+ format: uuid
+ description: A UUID string identifying this Duo Authenticator Setup Stage.
+ required: true
+ tags:
+ - stages
+ security:
+ - authentik: []
+ - cookieAuth: []
+ responses:
+ '204':
+ description: Enrollment successful
+ '400':
+ description: Enrollment pending/failed
+ '403':
+ $ref: '#/components/schemas/GenericError'
/api/v2beta/stages/authenticator/static/:
get:
operationId: stages_authenticator_static_list
@@ -14757,6 +14985,7 @@ components:
- authentik.sources.oauth
- authentik.sources.plex
- authentik.sources.saml
+ - authentik.stages.authenticator_duo
- authentik.stages.authenticator_static
- authentik.stages.authenticator_totp
- authentik.stages.authenticator_validate
@@ -14907,6 +15136,76 @@ components:
If empty, user will not be able to configure this stage.
required:
- name
+ AuthenticatorDuoStage:
+ type: object
+ description: AuthenticatorDuoStage Serializer
+ properties:
+ pk:
+ type: string
+ format: uuid
+ readOnly: true
+ title: Stage uuid
+ name:
+ type: string
+ component:
+ type: string
+ readOnly: true
+ verbose_name:
+ type: string
+ readOnly: true
+ verbose_name_plural:
+ type: string
+ readOnly: true
+ flow_set:
+ type: array
+ items:
+ $ref: '#/components/schemas/Flow'
+ configure_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used by an authenticated user to configure this Stage.
+ If empty, user will not be able to configure this stage.
+ client_id:
+ type: string
+ api_hostname:
+ type: string
+ required:
+ - api_hostname
+ - client_id
+ - component
+ - name
+ - pk
+ - verbose_name
+ - verbose_name_plural
+ AuthenticatorDuoStageRequest:
+ type: object
+ description: AuthenticatorDuoStage Serializer
+ properties:
+ name:
+ type: string
+ flow_set:
+ type: array
+ items:
+ $ref: '#/components/schemas/FlowRequest'
+ configure_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used by an authenticated user to configure this Stage.
+ If empty, user will not be able to configure this stage.
+ client_id:
+ type: string
+ client_secret:
+ type: string
+ writeOnly: true
+ api_hostname:
+ type: string
+ required:
+ - api_hostname
+ - client_id
+ - client_secret
+ - name
AuthenticatorStaticStage:
type: object
description: AuthenticatorStaticStage Serializer
@@ -17728,6 +18027,41 @@ components:
required:
- pagination
- results
+ PaginatedAuthenticatorDuoStageList:
+ type: object
+ properties:
+ pagination:
+ type: object
+ properties:
+ next:
+ type: number
+ previous:
+ type: number
+ count:
+ type: number
+ current:
+ type: number
+ total_pages:
+ type: number
+ start_index:
+ type: number
+ end_index:
+ type: number
+ required:
+ - next
+ - previous
+ - count
+ - current
+ - total_pages
+ - start_index
+ - end_index
+ results:
+ type: array
+ items:
+ $ref: '#/components/schemas/AuthenticatorDuoStage'
+ required:
+ - pagination
+ - results
PaginatedAuthenticatorStaticStageList:
type: object
properties:
@@ -20315,6 +20649,29 @@ components:
nullable: true
description: Flow used by an authenticated user to configure this Stage.
If empty, user will not be able to configure this stage.
+ PatchedAuthenticatorDuoStageRequest:
+ type: object
+ description: AuthenticatorDuoStage Serializer
+ properties:
+ name:
+ type: string
+ flow_set:
+ type: array
+ items:
+ $ref: '#/components/schemas/FlowRequest'
+ configure_flow:
+ type: string
+ format: uuid
+ nullable: true
+ description: Flow used by an authenticated user to configure this Stage.
+ If empty, user will not be able to configure this stage.
+ client_id:
+ type: string
+ client_secret:
+ type: string
+ writeOnly: true
+ api_hostname:
+ type: string
PatchedAuthenticatorStaticStageRequest:
type: object
description: AuthenticatorStaticStage Serializer
diff --git a/web/src/flows/FlowExecutor.ts b/web/src/flows/FlowExecutor.ts
index 7b8d3785d..0cb048c6e 100644
--- a/web/src/flows/FlowExecutor.ts
+++ b/web/src/flows/FlowExecutor.ts
@@ -13,6 +13,7 @@ import { unsafeHTML } from "lit-html/directives/unsafe-html";
import "./access_denied/FlowAccessDenied";
import "./stages/authenticator_static/AuthenticatorStaticStage";
import "./stages/authenticator_totp/AuthenticatorTOTPStage";
+import "./stages/authenticator_duo/AuthenticatorDuoStage";
import "./stages/authenticator_validate/AuthenticatorValidateStage";
import "./stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage";
import "./stages/autosubmit/AutosubmitStage";
@@ -46,6 +47,7 @@ import { PFSize } from "../elements/Spinner";
import { TITLE_DEFAULT } from "../constants";
import { configureSentry } from "../api/Sentry";
import { PlexAuthenticationChallenge } from "./sources/plex/PlexLoginInit";
+import { AuthenticatorDuoChallenge } from "./stages/authenticator_duo/AuthenticatorDuoStage";
@customElement("ak-flow-executor")
export class FlowExecutor extends LitElement implements StageHost {
@@ -219,6 +221,8 @@ export class FlowExecutor extends LitElement implements StageHost {
return html`
+ ${this.challenge.title}
+
+
${t`Error: unsupported stage settings: ${stage.component}`}
`; } diff --git a/web/src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts b/web/src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts new file mode 100644 index 000000000..2648cd3ac --- /dev/null +++ b/web/src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts @@ -0,0 +1,79 @@ +import { AuthenticatorsApi } from "authentik-api"; +import { t } from "@lingui/macro"; +import { customElement, html, property, TemplateResult } from "lit-element"; +import { until } from "lit-html/directives/until"; +import { DEFAULT_CONFIG } from "../../../api/Config"; +import { FlowURLManager } from "../../../api/legacy"; +import { BaseUserSettings } from "./BaseUserSettings"; + +@customElement("ak-user-settings-authenticator-duo") +export class UserSettingsAuthenticatorDuo extends BaseUserSettings { + + @property({ type: Boolean }) + configureFlow = false; + + renderEnabled(): TemplateResult { + return html`+ ${t`Status: Enabled`} + +
++ ${t`Status: Disabled`} + +
+