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``; case "ak-stage-authenticator-totp": return html``; + case "ak-stage-authenticator-duo": + return html``; case "ak-stage-authenticator-static": return html``; case "ak-stage-authenticator-webauthn": diff --git a/web/src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts b/web/src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts new file mode 100644 index 000000000..b261d4705 --- /dev/null +++ b/web/src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts @@ -0,0 +1,90 @@ +import { t } from "@lingui/macro"; +import { CSSResult, customElement, html, property, TemplateResult } from "lit-element"; +import { WithUserInfoChallenge } from "../../../api/Flows"; +import PFLogin from "@patternfly/patternfly/components/Login/login.css"; +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; +import PFTitle from "@patternfly/patternfly/components/Title/title.css"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; +import AKGlobal from "../../../authentik.css"; +import { BaseStage } from "../base"; +import "../../../elements/forms/FormElement"; +import "../../../elements/EmptyState"; +import "../../FormStatic"; +import { FlowURLManager } from "../../../api/legacy"; +import { StagesApi } from "authentik-api"; +import { DEFAULT_CONFIG } from "../../../api/Config"; + +export interface AuthenticatorDuoChallenge extends WithUserInfoChallenge { + activation_barcode: string; + activation_code: string; + stage_uuid: string; +} + +@customElement("ak-stage-authenticator-duo") +export class AuthenticatorDuoStage extends BaseStage { + + @property({ attribute: false }) + challenge?: AuthenticatorDuoChallenge; + + static get styles(): CSSResult[] { + return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal]; + } + + firstUpdated(): void { + const i = setInterval(() => { + new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoEnrollmentStatusCreate({ + stageUuid: this.challenge?.stage_uuid || "", + }).then(r => { + console.log("success"); + clearInterval(i); + this.host?.submit(new FormData()); + }).catch(e => { + console.log("error"); + }); + }, 500); + } + + render(): TemplateResult { + if (!this.challenge) { + return html` + `; + } + return html`
+

+ ${this.challenge.title} +

+
+
+
{ this.submitForm(e); }}> + +
+ ${t`Not you?`} +
+
+ +

+ ${t`Alternatively, if your current device has Duo installed, click on this link:`} +

+ ${t`Duo activation`} + +
+ +
+
+
+ `; + } + +} diff --git a/web/src/pages/stages/StageListPage.ts b/web/src/pages/stages/StageListPage.ts index 47d3314b5..04104d26b 100644 --- a/web/src/pages/stages/StageListPage.ts +++ b/web/src/pages/stages/StageListPage.ts @@ -15,6 +15,7 @@ import { Stage, StagesApi } from "authentik-api"; import { DEFAULT_CONFIG } from "../../api/Config"; import { ifDefined } from "lit-html/directives/if-defined"; +import "./authenticator_duo/AuthenticatorDuoStageForm.ts"; import "./authenticator_static/AuthenticatorStaticStageForm.ts"; import "./authenticator_totp/AuthenticatorTOTPStageForm.ts"; import "./authenticator_validate/AuthenticatorValidateStageForm.ts"; diff --git a/web/src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts b/web/src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts new file mode 100644 index 000000000..a203cae89 --- /dev/null +++ b/web/src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts @@ -0,0 +1,105 @@ +import { FlowsApi, AuthenticatorDuoStage, StagesApi, FlowsInstancesListDesignationEnum, AuthenticatorDuoStageRequest } from "authentik-api"; +import { t } from "@lingui/macro"; +import { customElement } from "lit-element"; +import { html, TemplateResult } from "lit-html"; +import { DEFAULT_CONFIG } from "../../../api/Config"; +import { ifDefined } from "lit-html/directives/if-defined"; +import "../../../elements/forms/HorizontalFormElement"; +import "../../../elements/forms/FormGroup"; +import { until } from "lit-html/directives/until"; +import { first } from "../../../utils"; +import { ModelForm } from "../../../elements/forms/ModelForm"; + +@customElement("ak-stage-authenticator-duo-form") +export class AuthenticatorDuoStageForm extends ModelForm { + + loadInstance(pk: string): Promise { + return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoRetrieve({ + stageUuid: pk, + }); + } + + getSuccessMessage(): string { + if (this.instance) { + return t`Successfully updated stage.`; + } else { + return t`Successfully created stage.`; + } + } + + send = (data: AuthenticatorDuoStage): Promise => { + if (this.instance) { + return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoPartialUpdate({ + stageUuid: this.instance.pk || "", + patchedAuthenticatorDuoStageRequest: data + }); + } else { + return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoCreate({ + authenticatorDuoStageRequest: data as unknown as AuthenticatorDuoStageRequest + }); + } + }; + + renderForm(): TemplateResult { + return html`
+
+ ${t`Stage used to configure a duo-based authenticator. This stage should be used for configuration flows.`} +
+ + + + + + ${t`Stage-specific settings`} + +
+ + + + + + + + + + + +

+ ${t`Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.`} +

+
+
+
+
`; + } + +} diff --git a/web/src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts b/web/src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts index d88856b38..8bb3d8109 100644 --- a/web/src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts +++ b/web/src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts @@ -34,8 +34,8 @@ export class AuthenticatorStaticStageForm extends ModelForm `; + case "ak-user-settings-authenticator-duo": + return html` + `; default: return html`

${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`} + +

+
    + ${until(new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsStaticList({}).then((devices) => { + if (devices.results.length < 1) { + return; + } + return devices.results[0].tokenSet?.map((token) => { + return html`
  • ${token.token}
  • `; + }); + }))} +
+
+ `; + } + + renderDisabled(): TemplateResult { + return html` +
+

+ ${t`Status: Disabled`} + +

+
+ `; + } + + render(): TemplateResult { + return html`
+
+ ${t`Duo`} +
+ ${this.renderDisabled()} + ${until(new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsStaticList({}).then((devices) => { + return devices.results.length > 0 ? this.renderEnabled() : this.renderDisabled(); + }))} +
`; + } + +} diff --git a/web/src/pages/user-settings/settings/UserSettingsAuthenticatorStatic.ts b/web/src/pages/user-settings/settings/UserSettingsAuthenticatorStatic.ts index a8c0e7b95..44df27c5a 100644 --- a/web/src/pages/user-settings/settings/UserSettingsAuthenticatorStatic.ts +++ b/web/src/pages/user-settings/settings/UserSettingsAuthenticatorStatic.ts @@ -72,7 +72,7 @@ export class UserSettingsAuthenticatorStatic extends BaseUserSettings { render(): TemplateResult { return html`
- ${t`Time-based One-Time Passwords`} + ${t`Static tokens`}
${until(new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsStaticList({}).then((devices) => { return devices.results.length > 0 ? this.renderEnabled() : this.renderDisabled(); diff --git a/web/src/pages/user-settings/settings/UserSettingsAuthenticatorTOTP.ts b/web/src/pages/user-settings/settings/UserSettingsAuthenticatorTOTP.ts index ca58bf791..82343b16a 100644 --- a/web/src/pages/user-settings/settings/UserSettingsAuthenticatorTOTP.ts +++ b/web/src/pages/user-settings/settings/UserSettingsAuthenticatorTOTP.ts @@ -57,7 +57,7 @@ export class UserSettingsAuthenticatorTOTP extends BaseUserSettings { render(): TemplateResult { return html`
- ${t`Static tokens`} + ${t`Time-based One-Time Passwords`}
${until(new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsTotpList({}).then((devices) => { return devices.results.length > 0 ? this.renderEnabled() : this.renderDisabled();