stages: add WebAuthn stage (#550)
* core: add User.uid for globally unique user ID * admin: fix ?next for Flow list * stages: add initial webauthn implementation * web: add ak-flow-submit event to submit flow stage * web: show error message for webauthn registration * admin: fix next param not redirecting correctly * stages/webauthn: remove form * stages/webauthn: add API * web: update flow diagram on ak-refresh * stages/webauthn: add initial authentication * stages/webauthn: initial authentication implementation * web: cleanup webauthn utils * stages: rename otp_* to authenticator and move webauthn to authenticator * docs: fix broken links * stages/authenticator_*: fix template paths * stages/authenticator_validate: add device classes * stages/authenticator_webauthn: implement django_otp.devices * stages/authenticator_*: update default stage names * web: add button to create stage on flow page * web: don't minify HTML, remove nbsp * admin: fix typo in stage list * stages/*: use common base class for stage serializer * stages/authenticator_*: create default objects after rename * tests/e2e: adjust stage order
This commit is contained in:
parent
e020b8bf32
commit
8708e487ae
1
Pipfile
1
Pipfile
|
@ -45,6 +45,7 @@ kubernetes = "*"
|
|||
docker = "*"
|
||||
xmlsec = "*"
|
||||
geoip2 = "*"
|
||||
webauthn = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.9"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "7151710a45e6ca0bd25335b14be005aa5179eb91de361de93686022c9b71c3d1"
|
||||
"sha256": "933685b75680e3a06d2f523239d848b14d1507d385de42401863f4fb6345366c"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -56,6 +56,7 @@
|
|||
"sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245",
|
||||
"sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.7.3"
|
||||
},
|
||||
"aioredis": {
|
||||
|
@ -70,6 +71,7 @@
|
|||
"sha256:1e759a7f202d910939de6eca45c23a107f6b71111f41d1282c648e9ac3d21901",
|
||||
"sha256:affdd263d8b8eb3c98170b78bf83867cdb6a14901d586e00ddb65bfe2f0c4e60"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.0.5"
|
||||
},
|
||||
"asgiref": {
|
||||
|
@ -77,6 +79,7 @@
|
|||
"sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
|
||||
"sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.3.1"
|
||||
},
|
||||
"async-timeout": {
|
||||
|
@ -84,6 +87,7 @@
|
|||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||
],
|
||||
"markers": "python_full_version >= '3.5.3'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"attrs": {
|
||||
|
@ -91,6 +95,7 @@
|
|||
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
|
||||
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.3.0"
|
||||
},
|
||||
"autobahn": {
|
||||
|
@ -98,6 +103,7 @@
|
|||
"sha256:41a3a3f89cde48643baf4e105d9491c566295f9abee951379e59121784044b8b",
|
||||
"sha256:7e6b1bf95196b733978bab2d54a7ab8899c16ce11be369dc58422c07b7eea726"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==21.2.1"
|
||||
},
|
||||
"automat": {
|
||||
|
@ -116,7 +122,8 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:877f204dabe1bfa21aa9cfaacc72bd4b70a897d0fdcea799afa5c4743b6fc7ac"
|
||||
"sha256:877f204dabe1bfa21aa9cfaacc72bd4b70a897d0fdcea799afa5c4743b6fc7ac",
|
||||
"sha256:3a8412020a59509e783755b5c9b910a4fc7f6b6f2b9473e7cd1e07b67672e0d1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.17.9"
|
||||
|
@ -126,6 +133,7 @@
|
|||
"sha256:c8614c230e7a8e042a8c07d47caea50ad21cb51415289bd34fa6d0382beddad7",
|
||||
"sha256:d725840b881be62fc52e8e24a6ada651128cf7f1ed1639b87322a7a213ffdbad"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==1.20.9"
|
||||
},
|
||||
"cachetools": {
|
||||
|
@ -133,8 +141,15 @@
|
|||
"sha256:1d9d5f567be80f7c07d765e21b814326d78c61eb0c3a637dffc0e5d1796cb2e2",
|
||||
"sha256:f469e29e7aa4cff64d8de4aad95ce76de8ea1125a16c68e0d93f65c3c3dc92e9"
|
||||
],
|
||||
"markers": "python_version ~= '3.5'",
|
||||
"version": "==4.2.1"
|
||||
},
|
||||
"cbor2": {
|
||||
"hashes": [
|
||||
"sha256:a33aa2e5534fd74401ac95686886e655e3b2ce6383b3f958199b6e70a87c94bf"
|
||||
],
|
||||
"version": "==5.2.0"
|
||||
},
|
||||
"celery": {
|
||||
"hashes": [
|
||||
"sha256:5e8d364e058554e83bbb116e8377d90c79be254785f357cb2cec026e79febe13",
|
||||
|
@ -220,6 +235,7 @@
|
|||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"click-didyoumean": {
|
||||
|
@ -288,6 +304,7 @@
|
|||
"sha256:0052c9887600c57054a5867d4b0240159fa009faa3bcf6a1627271d9cdcb005a",
|
||||
"sha256:c22b692707f514de9013651ecb687f2abe4f35cf6fe292ece634e9f1737bc7e3"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"defusedxml": {
|
||||
|
@ -428,6 +445,7 @@
|
|||
"hashes": [
|
||||
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"geoip2": {
|
||||
|
@ -443,6 +461,7 @@
|
|||
"sha256:1b461d079b5650efe492a7814e95c536ffa9e7a96e39a6d16189c1604f18554f",
|
||||
"sha256:8ce6862cf4e9252de10045f05fa80393fde831da9c2b45c39288edeee3cde7f2"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==1.26.1"
|
||||
},
|
||||
"gunicorn": {
|
||||
|
@ -458,6 +477,7 @@
|
|||
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
|
||||
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.12.0"
|
||||
},
|
||||
"hiredis": {
|
||||
|
@ -509,6 +529,7 @@
|
|||
"sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390",
|
||||
"sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"httptools": {
|
||||
|
@ -554,6 +575,7 @@
|
|||
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
||||
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"itypes": {
|
||||
|
@ -568,6 +590,7 @@
|
|||
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419",
|
||||
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.11.3"
|
||||
},
|
||||
"jmespath": {
|
||||
|
@ -575,6 +598,7 @@
|
|||
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
|
||||
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"jsonschema": {
|
||||
|
@ -589,6 +613,7 @@
|
|||
"sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006",
|
||||
"sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.0.2"
|
||||
},
|
||||
"kubernetes": {
|
||||
|
@ -601,8 +626,11 @@
|
|||
},
|
||||
"ldap3": {
|
||||
"hashes": [
|
||||
"sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91",
|
||||
"sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57"
|
||||
"sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57",
|
||||
"sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056",
|
||||
"sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59",
|
||||
"sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c",
|
||||
"sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.9"
|
||||
|
@ -705,12 +733,14 @@
|
|||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
|
||||
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"maxminddb": {
|
||||
"hashes": [
|
||||
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.0.3"
|
||||
},
|
||||
"msgpack": {
|
||||
|
@ -786,6 +816,7 @@
|
|||
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
|
||||
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.1.0"
|
||||
},
|
||||
"oauthlib": {
|
||||
|
@ -793,6 +824,7 @@
|
|||
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
|
||||
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"packaging": {
|
||||
|
@ -815,6 +847,7 @@
|
|||
"sha256:0fa02fa80363844a4ab4b8d6891f62dd0645ba672723130423ca4037b80c1974",
|
||||
"sha256:62c811e46bd09130fb11ab759012a4ae385ce4fb2073442d1898867a824183bd"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==3.0.16"
|
||||
},
|
||||
"psycopg2-binary": {
|
||||
|
@ -860,15 +893,37 @@
|
|||
},
|
||||
"pyasn1": {
|
||||
"hashes": [
|
||||
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
|
||||
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
|
||||
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
|
||||
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
|
||||
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
|
||||
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
|
||||
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
|
||||
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
|
||||
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
|
||||
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
|
||||
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
|
||||
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
|
||||
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3",
|
||||
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"
|
||||
],
|
||||
"version": "==0.4.8"
|
||||
},
|
||||
"pyasn1-modules": {
|
||||
"hashes": [
|
||||
"sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
|
||||
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
|
||||
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"
|
||||
"sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199",
|
||||
"sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811",
|
||||
"sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0",
|
||||
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74",
|
||||
"sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d",
|
||||
"sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8",
|
||||
"sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd",
|
||||
"sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405",
|
||||
"sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb",
|
||||
"sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed",
|
||||
"sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"
|
||||
],
|
||||
"version": "==0.2.8"
|
||||
},
|
||||
|
@ -877,6 +932,7 @@
|
|||
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
|
||||
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.20"
|
||||
},
|
||||
"pycryptodome": {
|
||||
|
@ -948,6 +1004,7 @@
|
|||
"sha256:f933ecf4cb736c7af60a6a533db2bf569717f2318b265f92907acff1db43bc34",
|
||||
"sha256:fc9c55dc1ed57db76595f2d19a479fc1c3a1be2c9da8de798a93d286c5f65f38"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==3.10.1"
|
||||
},
|
||||
"pyhamcrest": {
|
||||
|
@ -955,6 +1012,7 @@
|
|||
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
|
||||
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.0.2"
|
||||
},
|
||||
"pyjwkest": {
|
||||
|
@ -976,12 +1034,14 @@
|
|||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.17.3"
|
||||
},
|
||||
"python-dateutil": {
|
||||
|
@ -989,6 +1049,7 @@
|
|||
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
|
||||
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.8.1"
|
||||
},
|
||||
"python-dotenv": {
|
||||
|
@ -1045,6 +1106,7 @@
|
|||
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
|
||||
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==3.5.3"
|
||||
},
|
||||
"requests": {
|
||||
|
@ -1052,11 +1114,13 @@
|
|||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.25.1"
|
||||
},
|
||||
"requests-oauthlib": {
|
||||
"hashes": [
|
||||
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
|
||||
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc",
|
||||
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
|
||||
],
|
||||
"index": "pypi",
|
||||
|
@ -1105,6 +1169,7 @@
|
|||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"sqlparse": {
|
||||
|
@ -1112,6 +1177,7 @@
|
|||
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
||||
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"structlog": {
|
||||
|
@ -1159,6 +1225,7 @@
|
|||
"sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467",
|
||||
"sha256:f3c19e5bd42bbe4bf345704ad7c326c74d3fd7a1b3844987853bef180be638d4"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==20.3.0"
|
||||
},
|
||||
"txaio": {
|
||||
|
@ -1166,6 +1233,7 @@
|
|||
"sha256:1488d31d564a116538cc1265ac3f7979fb6223bb5a9e9f1479436ee2c17d8549",
|
||||
"sha256:a8676d6c68aea1f0e2548c4afdb8e6253873af3bc2659bb5bcd9f39dff7ff90f"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==20.12.1"
|
||||
},
|
||||
"typing-extensions": {
|
||||
|
@ -1181,6 +1249,7 @@
|
|||
"sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
|
||||
"sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"urllib3": {
|
||||
|
@ -1225,6 +1294,7 @@
|
|||
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
|
||||
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.0.0"
|
||||
},
|
||||
"watchgod": {
|
||||
|
@ -1241,6 +1311,14 @@
|
|||
],
|
||||
"version": "==0.2.5"
|
||||
},
|
||||
"webauthn": {
|
||||
"hashes": [
|
||||
"sha256:238391b2e2cc60fb51a2cd2d2d6be149920b9af6184651353d9f95856617a9e7",
|
||||
"sha256:8ad9072ff1d6169f3be30d4dc8733ea563dd266962397bc58b40f674a6af74ac"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.7"
|
||||
},
|
||||
"websocket-client": {
|
||||
"hashes": [
|
||||
"sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549",
|
||||
|
@ -1334,6 +1412,7 @@
|
|||
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
|
||||
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.6.3"
|
||||
},
|
||||
"zope.interface": {
|
||||
|
@ -1391,6 +1470,7 @@
|
|||
"sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd",
|
||||
"sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==5.2.0"
|
||||
}
|
||||
},
|
||||
|
@ -1407,6 +1487,7 @@
|
|||
"sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
|
||||
"sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.3.1"
|
||||
},
|
||||
"astroid": {
|
||||
|
@ -1414,6 +1495,7 @@
|
|||
"sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1",
|
||||
"sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.4.1"
|
||||
},
|
||||
"attrs": {
|
||||
|
@ -1421,6 +1503,7 @@
|
|||
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
|
||||
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.3.0"
|
||||
},
|
||||
"autopep8": {
|
||||
|
@ -1451,6 +1534,7 @@
|
|||
"sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410",
|
||||
"sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"bumpversion": {
|
||||
|
@ -1466,6 +1550,7 @@
|
|||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"colorama": {
|
||||
|
@ -1559,6 +1644,7 @@
|
|||
"sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839",
|
||||
"sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.8.4"
|
||||
},
|
||||
"flake8-polyfill": {
|
||||
|
@ -1573,6 +1659,7 @@
|
|||
"sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac",
|
||||
"sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"
|
||||
],
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==4.0.5"
|
||||
},
|
||||
"gitpython": {
|
||||
|
@ -1580,6 +1667,7 @@
|
|||
"sha256:8621a7e777e276a5ec838b59280ba5272dd144a18169c36c903d8b38b99f750a",
|
||||
"sha256:c5347c81d232d9b8e7f47b68a83e5dc92e7952127133c5f2df9133f2c75a1b29"
|
||||
],
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==3.1.13"
|
||||
},
|
||||
"iniconfig": {
|
||||
|
@ -1594,6 +1682,7 @@
|
|||
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
|
||||
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==4.3.21"
|
||||
},
|
||||
"lazy-object-proxy": {
|
||||
|
@ -1620,6 +1709,7 @@
|
|||
"sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4",
|
||||
"sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"mccabe": {
|
||||
|
@ -1656,6 +1746,7 @@
|
|||
"sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9",
|
||||
"sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"
|
||||
],
|
||||
"markers": "python_version >= '2.6'",
|
||||
"version": "==5.5.1"
|
||||
},
|
||||
"pep8-naming": {
|
||||
|
@ -1670,6 +1761,7 @@
|
|||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"prospector": {
|
||||
|
@ -1684,6 +1776,7 @@
|
|||
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
|
||||
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.10.0"
|
||||
},
|
||||
"pycodestyle": {
|
||||
|
@ -1691,6 +1784,7 @@
|
|||
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
|
||||
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"pydocstyle": {
|
||||
|
@ -1698,6 +1792,7 @@
|
|||
"sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325",
|
||||
"sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==5.1.1"
|
||||
},
|
||||
"pyflakes": {
|
||||
|
@ -1705,6 +1800,7 @@
|
|||
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
|
||||
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"pylint": {
|
||||
|
@ -1747,6 +1843,7 @@
|
|||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pytest": {
|
||||
|
@ -1870,6 +1967,7 @@
|
|||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"smmap": {
|
||||
|
@ -1877,6 +1975,7 @@
|
|||
"sha256:7bfcf367828031dc893530a29cb35eb8c8f2d7c8f2d0989354d75d24c8573714",
|
||||
"sha256:84c2751ef3072d4f6b2785ec7ee40244c6f45eb934d9e543e2c51f1bd3d54c50"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.0.5"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
|
@ -1891,6 +1990,7 @@
|
|||
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
||||
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"stevedore": {
|
||||
|
@ -1898,6 +1998,7 @@
|
|||
"sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee",
|
||||
"sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"toml": {
|
||||
|
@ -1905,6 +2006,7 @@
|
|||
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.10.2"
|
||||
},
|
||||
"typed-ast": {
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
<td role="cell">
|
||||
<ul>
|
||||
{% for flow in stage.flow_set.all %}
|
||||
<li>{{ flow.slug }}<</li>
|
||||
<li>{{ flow.slug }}</li>
|
||||
{% empty %}
|
||||
<li>-</li>
|
||||
{% endfor %}
|
||||
|
|
|
@ -57,15 +57,18 @@ from authentik.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProvide
|
|||
from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
||||
from authentik.sources.oauth.api import OAuthSourceViewSet
|
||||
from authentik.sources.saml.api import SAMLSourceViewSet
|
||||
from authentik.stages.authenticator_static.api import AuthenticatorStaticStageViewSet
|
||||
from authentik.stages.authenticator_totp.api import AuthenticatorTOTPStageViewSet
|
||||
from authentik.stages.authenticator_validate.api import (
|
||||
AuthenticatorValidateStageViewSet,
|
||||
)
|
||||
from authentik.stages.authenticator_webauthn.api import AuthenticateWebAuthnStageViewSet
|
||||
from authentik.stages.captcha.api import CaptchaStageViewSet
|
||||
from authentik.stages.consent.api import ConsentStageViewSet
|
||||
from authentik.stages.dummy.api import DummyStageViewSet
|
||||
from authentik.stages.email.api import EmailStageViewSet
|
||||
from authentik.stages.identification.api import IdentificationStageViewSet
|
||||
from authentik.stages.invitation.api import InvitationStageViewSet, InvitationViewSet
|
||||
from authentik.stages.otp_static.api import OTPStaticStageViewSet
|
||||
from authentik.stages.otp_time.api import OTPTimeStageViewSet
|
||||
from authentik.stages.otp_validate.api import OTPValidateStageViewSet
|
||||
from authentik.stages.password.api import PasswordStageViewSet
|
||||
from authentik.stages.prompt.api import PromptStageViewSet, PromptViewSet
|
||||
from authentik.stages.user_delete.api import UserDeleteStageViewSet
|
||||
|
@ -134,15 +137,16 @@ router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
|||
router.register("propertymappings/scope", ScopeMappingViewSet)
|
||||
|
||||
router.register("stages/all", StageViewSet)
|
||||
router.register("stages/authenticator/static", AuthenticatorStaticStageViewSet)
|
||||
router.register("stages/authenticator/totp", AuthenticatorTOTPStageViewSet)
|
||||
router.register("stages/authenticator/validate", AuthenticatorValidateStageViewSet)
|
||||
router.register("stages/authenticator/webauthn", AuthenticateWebAuthnStageViewSet)
|
||||
router.register("stages/captcha", CaptchaStageViewSet)
|
||||
router.register("stages/consent", ConsentStageViewSet)
|
||||
router.register("stages/email", EmailStageViewSet)
|
||||
router.register("stages/identification", IdentificationStageViewSet)
|
||||
router.register("stages/invitation", InvitationStageViewSet)
|
||||
router.register("stages/invitation/invitations", InvitationViewSet)
|
||||
router.register("stages/otp_static", OTPStaticStageViewSet)
|
||||
router.register("stages/otp_time", OTPTimeStageViewSet)
|
||||
router.register("stages/otp_validate", OTPValidateStageViewSet)
|
||||
router.register("stages/password", PasswordStageViewSet)
|
||||
router.register("stages/prompt/prompts", PromptViewSet)
|
||||
router.register("stages/prompt/stages", PromptStageViewSet)
|
||||
|
|
|
@ -3,7 +3,7 @@ from dataclasses import dataclass
|
|||
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Model
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.shortcuts import get_object_or_404, reverse
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
|
@ -18,8 +18,11 @@ from rest_framework.serializers import (
|
|||
)
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.flows.models import Flow, FlowStageBinding, Stage
|
||||
from authentik.flows.planner import cache_key
|
||||
from authentik.lib.templatetags.authentik_utils import verbose_name
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
|
||||
|
||||
class FlowSerializer(ModelSerializer):
|
||||
|
@ -154,24 +157,19 @@ class FlowViewSet(ModelViewSet):
|
|||
return Response({"diagram": diagram})
|
||||
|
||||
|
||||
class StageSerializer(ModelSerializer):
|
||||
class StageSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""Stage Serializer"""
|
||||
|
||||
__type__ = SerializerMethodField(method_name="get_type")
|
||||
verbose_name = SerializerMethodField(method_name="get_verbose_name")
|
||||
object_type = SerializerMethodField()
|
||||
|
||||
def get_type(self, obj: Stage) -> str:
|
||||
def get_object_type(self, obj):
|
||||
"""Get object type so that we know which API Endpoint to use to get the full object"""
|
||||
return obj._meta.object_name.lower().replace("stage", "")
|
||||
|
||||
def get_verbose_name(self, obj: Stage) -> str:
|
||||
"""Get verbose name for UI"""
|
||||
return obj._meta.verbose_name
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Stage
|
||||
fields = ["pk", "name", "__type__", "verbose_name"]
|
||||
fields = ["pk", "name", "object_type", "verbose_name", "verbose_name_plural"]
|
||||
|
||||
|
||||
class StageViewSet(ReadOnlyModelViewSet):
|
||||
|
@ -183,6 +181,23 @@ class StageViewSet(ReadOnlyModelViewSet):
|
|||
def get_queryset(self):
|
||||
return Stage.objects.select_subclasses()
|
||||
|
||||
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||
@action(detail=False)
|
||||
def types(self, request: Request) -> Response:
|
||||
"""Get all creatable stage types"""
|
||||
data = []
|
||||
for subclass in all_subclasses(self.queryset.model, False):
|
||||
data.append(
|
||||
{
|
||||
"name": verbose_name(subclass),
|
||||
"description": subclass.__doc__,
|
||||
"link": reverse("authentik_admin:stage-create")
|
||||
+ f"?type={subclass.__name__}",
|
||||
}
|
||||
)
|
||||
data = sorted(data, key=lambda x: x["name"])
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
||||
|
||||
class FlowStageBindingSerializer(ModelSerializer):
|
||||
"""FlowStageBinding Serializer"""
|
||||
|
|
|
@ -50,21 +50,21 @@ def create_default_authentication_flow(
|
|||
target=flow,
|
||||
stage=identification_stage,
|
||||
defaults={
|
||||
"order": 0,
|
||||
"order": 10,
|
||||
},
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow,
|
||||
stage=password_stage,
|
||||
defaults={
|
||||
"order": 1,
|
||||
"order": 20,
|
||||
},
|
||||
)
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow,
|
||||
stage=login_stage,
|
||||
defaults={
|
||||
"order": 2,
|
||||
"order": 100,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ class TestFlowsAPI(APITestCase):
|
|||
def test_api_serializer(self):
|
||||
"""Test that stage serializer returns the correct type"""
|
||||
obj = DummyStage()
|
||||
self.assertEqual(StageSerializer().get_type(obj), "dummy")
|
||||
self.assertEqual(StageSerializer().get_object_type(obj), "dummy")
|
||||
self.assertEqual(StageSerializer().get_verbose_name(obj), "Dummy Stage")
|
||||
|
||||
def test_api_viewset(self):
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
"""transfer common classes"""
|
||||
from dataclasses import asdict, dataclass, field, is_dataclass
|
||||
from json.encoder import JSONEncoder
|
||||
from typing import Any, Dict, List
|
||||
from uuid import UUID
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
|
||||
|
@ -11,7 +12,17 @@ from authentik.lib.sentry import SentryIgnoredException
|
|||
def get_attrs(obj: SerializerModel) -> Dict[str, Any]:
|
||||
"""Get object's attributes via their serializer, and covert it to a normal dict"""
|
||||
data = dict(obj.serializer(obj).data)
|
||||
to_remove = ("policies", "stages", "pk", "background", "group", "user")
|
||||
to_remove = (
|
||||
"policies",
|
||||
"stages",
|
||||
"pk",
|
||||
"background",
|
||||
"group",
|
||||
"user",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"object_type",
|
||||
)
|
||||
for to_remove_name in to_remove:
|
||||
if to_remove_name in data:
|
||||
data.pop(to_remove_name)
|
||||
|
@ -53,7 +64,7 @@ class FlowBundle:
|
|||
entries: List[FlowBundleEntry] = field(default_factory=list)
|
||||
|
||||
|
||||
class DataclassEncoder(JSONEncoder):
|
||||
class DataclassEncoder(DjangoJSONEncoder):
|
||||
"""Convert FlowBundleEntry to json"""
|
||||
|
||||
def default(self, o):
|
||||
|
|
|
@ -98,4 +98,5 @@ class FlowExporter:
|
|||
|
||||
def export_to_string(self) -> str:
|
||||
"""Call export and convert it to json"""
|
||||
return dumps(self.export(), cls=DataclassEncoder)
|
||||
bundle = self.export()
|
||||
return dumps(bundle, cls=DataclassEncoder)
|
||||
|
|
|
@ -60,8 +60,11 @@ class Migration(migrations.Migration):
|
|||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
||||
("authentik.stages.otp_static", "authentik OTP.Static"),
|
||||
("authentik.stages.otp_time", "authentik OTP.Time"),
|
||||
("authentik.stages.otp_validate", "authentik OTP.Validate"),
|
||||
("authentik.stages.authenticator_totp", "authentik OTP.Time"),
|
||||
(
|
||||
"authentik.stages.authenticator_validate",
|
||||
"authentik OTP.Validate",
|
||||
),
|
||||
("authentik.stages.password", "authentik Stages.Password"),
|
||||
("authentik.core", "authentik Core"),
|
||||
],
|
||||
|
|
|
@ -67,8 +67,14 @@ class Migration(migrations.Migration):
|
|||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
||||
("authentik.stages.otp_static", "authentik Stages.OTP.Static"),
|
||||
("authentik.stages.otp_time", "authentik Stages.OTP.Time"),
|
||||
("authentik.stages.otp_validate", "authentik Stages.OTP.Validate"),
|
||||
(
|
||||
"authentik.stages.authenticator_totp",
|
||||
"authentik Stages.OTP.Time",
|
||||
),
|
||||
(
|
||||
"authentik.stages.authenticator_validate",
|
||||
"authentik Stages.OTP.Validate",
|
||||
),
|
||||
("authentik.stages.password", "authentik Stages.Password"),
|
||||
("authentik.core", "authentik Core"),
|
||||
],
|
||||
|
|
|
@ -60,8 +60,14 @@ class Migration(migrations.Migration):
|
|||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
||||
("authentik.stages.otp_static", "authentik Stages.OTP.Static"),
|
||||
("authentik.stages.otp_time", "authentik Stages.OTP.Time"),
|
||||
("authentik.stages.otp_validate", "authentik Stages.OTP.Validate"),
|
||||
(
|
||||
"authentik.stages.authenticator_totp",
|
||||
"authentik Stages.OTP.Time",
|
||||
),
|
||||
(
|
||||
"authentik.stages.authenticator_validate",
|
||||
"authentik Stages.OTP.Validate",
|
||||
),
|
||||
("authentik.stages.password", "authentik Stages.Password"),
|
||||
("authentik.managed", "authentik Managed"),
|
||||
("authentik.core", "authentik Core"),
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
# Generated by Django 3.1.6 on 2021-02-13 16:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_policies_event_matcher", "0007_auto_20210209_1657"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="eventmatcherpolicy",
|
||||
name="app",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("authentik.admin", "authentik Admin"),
|
||||
("authentik.api", "authentik API"),
|
||||
("authentik.events", "authentik Events"),
|
||||
("authentik.crypto", "authentik Crypto"),
|
||||
("authentik.flows", "authentik Flows"),
|
||||
("authentik.outposts", "authentik Outpost"),
|
||||
("authentik.lib", "authentik lib"),
|
||||
("authentik.policies", "authentik Policies"),
|
||||
("authentik.policies.dummy", "authentik Policies.Dummy"),
|
||||
(
|
||||
"authentik.policies.event_matcher",
|
||||
"authentik Policies.Event Matcher",
|
||||
),
|
||||
("authentik.policies.expiry", "authentik Policies.Expiry"),
|
||||
("authentik.policies.expression", "authentik Policies.Expression"),
|
||||
(
|
||||
"authentik.policies.group_membership",
|
||||
"authentik Policies.Group Membership",
|
||||
),
|
||||
("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"),
|
||||
("authentik.policies.password", "authentik Policies.Password"),
|
||||
("authentik.policies.reputation", "authentik Policies.Reputation"),
|
||||
("authentik.providers.proxy", "authentik Providers.Proxy"),
|
||||
("authentik.providers.oauth2", "authentik Providers.OAuth2"),
|
||||
("authentik.providers.saml", "authentik Providers.SAML"),
|
||||
("authentik.recovery", "authentik Recovery"),
|
||||
("authentik.sources.ldap", "authentik Sources.LDAP"),
|
||||
("authentik.sources.oauth", "authentik Sources.OAuth"),
|
||||
("authentik.sources.saml", "authentik Sources.SAML"),
|
||||
("authentik.stages.captcha", "authentik Stages.Captcha"),
|
||||
("authentik.stages.consent", "authentik Stages.Consent"),
|
||||
("authentik.stages.dummy", "authentik Stages.Dummy"),
|
||||
("authentik.stages.email", "authentik Stages.Email"),
|
||||
("authentik.stages.prompt", "authentik Stages.Prompt"),
|
||||
(
|
||||
"authentik.stages.identification",
|
||||
"authentik Stages.Identification",
|
||||
),
|
||||
("authentik.stages.invitation", "authentik Stages.User Invitation"),
|
||||
("authentik.stages.user_delete", "authentik Stages.User Delete"),
|
||||
("authentik.stages.user_login", "authentik Stages.User Login"),
|
||||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
||||
("authentik.stages.otp_static", "authentik Stages.OTP.Static"),
|
||||
(
|
||||
"authentik.stages.authenticator_totp",
|
||||
"authentik Stages.OTP.Time",
|
||||
),
|
||||
(
|
||||
"authentik.stages.authenticator_validate",
|
||||
"authentik Stages.OTP.Validate",
|
||||
),
|
||||
("authentik.stages.password", "authentik Stages.Password"),
|
||||
(
|
||||
"authentik.stages.authenticator_webauthn",
|
||||
"authentik Stages.WebAuthn",
|
||||
),
|
||||
("authentik.managed", "authentik Managed"),
|
||||
("authentik.core", "authentik Core"),
|
||||
],
|
||||
default="",
|
||||
help_text="Match events created by selected application. When left empty, all applications are matched.",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,86 @@
|
|||
# Generated by Django 3.1.6 on 2021-02-15 21:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_policies_event_matcher", "0008_auto_20210213_1640"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="eventmatcherpolicy",
|
||||
name="app",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("authentik.admin", "authentik Admin"),
|
||||
("authentik.api", "authentik API"),
|
||||
("authentik.events", "authentik Events"),
|
||||
("authentik.crypto", "authentik Crypto"),
|
||||
("authentik.flows", "authentik Flows"),
|
||||
("authentik.outposts", "authentik Outpost"),
|
||||
("authentik.lib", "authentik lib"),
|
||||
("authentik.policies", "authentik Policies"),
|
||||
("authentik.policies.dummy", "authentik Policies.Dummy"),
|
||||
(
|
||||
"authentik.policies.event_matcher",
|
||||
"authentik Policies.Event Matcher",
|
||||
),
|
||||
("authentik.policies.expiry", "authentik Policies.Expiry"),
|
||||
("authentik.policies.expression", "authentik Policies.Expression"),
|
||||
(
|
||||
"authentik.policies.group_membership",
|
||||
"authentik Policies.Group Membership",
|
||||
),
|
||||
("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"),
|
||||
("authentik.policies.password", "authentik Policies.Password"),
|
||||
("authentik.policies.reputation", "authentik Policies.Reputation"),
|
||||
("authentik.providers.proxy", "authentik Providers.Proxy"),
|
||||
("authentik.providers.oauth2", "authentik Providers.OAuth2"),
|
||||
("authentik.providers.saml", "authentik Providers.SAML"),
|
||||
("authentik.recovery", "authentik Recovery"),
|
||||
("authentik.sources.ldap", "authentik Sources.LDAP"),
|
||||
("authentik.sources.oauth", "authentik Sources.OAuth"),
|
||||
("authentik.sources.saml", "authentik Sources.SAML"),
|
||||
("authentik.stages.captcha", "authentik Stages.Captcha"),
|
||||
("authentik.stages.consent", "authentik Stages.Consent"),
|
||||
("authentik.stages.dummy", "authentik Stages.Dummy"),
|
||||
("authentik.stages.email", "authentik Stages.Email"),
|
||||
("authentik.stages.prompt", "authentik Stages.Prompt"),
|
||||
(
|
||||
"authentik.stages.identification",
|
||||
"authentik Stages.Identification",
|
||||
),
|
||||
("authentik.stages.invitation", "authentik Stages.User Invitation"),
|
||||
("authentik.stages.user_delete", "authentik Stages.User Delete"),
|
||||
("authentik.stages.user_login", "authentik Stages.User Login"),
|
||||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
||||
(
|
||||
"authentik.stages.authenticator_static",
|
||||
"authentik Stages.Authenticator.Static",
|
||||
),
|
||||
(
|
||||
"authentik.stages.authenticator_totp",
|
||||
"authentik Stages.Authenticator.TOTP",
|
||||
),
|
||||
(
|
||||
"authentik.stages.authenticator_validate",
|
||||
"authentik Stages.Authenticator.Validate",
|
||||
),
|
||||
(
|
||||
"authentik.stages.authenticator_webauthn",
|
||||
"authentik Stages.Authenticator.WebAuthn",
|
||||
),
|
||||
("authentik.stages.password", "authentik Stages.Password"),
|
||||
("authentik.managed", "authentik Managed"),
|
||||
("authentik.core", "authentik Core"),
|
||||
],
|
||||
default="",
|
||||
help_text="Match events created by selected application. When left empty, all applications are matched.",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -119,9 +119,10 @@ INSTALLED_APPS = [
|
|||
"authentik.stages.user_login.apps.AuthentikStageUserLoginConfig",
|
||||
"authentik.stages.user_logout.apps.AuthentikStageUserLogoutConfig",
|
||||
"authentik.stages.user_write.apps.AuthentikStageUserWriteConfig",
|
||||
"authentik.stages.otp_static.apps.AuthentikStageOTPStaticConfig",
|
||||
"authentik.stages.otp_time.apps.AuthentikStageOTPTimeConfig",
|
||||
"authentik.stages.otp_validate.apps.AuthentikStageOTPValidateConfig",
|
||||
"authentik.stages.authenticator_static.apps.AuthentikStageAuthenticatorStaticConfig",
|
||||
"authentik.stages.authenticator_totp.apps.AuthentikStageAuthenticatorTOTPConfig",
|
||||
"authentik.stages.authenticator_validate.apps.AuthentikStageAuthenticatorValidateConfig",
|
||||
"authentik.stages.authenticator_webauthn.apps.AuthentikStageAuthenticatorWebAuthnConfig",
|
||||
"authentik.stages.password.apps.AuthentikStagePasswordConfig",
|
||||
"rest_framework",
|
||||
"django_filters",
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
"""AuthenticatorStaticStage API Views"""
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
|
||||
|
||||
|
||||
class AuthenticatorStaticStageSerializer(StageSerializer):
|
||||
"""AuthenticatorStaticStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = AuthenticatorStaticStage
|
||||
fields = StageSerializer.Meta.fields + ["configure_flow", "token_count"]
|
||||
|
||||
|
||||
class AuthenticatorStaticStageViewSet(ModelViewSet):
|
||||
"""AuthenticatorStaticStage Viewset"""
|
||||
|
||||
queryset = AuthenticatorStaticStage.objects.all()
|
||||
serializer_class = AuthenticatorStaticStageSerializer
|
|
@ -0,0 +1,11 @@
|
|||
"""Authenticator Static stage"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthentikStageAuthenticatorStaticConfig(AppConfig):
|
||||
"""Authenticator Static stage"""
|
||||
|
||||
name = "authentik.stages.authenticator_static"
|
||||
label = "authentik_stages_authenticator_static"
|
||||
verbose_name = "authentik Stages.Authenticator.Static"
|
||||
mountpoint = "-/user/authenticator/static/"
|
|
@ -1,8 +1,8 @@
|
|||
"""OTP Static forms"""
|
||||
"""Static Authenticator forms"""
|
||||
from django import forms
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from authentik.stages.otp_static.models import OTPStaticStage
|
||||
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
|
||||
|
||||
|
||||
class StaticTokenWidget(forms.widgets.Widget):
|
||||
|
@ -26,12 +26,12 @@ class SetupForm(forms.Form):
|
|||
self.fields["tokens"].initial = tokens
|
||||
|
||||
|
||||
class OTPStaticStageForm(forms.ModelForm):
|
||||
"""OTP Static Stage setup form"""
|
||||
class AuthenticatorStaticStageForm(forms.ModelForm):
|
||||
"""Static Authenticator Stage setup form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = OTPStaticStage
|
||||
model = AuthenticatorStaticStage
|
||||
fields = ["name", "configure_flow", "token_count"]
|
||||
|
||||
widgets = {
|
|
@ -30,8 +30,8 @@ class Migration(migrations.Migration):
|
|||
("token_count", models.IntegerField(default=6)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "OTP Static Setup Stage",
|
||||
"verbose_name_plural": "OTP Static Setup Stages",
|
||||
"verbose_name": "Static Authenticator Setup Stage",
|
||||
"verbose_name_plural": "Static Authenticator Setup Stages",
|
||||
},
|
||||
bases=("authentik_flows.stage",),
|
||||
),
|
|
@ -8,7 +8,7 @@ class Migration(migrations.Migration):
|
|||
|
||||
dependencies = [
|
||||
("authentik_flows", "0013_auto_20200924_1605"),
|
||||
("authentik_stages_otp_static", "0001_initial"),
|
||||
("authentik_stages_authenticator_static", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
|
@ -0,0 +1,15 @@
|
|||
# Generated by Django 3.1.1 on 2020-09-25 14:32
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_stages_authenticator_static",
|
||||
"0002_otpstaticstage_configure_flow",
|
||||
),
|
||||
]
|
||||
|
||||
operations = []
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.1.6 on 2021-02-16 08:38
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0016_auto_20201202_1307"),
|
||||
("authentik_stages_authenticator_static", "0003_default_setup_flow"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name="OTPStaticStage",
|
||||
new_name="AuthenticatorStaticStage",
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="authenticatorstaticstage",
|
||||
options={
|
||||
"verbose_name": "Static Authenticator Stage",
|
||||
"verbose_name_plural": "Static Authenticator Stages",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,55 @@
|
|||
# Generated by Django 3.1.1 on 2020-09-25 14:32
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from authentik.flows.models import FlowDesignation
|
||||
|
||||
|
||||
def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
Flow = apps.get_model("authentik_flows", "Flow")
|
||||
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
|
||||
|
||||
AuthenticatorStaticStage = apps.get_model(
|
||||
"authentik_stages_authenticator_static", "AuthenticatorStaticStage"
|
||||
)
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
||||
slug="default-authenticator-static-setup",
|
||||
designation=FlowDesignation.STAGE_CONFIGURATION,
|
||||
defaults={
|
||||
"name": "default-authenticator-static-setup",
|
||||
"title": "Setup Static OTP Tokens",
|
||||
},
|
||||
)
|
||||
|
||||
stage, _ = AuthenticatorStaticStage.objects.using(db_alias).update_or_create(
|
||||
name="default-authenticator-static-setup", defaults={"token_count": 6}
|
||||
)
|
||||
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow, stage=stage, defaults={"order": 0}
|
||||
)
|
||||
|
||||
for stage in AuthenticatorStaticStage.objects.using(db_alias).filter(
|
||||
configure_flow=None
|
||||
):
|
||||
stage.configure_flow = flow
|
||||
stage.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_stages_authenticator_static",
|
||||
"0004_auto_20210216_0838",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_default_setup_flow),
|
||||
]
|
|
@ -0,0 +1,56 @@
|
|||
"""Static Authenticator models"""
|
||||
from typing import Optional, Type
|
||||
|
||||
from django.db import models
|
||||
from django.forms import ModelForm
|
||||
from django.shortcuts import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.flows.models import ConfigurableStage, Stage
|
||||
|
||||
|
||||
class AuthenticatorStaticStage(ConfigurableStage, Stage):
|
||||
"""Generate static tokens for the user as a backup."""
|
||||
|
||||
token_count = models.IntegerField(default=6)
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
from authentik.stages.authenticator_static.api import (
|
||||
AuthenticatorStaticStageSerializer,
|
||||
)
|
||||
|
||||
return AuthenticatorStaticStageSerializer
|
||||
|
||||
@property
|
||||
def type(self) -> Type[View]:
|
||||
from authentik.stages.authenticator_static.stage import (
|
||||
AuthenticatorStaticStageView,
|
||||
)
|
||||
|
||||
return AuthenticatorStaticStageView
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from authentik.stages.authenticator_static.forms import (
|
||||
AuthenticatorStaticStageForm,
|
||||
)
|
||||
|
||||
return AuthenticatorStaticStageForm
|
||||
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[str]:
|
||||
return reverse(
|
||||
"authentik_stages_authenticator_static:user-settings",
|
||||
kwargs={"stage_uuid": self.stage_uuid},
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Static Authenticator Stage {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Static Authenticator Stage")
|
||||
verbose_name_plural = _("Static Authenticator Stages")
|
|
@ -1,4 +1,4 @@
|
|||
"""OTP Static settings"""
|
||||
"""Static Authenticator settings"""
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django_otp.plugins.otp_static",
|
|
@ -8,15 +8,15 @@ from structlog.stdlib import get_logger
|
|||
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.stages.otp_static.forms import SetupForm
|
||||
from authentik.stages.otp_static.models import OTPStaticStage
|
||||
from authentik.stages.authenticator_static.forms import SetupForm
|
||||
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
|
||||
|
||||
LOGGER = get_logger()
|
||||
SESSION_STATIC_DEVICE = "static_device"
|
||||
SESSION_STATIC_TOKENS = "static_device_tokens"
|
||||
|
||||
|
||||
class OTPStaticStageView(FormView, StageView):
|
||||
class AuthenticatorStaticStageView(FormView, StageView):
|
||||
"""Static OTP Setup stage"""
|
||||
|
||||
form_class = SetupForm
|
||||
|
@ -38,7 +38,7 @@ class OTPStaticStageView(FormView, StageView):
|
|||
if StaticDevice.objects.filter(user=user).exists():
|
||||
return self.executor.stage_ok()
|
||||
|
||||
stage: OTPStaticStage = self.executor.current_stage
|
||||
stage: AuthenticatorStaticStage = self.executor.current_stage
|
||||
|
||||
if SESSION_STATIC_DEVICE not in self.request.session:
|
||||
device = StaticDevice(user=user, confirmed=True)
|
|
@ -22,10 +22,10 @@
|
|||
</ul>
|
||||
{% if not state %}
|
||||
{% if stage.configure_flow %}
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next={% url 'authentik_core:user-settings' %}" class="ak-root-link pf-c-button pf-m-primary">{% trans "Enable Static Tokens" %}</a>
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next=/%23{% url 'authentik_core:user-settings' %}" class="ak-root-link pf-c-button pf-m-primary">{% trans "Enable Static Tokens" %}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="{% url 'authentik_stages_otp_static:disable' stage_uuid=stage.stage_uuid %}" class="ak-root-pf-c-button pf-m-danger">{% trans "Disable Static Tokens" %}</a>
|
||||
<a href="{% url 'authentik_stages_authenticator_static:disable' stage_uuid=stage.stage_uuid %}" class="ak-root-pf-c-button pf-m-danger">{% trans "Disable Static Tokens" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +1,7 @@
|
|||
"""OTP static urls"""
|
||||
"""Static Authenticator urls"""
|
||||
from django.urls import path
|
||||
|
||||
from authentik.stages.otp_static.views import DisableView, UserSettingsView
|
||||
from authentik.stages.authenticator_static.views import DisableView, UserSettingsView
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
|
@ -1,4 +1,4 @@
|
|||
"""otp Static view Tokens"""
|
||||
"""Static Authenticator view Tokens"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
@ -8,17 +8,19 @@ from django.views.generic import TemplateView
|
|||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
|
||||
from authentik.events.models import Event
|
||||
from authentik.stages.otp_static.models import OTPStaticStage
|
||||
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
|
||||
|
||||
|
||||
class UserSettingsView(LoginRequiredMixin, TemplateView):
|
||||
"""View for user settings to control OTP"""
|
||||
|
||||
template_name = "stages/otp_static/user_settings.html"
|
||||
template_name = "stages/authenticator_static/user_settings.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
stage = get_object_or_404(OTPStaticStage, pk=self.kwargs["stage_uuid"])
|
||||
stage = get_object_or_404(
|
||||
AuthenticatorStaticStage, pk=self.kwargs["stage_uuid"]
|
||||
)
|
||||
kwargs["stage"] = stage
|
||||
static_devices = StaticDevice.objects.filter(
|
||||
user=self.request.user, confirmed=True
|
|
@ -0,0 +1,21 @@
|
|||
"""AuthenticatorTOTPStage API Views"""
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
|
||||
|
||||
|
||||
class AuthenticatorTOTPStageSerializer(StageSerializer):
|
||||
"""AuthenticatorTOTPStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = AuthenticatorTOTPStage
|
||||
fields = StageSerializer.Meta.fields + ["configure_flow", "digits"]
|
||||
|
||||
|
||||
class AuthenticatorTOTPStageViewSet(ModelViewSet):
|
||||
"""AuthenticatorTOTPStage Viewset"""
|
||||
|
||||
queryset = AuthenticatorTOTPStage.objects.all()
|
||||
serializer_class = AuthenticatorTOTPStageSerializer
|
|
@ -0,0 +1,11 @@
|
|||
"""OTP Time"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthentikStageAuthenticatorTOTPConfig(AppConfig):
|
||||
"""TOTP App config"""
|
||||
|
||||
name = "authentik.stages.authenticator_totp"
|
||||
label = "authentik_stages_authenticator_totp"
|
||||
verbose_name = "authentik Stages.Authenticator.TOTP"
|
||||
mountpoint = "-/user/authenticator/totp/"
|
|
@ -4,7 +4,7 @@ from django.utils.safestring import mark_safe
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django_otp.models import Device
|
||||
|
||||
from authentik.stages.otp_time.models import OTPTimeStage
|
||||
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
|
||||
|
||||
|
||||
class PictureWidget(forms.widgets.Widget):
|
||||
|
@ -49,12 +49,12 @@ class SetupForm(forms.Form):
|
|||
return self.cleaned_data.get("code")
|
||||
|
||||
|
||||
class OTPTimeStageForm(forms.ModelForm):
|
||||
class AuthenticatorTOTPStageForm(forms.ModelForm):
|
||||
"""OTP Time-based Stage setup form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = OTPTimeStage
|
||||
model = AuthenticatorTOTPStage
|
||||
fields = ["name", "configure_flow", "digits"]
|
||||
|
||||
widgets = {
|
|
@ -6,7 +6,7 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_otp_time", "0001_initial"),
|
||||
("authentik_stages_authenticator_totp", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
|
@ -8,7 +8,7 @@ class Migration(migrations.Migration):
|
|||
|
||||
dependencies = [
|
||||
("authentik_flows", "0013_auto_20200924_1605"),
|
||||
("authentik_stages_otp_time", "0002_auto_20200701_1900"),
|
||||
("authentik_stages_authenticator_totp", "0002_auto_20200701_1900"),
|
||||
]
|
||||
|
||||
operations = [
|
|
@ -0,0 +1,15 @@
|
|||
# Generated by Django 3.1.1 on 2020-09-25 15:36
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_stages_authenticator_totp",
|
||||
"0003_otptimestage_configure_flow",
|
||||
),
|
||||
]
|
||||
|
||||
operations = []
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.1.6 on 2021-02-16 08:38
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0016_auto_20201202_1307"),
|
||||
("authentik_stages_authenticator_totp", "0004_default_setup_flow"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name="OTPTimeStage",
|
||||
new_name="AuthenticatorTOTPStage",
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="authenticatortotpstage",
|
||||
options={
|
||||
"verbose_name": "TOTP Authenticator Setup Stage",
|
||||
"verbose_name_plural": "TOTP Authenticator Setup Stages",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -5,35 +5,39 @@ from django.db import migrations
|
|||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.stages.otp_time.models import TOTPDigits
|
||||
from authentik.stages.authenticator_totp.models import TOTPDigits
|
||||
|
||||
|
||||
def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
Flow = apps.get_model("authentik_flows", "Flow")
|
||||
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
|
||||
|
||||
OTPTimeStage = apps.get_model("authentik_stages_otp_time", "OTPTimeStage")
|
||||
AuthenticatorTOTPStage = apps.get_model(
|
||||
"authentik_stages_authenticator_totp", "AuthenticatorTOTPStage"
|
||||
)
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
||||
slug="default-otp-time-configure",
|
||||
slug="default-authenticator-totp-setup",
|
||||
designation=FlowDesignation.STAGE_CONFIGURATION,
|
||||
defaults={
|
||||
"name": "default-otp-time-configure",
|
||||
"name": "default-authenticator-totp-setup",
|
||||
"title": "Setup Two-Factor authentication",
|
||||
},
|
||||
)
|
||||
|
||||
stage, _ = OTPTimeStage.objects.using(db_alias).update_or_create(
|
||||
name="default-otp-time-configure", defaults={"digits": TOTPDigits.SIX}
|
||||
stage, _ = AuthenticatorTOTPStage.objects.using(db_alias).update_or_create(
|
||||
name="default-authenticator-totp-setup", defaults={"digits": TOTPDigits.SIX}
|
||||
)
|
||||
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow, stage=stage, defaults={"order": 0}
|
||||
)
|
||||
|
||||
for stage in OTPTimeStage.objects.using(db_alias).filter(configure_flow=None):
|
||||
for stage in AuthenticatorTOTPStage.objects.using(db_alias).filter(
|
||||
configure_flow=None
|
||||
):
|
||||
stage.configure_flow = flow
|
||||
stage.save()
|
||||
|
||||
|
@ -41,7 +45,10 @@ def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_otp_time", "0003_otptimestage_configure_flow"),
|
||||
(
|
||||
"authentik_stages_authenticator_totp",
|
||||
"0005_auto_20210216_0838",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
|
@ -18,40 +18,42 @@ class TOTPDigits(models.IntegerChoices):
|
|||
EIGHT = 8, _("8 digits, not compatible with apps like Google Authenticator")
|
||||
|
||||
|
||||
class OTPTimeStage(ConfigurableStage, Stage):
|
||||
class AuthenticatorTOTPStage(ConfigurableStage, Stage):
|
||||
"""Enroll a user's device into Time-based OTP."""
|
||||
|
||||
digits = models.IntegerField(choices=TOTPDigits.choices)
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
from authentik.stages.otp_time.api import OTPTimeStageSerializer
|
||||
from authentik.stages.authenticator_totp.api import (
|
||||
AuthenticatorTOTPStageSerializer,
|
||||
)
|
||||
|
||||
return OTPTimeStageSerializer
|
||||
return AuthenticatorTOTPStageSerializer
|
||||
|
||||
@property
|
||||
def type(self) -> Type[View]:
|
||||
from authentik.stages.otp_time.stage import OTPTimeStageView
|
||||
from authentik.stages.authenticator_totp.stage import AuthenticatorTOTPStageView
|
||||
|
||||
return OTPTimeStageView
|
||||
return AuthenticatorTOTPStageView
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from authentik.stages.otp_time.forms import OTPTimeStageForm
|
||||
from authentik.stages.authenticator_totp.forms import AuthenticatorTOTPStageForm
|
||||
|
||||
return OTPTimeStageForm
|
||||
return AuthenticatorTOTPStageForm
|
||||
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[str]:
|
||||
return reverse(
|
||||
"authentik_stages_otp_time:user-settings",
|
||||
"authentik_stages_authenticator_totp:user-settings",
|
||||
kwargs={"stage_uuid": self.stage_uuid},
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"OTP Time (TOTP) Stage {self.name}"
|
||||
return f"TOTP Authenticator Setup Stage {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("OTP Time (TOTP) Setup Stage")
|
||||
verbose_name_plural = _("OTP Time (TOTP) Setup Stages")
|
||||
verbose_name = _("TOTP Authenticator Setup Stage")
|
||||
verbose_name_plural = _("TOTP Authenticator Setup Stages")
|
|
@ -12,14 +12,14 @@ from structlog.stdlib import get_logger
|
|||
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.stages.otp_time.forms import SetupForm
|
||||
from authentik.stages.otp_time.models import OTPTimeStage
|
||||
from authentik.stages.authenticator_totp.forms import SetupForm
|
||||
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
|
||||
|
||||
LOGGER = get_logger()
|
||||
SESSION_TOTP_DEVICE = "totp_device"
|
||||
|
||||
|
||||
class OTPTimeStageView(FormView, StageView):
|
||||
class AuthenticatorTOTPStageView(FormView, StageView):
|
||||
"""OTP totp Setup stage"""
|
||||
|
||||
form_class = SetupForm
|
||||
|
@ -50,7 +50,7 @@ class OTPTimeStageView(FormView, StageView):
|
|||
if TOTPDevice.objects.filter(user=user).exists():
|
||||
return self.executor.stage_ok()
|
||||
|
||||
stage: OTPTimeStage = self.executor.current_stage
|
||||
stage: AuthenticatorTOTPStage = self.executor.current_stage
|
||||
|
||||
if SESSION_TOTP_DEVICE not in self.request.session:
|
||||
device = TOTPDevice(user=user, confirmed=True, digits=stage.digits)
|
|
@ -18,10 +18,10 @@
|
|||
<p>
|
||||
{% if not state %}
|
||||
{% if stage.configure_flow %}
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next={% url 'authentik_core:user-settings' %}" class="ak-root-link pf-c-button pf-m-primary">{% trans "Enable Time-based OTP" %}</a>
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next=/%23{% url 'authentik_core:user-settings' %}" class="ak-root-link pf-c-button pf-m-primary">{% trans "Enable Time-based OTP" %}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="{% url 'authentik_stages_otp_time:disable' stage_uuid=stage.stage_uuid %}" class="ak-root-pf-c-button pf-m-danger">{% trans "Disable Time-based OTP" %}</a>
|
||||
<a href="{% url 'authentik_stages_authenticator_totp:disable' stage_uuid=stage.stage_uuid %}" class="ak-root-pf-c-button pf-m-danger">{% trans "Disable Time-based OTP" %}</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
|
@ -1,7 +1,7 @@
|
|||
"""OTP Time urls"""
|
||||
from django.urls import path
|
||||
|
||||
from authentik.stages.otp_time.views import DisableView, UserSettingsView
|
||||
from authentik.stages.authenticator_totp.views import DisableView, UserSettingsView
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
|
@ -8,17 +8,17 @@ from django.views.generic import TemplateView
|
|||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
|
||||
from authentik.events.models import Event
|
||||
from authentik.stages.otp_time.models import OTPTimeStage
|
||||
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
|
||||
|
||||
|
||||
class UserSettingsView(LoginRequiredMixin, TemplateView):
|
||||
"""View for user settings to control OTP"""
|
||||
|
||||
template_name = "stages/otp_time/user_settings.html"
|
||||
template_name = "stages/authenticator_totp/user_settings.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
stage = get_object_or_404(OTPTimeStage, pk=self.kwargs["stage_uuid"])
|
||||
stage = get_object_or_404(AuthenticatorTOTPStage, pk=self.kwargs["stage_uuid"])
|
||||
kwargs["stage"] = stage
|
||||
|
||||
totp_devices = TOTPDevice.objects.filter(user=self.request.user, confirmed=True)
|
|
@ -0,0 +1,24 @@
|
|||
"""AuthenticatorValidateStage API Views"""
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
||||
|
||||
|
||||
class AuthenticatorValidateStageSerializer(StageSerializer):
|
||||
"""AuthenticatorValidateStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = AuthenticatorValidateStage
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"not_configured_action",
|
||||
"device_classes",
|
||||
]
|
||||
|
||||
|
||||
class AuthenticatorValidateStageViewSet(ModelViewSet):
|
||||
"""AuthenticatorValidateStage Viewset"""
|
||||
|
||||
queryset = AuthenticatorValidateStage.objects.all()
|
||||
serializer_class = AuthenticatorValidateStageSerializer
|
|
@ -0,0 +1,10 @@
|
|||
"""Authenticator Validation Stage"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthentikStageAuthenticatorValidateConfig(AppConfig):
|
||||
"""Authenticator Validation Stage"""
|
||||
|
||||
name = "authentik.stages.authenticator_validate"
|
||||
label = "authentik_stages_authenticator_validate"
|
||||
verbose_name = "authentik Stages.Authenticator.Validate"
|
|
@ -4,7 +4,10 @@ from django.utils.translation import gettext_lazy as _
|
|||
from django_otp import match_token
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.stages.otp_validate.models import OTPValidateStage
|
||||
from authentik.stages.authenticator_validate.models import (
|
||||
AuthenticatorValidateStage,
|
||||
DeviceClasses,
|
||||
)
|
||||
|
||||
|
||||
class ValidationForm(forms.Form):
|
||||
|
@ -36,14 +39,15 @@ class ValidationForm(forms.Form):
|
|||
return code
|
||||
|
||||
|
||||
class OTPValidateStageForm(forms.ModelForm):
|
||||
class AuthenticatorValidateStageForm(forms.ModelForm):
|
||||
"""OTP Validate stage forms"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = OTPValidateStage
|
||||
fields = ["name"]
|
||||
model = AuthenticatorValidateStage
|
||||
fields = ["name", "device_classes"]
|
||||
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"device_classes": forms.SelectMultiple(choices=DeviceClasses.choices),
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.1.6 on 2021-02-16 08:38
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0016_auto_20201202_1307"),
|
||||
("authentik_stages_authenticator_validate", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name="OTPValidateStage",
|
||||
new_name="AuthenticatorValidateStage",
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="authenticatorvalidatestage",
|
||||
options={
|
||||
"verbose_name": "Authenticator Validation Stage",
|
||||
"verbose_name_plural": "Authenticator Validation Stages",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 3.1.6 on 2021-02-16 13:03
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import authentik.stages.authenticator_validate.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_authenticator_validate", "0002_auto_20210216_0838"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="authenticatorvalidatestage",
|
||||
name="device_classes",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(),
|
||||
default=authentik.stages.authenticator_validate.models.default_device_classes,
|
||||
help_text="Device classes which can be used to authenticate",
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,74 @@
|
|||
"""Authenticator Validation Stage"""
|
||||
from typing import Type
|
||||
|
||||
from django.contrib.postgres.fields.array import ArrayField
|
||||
from django.db import models
|
||||
from django.forms import ModelForm
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.flows.models import NotConfiguredAction, Stage
|
||||
|
||||
|
||||
class DeviceClasses(models.TextChoices):
|
||||
"""Device classes this stage can validate"""
|
||||
|
||||
STATIC = "static"
|
||||
TOTP = "totp", _("TOTP")
|
||||
WEBAUTHN = "webauthn", _("WebAuthn")
|
||||
|
||||
|
||||
def default_device_classes() -> list:
|
||||
"""By default, accept all device classes"""
|
||||
return [
|
||||
DeviceClasses.STATIC,
|
||||
DeviceClasses.TOTP,
|
||||
DeviceClasses.WEBAUTHN,
|
||||
]
|
||||
|
||||
|
||||
class AuthenticatorValidateStage(Stage):
|
||||
"""Validate user's configured OTP Device."""
|
||||
|
||||
not_configured_action = models.TextField(
|
||||
choices=NotConfiguredAction.choices, default=NotConfiguredAction.SKIP
|
||||
)
|
||||
|
||||
device_classes = ArrayField(
|
||||
models.TextField(),
|
||||
help_text=_("Device classes which can be used to authenticate"),
|
||||
default=default_device_classes,
|
||||
)
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
from authentik.stages.authenticator_validate.api import (
|
||||
AuthenticatorValidateStageSerializer,
|
||||
)
|
||||
|
||||
return AuthenticatorValidateStageSerializer
|
||||
|
||||
@property
|
||||
def type(self) -> Type[View]:
|
||||
from authentik.stages.authenticator_validate.stage import (
|
||||
AuthenticatorValidateStageView,
|
||||
)
|
||||
|
||||
return AuthenticatorValidateStageView
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from authentik.stages.authenticator_validate.forms import (
|
||||
AuthenticatorValidateStageForm,
|
||||
)
|
||||
|
||||
return AuthenticatorValidateStageForm
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Authenticator Validation Stage {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Authenticator Validation Stage")
|
||||
verbose_name_plural = _("Authenticator Validation Stages")
|
|
@ -9,13 +9,13 @@ from structlog.stdlib import get_logger
|
|||
from authentik.flows.models import NotConfiguredAction
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.stages.otp_validate.forms import ValidationForm
|
||||
from authentik.stages.otp_validate.models import OTPValidateStage
|
||||
from authentik.stages.authenticator_validate.forms import ValidationForm
|
||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class OTPValidateStageView(FormView, StageView):
|
||||
class AuthenticatorValidateStageView(FormView, StageView):
|
||||
"""OTP Validation"""
|
||||
|
||||
form_class = ValidationForm
|
||||
|
@ -31,11 +31,11 @@ class OTPValidateStageView(FormView, StageView):
|
|||
LOGGER.debug("No pending user, continuing")
|
||||
return self.executor.stage_ok()
|
||||
has_devices = user_has_device(user)
|
||||
stage: OTPValidateStage = self.executor.current_stage
|
||||
stage: AuthenticatorValidateStage = self.executor.current_stage
|
||||
|
||||
if not has_devices:
|
||||
if stage.not_configured_action == NotConfiguredAction.SKIP:
|
||||
LOGGER.debug("OTP not configured, skipping stage")
|
||||
LOGGER.debug("Authenticator not configured, skipping stage")
|
||||
return self.executor.stage_ok()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
"""AuthenticateWebAuthnStage API Views"""
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage
|
||||
|
||||
|
||||
class AuthenticateWebAuthnStageSerializer(StageSerializer):
|
||||
"""AuthenticateWebAuthnStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = AuthenticateWebAuthnStage
|
||||
fields = StageSerializer.Meta.fields
|
||||
|
||||
|
||||
class AuthenticateWebAuthnStageViewSet(ModelViewSet):
|
||||
"""AuthenticateWebAuthnStage Viewset"""
|
||||
|
||||
queryset = AuthenticateWebAuthnStage.objects.all()
|
||||
serializer_class = AuthenticateWebAuthnStageSerializer
|
|
@ -0,0 +1,11 @@
|
|||
"""authentik webauthn app config"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthentikStageAuthenticatorWebAuthnConfig(AppConfig):
|
||||
"""authentik webauthn config"""
|
||||
|
||||
name = "authentik.stages.authenticator_webauthn"
|
||||
label = "authentik_stages_authenticator_webauthn"
|
||||
verbose_name = "authentik Stages.Authenticator.WebAuthn"
|
||||
mountpoint = "-/user/authenticator/webauthn/"
|
|
@ -0,0 +1,17 @@
|
|||
"""Webauthn stage forms"""
|
||||
from django import forms
|
||||
|
||||
from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage
|
||||
|
||||
|
||||
class AuthenticateWebAuthnStageForm(forms.ModelForm):
|
||||
"""OTP Time-based Stage setup form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = AuthenticateWebAuthnStage
|
||||
fields = ["name"]
|
||||
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
# Generated by Django 3.1.6 on 2021-02-17 10:48
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("authentik_flows", "0016_auto_20201202_1307"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="WebAuthnDevice",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.TextField(max_length=200)),
|
||||
("credential_id", models.CharField(max_length=300, unique=True)),
|
||||
("public_key", models.TextField()),
|
||||
("sign_count", models.IntegerField(default=0)),
|
||||
("rp_id", models.CharField(max_length=253)),
|
||||
("created_on", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"last_used_on",
|
||||
models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="AuthenticateWebAuthnStage",
|
||||
fields=[
|
||||
(
|
||||
"stage_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_flows.stage",
|
||||
),
|
||||
),
|
||||
(
|
||||
"configure_flow",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="authentik_flows.flow",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "WebAuthn Authenticator Setup Stage",
|
||||
"verbose_name_plural": "WebAuthn Authenticator Setup Stages",
|
||||
},
|
||||
bases=("authentik_flows.stage", models.Model),
|
||||
),
|
||||
]
|
|
@ -11,28 +11,32 @@ def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
|
|||
Flow = apps.get_model("authentik_flows", "Flow")
|
||||
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
|
||||
|
||||
OTPStaticStage = apps.get_model("authentik_stages_otp_static", "OTPStaticStage")
|
||||
AuthenticateWebAuthnStage = apps.get_model(
|
||||
"authentik_stages_authenticator_webauthn", "AuthenticateWebAuthnStage"
|
||||
)
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
||||
slug="default-otp-static-configure",
|
||||
slug="default-authenticator-webuahtn-setup",
|
||||
designation=FlowDesignation.STAGE_CONFIGURATION,
|
||||
defaults={
|
||||
"name": "default-otp-static-configure",
|
||||
"name": "default-authenticator-webuahtn-setup",
|
||||
"title": "Setup Static OTP Tokens",
|
||||
},
|
||||
)
|
||||
|
||||
stage, _ = OTPStaticStage.objects.using(db_alias).update_or_create(
|
||||
name="default-otp-static-configure", defaults={"token_count": 6}
|
||||
stage, _ = AuthenticateWebAuthnStage.objects.using(db_alias).update_or_create(
|
||||
name="default-authenticator-webuahtn-setup"
|
||||
)
|
||||
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow, stage=stage, defaults={"order": 0}
|
||||
)
|
||||
|
||||
for stage in OTPStaticStage.objects.using(db_alias).filter(configure_flow=None):
|
||||
for stage in AuthenticateWebAuthnStage.objects.using(db_alias).filter(
|
||||
configure_flow=None
|
||||
):
|
||||
stage.configure_flow = flow
|
||||
stage.save()
|
||||
|
||||
|
@ -40,7 +44,10 @@ def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_otp_static", "0002_otpstaticstage_configure_flow"),
|
||||
(
|
||||
"authentik_stages_authenticator_webauthn",
|
||||
"0001_initial",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
|
@ -0,0 +1,80 @@
|
|||
"""WebAuthn stage"""
|
||||
from typing import Optional, Type
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.forms import ModelForm
|
||||
from django.shortcuts import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.flows.models import ConfigurableStage, Stage
|
||||
|
||||
|
||||
class AuthenticateWebAuthnStage(ConfigurableStage, Stage):
|
||||
"""WebAuthn stage"""
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
from authentik.stages.authenticator_webauthn.api import (
|
||||
AuthenticateWebAuthnStageSerializer,
|
||||
)
|
||||
|
||||
return AuthenticateWebAuthnStageSerializer
|
||||
|
||||
@property
|
||||
def type(self) -> Type[View]:
|
||||
from authentik.stages.authenticator_webauthn.stage import (
|
||||
AuthenticateWebAuthnStageView,
|
||||
)
|
||||
|
||||
return AuthenticateWebAuthnStageView
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from authentik.stages.authenticator_webauthn.forms import (
|
||||
AuthenticateWebAuthnStageForm,
|
||||
)
|
||||
|
||||
return AuthenticateWebAuthnStageForm
|
||||
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[str]:
|
||||
return reverse(
|
||||
"authentik_stages_authenticator_webauthn:user-settings",
|
||||
kwargs={"stage_uuid": self.stage_uuid},
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"WebAuthn Authenticator Setup Stage {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("WebAuthn Authenticator Setup Stage")
|
||||
verbose_name_plural = _("WebAuthn Authenticator Setup Stages")
|
||||
|
||||
|
||||
class WebAuthnDevice(models.Model):
|
||||
"""WebAuthn Device for a single user"""
|
||||
|
||||
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
||||
|
||||
name = models.TextField(max_length=200)
|
||||
credential_id = models.CharField(max_length=300, unique=True)
|
||||
public_key = models.TextField()
|
||||
sign_count = models.IntegerField(default=0)
|
||||
rp_id = models.CharField(max_length=253)
|
||||
|
||||
created_on = models.DateTimeField(auto_now_add=True)
|
||||
last_used_on = models.DateTimeField(default=now)
|
||||
|
||||
def set_sign_count(self, sign_count: int) -> None:
|
||||
"""Set the sign_count and update the last_used_on datetime."""
|
||||
self.sign_count = sign_count
|
||||
self.last_used_on = now()
|
||||
self.save()
|
||||
|
||||
def __str__(self):
|
||||
return self.name or str(self.user)
|
|
@ -0,0 +1,44 @@
|
|||
"""WebAuthn stage"""
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import FormView
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
SESSION_KEY_WEBAUTHN_AUTHENTICATED = (
|
||||
"authentik_stages_authenticator_webauthn_authenticated"
|
||||
)
|
||||
|
||||
|
||||
class AuthenticateWebAuthnStageView(FormView, StageView):
|
||||
"""WebAuthn stage"""
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
user = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER)
|
||||
if not user:
|
||||
LOGGER.debug("No pending user, continuing")
|
||||
return self.executor.stage_ok()
|
||||
devices = WebAuthnDevice.objects.filter(user=user)
|
||||
# If the current user is logged in already, or the pending user
|
||||
# has no devices, show setup
|
||||
if self.request.user == user:
|
||||
# Because the user is already authenticated, skip the later check
|
||||
self.request.session[SESSION_KEY_WEBAUTHN_AUTHENTICATED] = True
|
||||
return render(request, "stages/authenticator_webauthn/setup.html")
|
||||
if not devices.exists():
|
||||
return self.executor.stage_ok()
|
||||
self.request.session[SESSION_KEY_WEBAUTHN_AUTHENTICATED] = False
|
||||
return render(request, "stages/authenticator_webauthn/auth.html")
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Since the client can't directly indicate when a stage is done,
|
||||
we use the post handler for this"""
|
||||
if request.session.pop(SESSION_KEY_WEBAUTHN_AUTHENTICATED, False):
|
||||
return self.executor.stage_ok()
|
||||
return self.executor.stage_invalid()
|
|
@ -0,0 +1,15 @@
|
|||
{% load i18n %}
|
||||
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
{% trans 'WebAuthn' %}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
{% block card %}
|
||||
<div class="pf-c-form">
|
||||
<ak-stage-webauthn-auth>
|
||||
</ak-stage-webauthn-auth>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
|
@ -0,0 +1,16 @@
|
|||
{% load i18n %}
|
||||
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
{% trans 'Configure WebAuthn' %}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
{% block card %}
|
||||
<div class="pf-c-form">
|
||||
<ak-stage-webauthn-register>
|
||||
</ak-stage-webauthn-register>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__header pf-c-title pf-m-md">
|
||||
{% trans "WebAuthn Devices" %}
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<ul class="pf-c-data-list" role="list">
|
||||
{% for device in devices %}
|
||||
<li class="pf-c-data-list__item" aria-labelledby="data-list-basic-item-1">
|
||||
<div class="pf-c-data-list__item-row">
|
||||
<div class="pf-c-data-list__item-content">
|
||||
<div class="pf-c-data-list__cell">{{ device.name|default:"-" }}</div>
|
||||
<div class="pf-c-data-list__cell">
|
||||
{% blocktrans with created_on=device.created_on|naturaltime %}
|
||||
Created {{ created_on }}
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
{% if stage.configure_flow %}
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next=/%23{% url 'authentik_core:user-settings' %}"
|
||||
class="ak-root-link pf-c-button pf-m-primary">{% trans "Configure WebAuthn" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,38 @@
|
|||
"""WebAuthn urls"""
|
||||
from django.urls import path
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from authentik.stages.authenticator_webauthn.views import (
|
||||
BeginActivateView,
|
||||
BeginAssertion,
|
||||
UserSettingsView,
|
||||
VerifyAssertion,
|
||||
VerifyCredentialInfo,
|
||||
)
|
||||
|
||||
# TODO: Move to API views so we don't need csrf_exempt
|
||||
urlpatterns = [
|
||||
path(
|
||||
"begin-activate/",
|
||||
csrf_exempt(BeginActivateView.as_view()),
|
||||
name="activate-begin",
|
||||
),
|
||||
path(
|
||||
"begin-assertion/",
|
||||
csrf_exempt(BeginAssertion.as_view()),
|
||||
name="assertion-begin",
|
||||
),
|
||||
path(
|
||||
"verify-credential-info/",
|
||||
csrf_exempt(VerifyCredentialInfo.as_view()),
|
||||
name="credential-info-verify",
|
||||
),
|
||||
path(
|
||||
"verify-assertion/",
|
||||
csrf_exempt(VerifyAssertion.as_view()),
|
||||
name="assertion-verify",
|
||||
),
|
||||
path(
|
||||
"<uuid:stage_uuid>/settings/", UserSettingsView.as_view(), name="user-settings"
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
"""webauthn utils"""
|
||||
import base64
|
||||
import os
|
||||
|
||||
CHALLENGE_DEFAULT_BYTE_LEN = 32
|
||||
|
||||
|
||||
def generate_challenge(challenge_len=CHALLENGE_DEFAULT_BYTE_LEN):
|
||||
"""Generate a challenge of challenge_len bytes, Base64-encoded.
|
||||
We use URL-safe base64, but we *don't* strip the padding, so that
|
||||
the browser can decode it without too much hassle.
|
||||
Note that if we are doing byte comparisons with the challenge in collectedClientData
|
||||
later on, that value will not have padding, so we must remove the padding
|
||||
before storing the value in the session.
|
||||
"""
|
||||
# If we know Python 3.6 or greater is available, we could replace this with one
|
||||
# call to secrets.token_urlsafe
|
||||
challenge_bytes = os.urandom(challenge_len)
|
||||
challenge_base64 = base64.urlsafe_b64encode(challenge_bytes)
|
||||
# Python 2/3 compatibility: b64encode returns bytes only in newer Python versions
|
||||
if not isinstance(challenge_base64, str):
|
||||
challenge_base64 = challenge_base64.decode("utf-8")
|
||||
return challenge_base64
|
|
@ -0,0 +1,241 @@
|
|||
"""webauthn views"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views import View
|
||||
from django.views.generic import TemplateView
|
||||
from structlog.stdlib import get_logger
|
||||
from webauthn import (
|
||||
WebAuthnAssertionOptions,
|
||||
WebAuthnAssertionResponse,
|
||||
WebAuthnMakeCredentialOptions,
|
||||
WebAuthnRegistrationResponse,
|
||||
WebAuthnUser,
|
||||
)
|
||||
from webauthn.webauthn import (
|
||||
AuthenticationRejectedException,
|
||||
RegistrationRejectedException,
|
||||
WebAuthnUserDataMissing,
|
||||
)
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.lib.templatetags.authentik_utils import avatar
|
||||
from authentik.stages.authenticator_webauthn.models import (
|
||||
AuthenticateWebAuthnStage,
|
||||
WebAuthnDevice,
|
||||
)
|
||||
from authentik.stages.authenticator_webauthn.stage import (
|
||||
SESSION_KEY_WEBAUTHN_AUTHENTICATED,
|
||||
)
|
||||
from authentik.stages.authenticator_webauthn.utils import generate_challenge
|
||||
|
||||
LOGGER = get_logger()
|
||||
RP_ID = "localhost"
|
||||
RP_NAME = "authentik"
|
||||
ORIGIN = "http://localhost:8000"
|
||||
|
||||
|
||||
class FlowUserRequiredView(View):
|
||||
"""Base class for views which can only be called in the context of a flow."""
|
||||
|
||||
user: User
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
plan = request.session.get(SESSION_KEY_PLAN, None)
|
||||
if not plan:
|
||||
return HttpResponseBadRequest()
|
||||
self.user = plan.context.get(PLAN_CONTEXT_PENDING_USER)
|
||||
if not self.user:
|
||||
return HttpResponseBadRequest()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class BeginActivateView(FlowUserRequiredView):
|
||||
"""Initial device registration view"""
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Initial device registration view"""
|
||||
# clear session variables prior to starting a new registration
|
||||
request.session.pop("challenge", None)
|
||||
|
||||
challenge = generate_challenge(32)
|
||||
|
||||
# We strip the saved challenge of padding, so that we can do a byte
|
||||
# comparison on the URL-safe-without-padding challenge we get back
|
||||
# from the browser.
|
||||
# We will still pass the padded version down to the browser so that the JS
|
||||
# can decode the challenge into binary without too much trouble.
|
||||
request.session["challenge"] = challenge.rstrip("=")
|
||||
|
||||
make_credential_options = WebAuthnMakeCredentialOptions(
|
||||
challenge,
|
||||
RP_NAME,
|
||||
RP_ID,
|
||||
self.user.uid,
|
||||
self.user.username,
|
||||
self.user.name,
|
||||
avatar(self.user),
|
||||
)
|
||||
|
||||
return JsonResponse(make_credential_options.registration_dict)
|
||||
|
||||
|
||||
class VerifyCredentialInfo(FlowUserRequiredView):
|
||||
"""Finish device registration"""
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Finish device registration"""
|
||||
challenge = request.session["challenge"]
|
||||
|
||||
registration_response = request.POST
|
||||
trusted_attestation_cert_required = True
|
||||
self_attestation_permitted = True
|
||||
none_attestation_permitted = True
|
||||
|
||||
webauthn_registration_response = WebAuthnRegistrationResponse(
|
||||
RP_ID,
|
||||
ORIGIN,
|
||||
registration_response,
|
||||
challenge,
|
||||
trusted_attestation_cert_required=trusted_attestation_cert_required,
|
||||
self_attestation_permitted=self_attestation_permitted,
|
||||
none_attestation_permitted=none_attestation_permitted,
|
||||
uv_required=False,
|
||||
) # User Verification
|
||||
|
||||
try:
|
||||
webauthn_credential = webauthn_registration_response.verify()
|
||||
except RegistrationRejectedException as exc:
|
||||
LOGGER.warning("registration failed", exc=exc)
|
||||
return JsonResponse({"fail": "Registration failed. Error: {}".format(exc)})
|
||||
|
||||
# Step 17.
|
||||
#
|
||||
# Check that the credentialId is not yet registered to any other user.
|
||||
# If registration is requested for a credential that is already registered
|
||||
# to a different user, the Relying Party SHOULD fail this registration
|
||||
# ceremony, or it MAY decide to accept the registration, e.g. while deleting
|
||||
# the older registration.
|
||||
credential_id_exists = WebAuthnDevice.objects.filter(
|
||||
credential_id=webauthn_credential.credential_id
|
||||
).first()
|
||||
if credential_id_exists:
|
||||
return JsonResponse({"fail": "Credential ID already exists."}, status=401)
|
||||
|
||||
webauthn_credential.credential_id = str(
|
||||
webauthn_credential.credential_id, "utf-8"
|
||||
)
|
||||
webauthn_credential.public_key = str(webauthn_credential.public_key, "utf-8")
|
||||
existing_device = WebAuthnDevice.objects.filter(
|
||||
credential_id=webauthn_credential.credential_id
|
||||
).first()
|
||||
if not existing_device:
|
||||
user = WebAuthnDevice.objects.create(
|
||||
user=self.user,
|
||||
public_key=webauthn_credential.public_key,
|
||||
credential_id=webauthn_credential.credential_id,
|
||||
sign_count=webauthn_credential.sign_count,
|
||||
rp_id=RP_ID,
|
||||
)
|
||||
else:
|
||||
return JsonResponse({"fail": "User already exists."}, status=401)
|
||||
|
||||
LOGGER.debug("Successfully registered.", user=user)
|
||||
|
||||
return JsonResponse({"success": "User successfully registered."})
|
||||
|
||||
|
||||
class BeginAssertion(FlowUserRequiredView):
|
||||
"""Send the client a challenge that we'll check later"""
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Send the client a challenge that we'll check later"""
|
||||
request.session.pop("challenge", None)
|
||||
|
||||
challenge = generate_challenge(32)
|
||||
|
||||
# We strip the padding from the challenge stored in the session
|
||||
# for the reasons outlined in the comment in webauthn_begin_activate.
|
||||
request.session["challenge"] = challenge.rstrip("=")
|
||||
|
||||
devices = WebAuthnDevice.objects.filter(user=self.user)
|
||||
if not devices.exists():
|
||||
return HttpResponseBadRequest()
|
||||
device: WebAuthnDevice = devices.first()
|
||||
|
||||
webauthn_user = WebAuthnUser(
|
||||
self.user.uid,
|
||||
self.user.username,
|
||||
self.user.name,
|
||||
avatar(self.user),
|
||||
device.credential_id,
|
||||
device.public_key,
|
||||
device.sign_count,
|
||||
device.rp_id,
|
||||
)
|
||||
|
||||
webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge)
|
||||
|
||||
return JsonResponse(webauthn_assertion_options.assertion_dict)
|
||||
|
||||
|
||||
class VerifyAssertion(FlowUserRequiredView):
|
||||
"""Verify assertion result that we've sent to the client"""
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Verify assertion result that we've sent to the client"""
|
||||
challenge = request.session.get("challenge")
|
||||
assertion_response = request.POST
|
||||
credential_id = assertion_response.get("id")
|
||||
|
||||
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
|
||||
if not device:
|
||||
return JsonResponse({"fail": "Device does not exist."}, status=401)
|
||||
|
||||
webauthn_user = WebAuthnUser(
|
||||
self.user.uid,
|
||||
self.user.username,
|
||||
self.user.name,
|
||||
avatar(self.user),
|
||||
device.credential_id,
|
||||
device.public_key,
|
||||
device.sign_count,
|
||||
device.rp_id,
|
||||
)
|
||||
|
||||
webauthn_assertion_response = WebAuthnAssertionResponse(
|
||||
webauthn_user, assertion_response, challenge, ORIGIN, uv_required=False
|
||||
) # User Verification
|
||||
|
||||
try:
|
||||
sign_count = webauthn_assertion_response.verify()
|
||||
except (
|
||||
AuthenticationRejectedException,
|
||||
WebAuthnUserDataMissing,
|
||||
RegistrationRejectedException,
|
||||
) as exc:
|
||||
return JsonResponse({"fail": "Assertion failed. Error: {}".format(exc)})
|
||||
|
||||
device.set_sign_count(sign_count)
|
||||
request.session[SESSION_KEY_WEBAUTHN_AUTHENTICATED] = True
|
||||
return JsonResponse(
|
||||
{"success": "Successfully authenticated as {}".format(self.user.username)}
|
||||
)
|
||||
|
||||
|
||||
class UserSettingsView(LoginRequiredMixin, TemplateView):
|
||||
"""View for user settings to control WebAuthn devices"""
|
||||
|
||||
template_name = "stages/authenticator_webauthn/user_settings.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["devices"] = WebAuthnDevice.objects.filter(user=self.request.user)
|
||||
stage = get_object_or_404(
|
||||
AuthenticateWebAuthnStage, pk=self.kwargs["stage_uuid"]
|
||||
)
|
||||
kwargs["stage"] = stage
|
||||
return kwargs
|
|
@ -1,17 +1,17 @@
|
|||
"""CaptchaStage API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.captcha.models import CaptchaStage
|
||||
|
||||
|
||||
class CaptchaStageSerializer(ModelSerializer):
|
||||
class CaptchaStageSerializer(StageSerializer):
|
||||
"""CaptchaStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = CaptchaStage
|
||||
fields = ["pk", "name", "public_key", "private_key"]
|
||||
fields = StageSerializer.Meta.fields + ["public_key", "private_key"]
|
||||
|
||||
|
||||
class CaptchaStageViewSet(ModelViewSet):
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
"""ConsentStage API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.consent.models import ConsentStage
|
||||
|
||||
|
||||
class ConsentStageSerializer(ModelSerializer):
|
||||
class ConsentStageSerializer(StageSerializer):
|
||||
"""ConsentStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = ConsentStage
|
||||
fields = ["pk", "name", "mode", "consent_expire_in"]
|
||||
fields = StageSerializer.Meta.fields + ["mode", "consent_expire_in"]
|
||||
|
||||
|
||||
class ConsentStageViewSet(ModelViewSet):
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
"""DummyStage API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.dummy.models import DummyStage
|
||||
|
||||
|
||||
class DummyStageSerializer(ModelSerializer):
|
||||
class DummyStageSerializer(StageSerializer):
|
||||
"""DummyStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = DummyStage
|
||||
fields = ["pk", "name"]
|
||||
fields = StageSerializer.Meta.fields
|
||||
|
||||
|
||||
class DummyStageViewSet(ModelViewSet):
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
"""EmailStage API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.email.models import EmailStage, get_template_choices
|
||||
|
||||
|
||||
class EmailStageSerializer(ModelSerializer):
|
||||
class EmailStageSerializer(StageSerializer):
|
||||
"""EmailStage Serializer"""
|
||||
|
||||
def __init__(self, *args, **kwrags):
|
||||
super().__init__(*args, **kwrags)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["template"].choices = get_template_choices()
|
||||
|
||||
class Meta:
|
||||
|
||||
model = EmailStage
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"use_global_settings",
|
||||
"host",
|
||||
"port",
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
"""Identification Stage API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.identification.models import IdentificationStage
|
||||
|
||||
|
||||
class IdentificationStageSerializer(ModelSerializer):
|
||||
class IdentificationStageSerializer(StageSerializer):
|
||||
"""IdentificationStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = IdentificationStage
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"user_fields",
|
||||
"case_insensitive_matching",
|
||||
"show_matched_user",
|
||||
|
|
|
@ -2,18 +2,17 @@
|
|||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.invitation.models import Invitation, InvitationStage
|
||||
|
||||
|
||||
class InvitationStageSerializer(ModelSerializer):
|
||||
class InvitationStageSerializer(StageSerializer):
|
||||
"""InvitationStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = InvitationStage
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"continue_flow_without_invitation",
|
||||
]
|
||||
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
"""OTPStaticStage API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.stages.otp_static.models import OTPStaticStage
|
||||
|
||||
|
||||
class OTPStaticStageSerializer(ModelSerializer):
|
||||
"""OTPStaticStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = OTPStaticStage
|
||||
fields = ["pk", "name", "configure_flow", "token_count"]
|
||||
|
||||
|
||||
class OTPStaticStageViewSet(ModelViewSet):
|
||||
"""OTPStaticStage Viewset"""
|
||||
|
||||
queryset = OTPStaticStage.objects.all()
|
||||
serializer_class = OTPStaticStageSerializer
|
|
@ -1,11 +0,0 @@
|
|||
"""OTP Static stage"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthentikStageOTPStaticConfig(AppConfig):
|
||||
"""OTP Static stage"""
|
||||
|
||||
name = "authentik.stages.otp_static"
|
||||
label = "authentik_stages_otp_static"
|
||||
verbose_name = "authentik Stages.OTP.Static"
|
||||
mountpoint = "-/user/otp/static/"
|
|
@ -1,50 +0,0 @@
|
|||
"""OTP Static models"""
|
||||
from typing import Optional, Type
|
||||
|
||||
from django.db import models
|
||||
from django.forms import ModelForm
|
||||
from django.shortcuts import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.flows.models import ConfigurableStage, Stage
|
||||
|
||||
|
||||
class OTPStaticStage(ConfigurableStage, Stage):
|
||||
"""Generate static tokens for the user as a backup."""
|
||||
|
||||
token_count = models.IntegerField(default=6)
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
from authentik.stages.otp_static.api import OTPStaticStageSerializer
|
||||
|
||||
return OTPStaticStageSerializer
|
||||
|
||||
@property
|
||||
def type(self) -> Type[View]:
|
||||
from authentik.stages.otp_static.stage import OTPStaticStageView
|
||||
|
||||
return OTPStaticStageView
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from authentik.stages.otp_static.forms import OTPStaticStageForm
|
||||
|
||||
return OTPStaticStageForm
|
||||
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[str]:
|
||||
return reverse(
|
||||
"authentik_stages_otp_static:user-settings",
|
||||
kwargs={"stage_uuid": self.stage_uuid},
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"OTP Static Stage {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("OTP Static Setup Stage")
|
||||
verbose_name_plural = _("OTP Static Setup Stages")
|
|
@ -1,21 +0,0 @@
|
|||
"""OTPTimeStage API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.stages.otp_time.models import OTPTimeStage
|
||||
|
||||
|
||||
class OTPTimeStageSerializer(ModelSerializer):
|
||||
"""OTPTimeStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = OTPTimeStage
|
||||
fields = ["pk", "name", "configure_flow", "digits"]
|
||||
|
||||
|
||||
class OTPTimeStageViewSet(ModelViewSet):
|
||||
"""OTPTimeStage Viewset"""
|
||||
|
||||
queryset = OTPTimeStage.objects.all()
|
||||
serializer_class = OTPTimeStageSerializer
|
|
@ -1,11 +0,0 @@
|
|||
"""OTP Time"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthentikStageOTPTimeConfig(AppConfig):
|
||||
"""OTP time App config"""
|
||||
|
||||
name = "authentik.stages.otp_time"
|
||||
label = "authentik_stages_otp_time"
|
||||
verbose_name = "authentik Stages.OTP.Time"
|
||||
mountpoint = "-/user/otp/time/"
|
|
@ -1,24 +0,0 @@
|
|||
"""OTPValidateStage API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.stages.otp_validate.models import OTPValidateStage
|
||||
|
||||
|
||||
class OTPValidateStageSerializer(ModelSerializer):
|
||||
"""OTPValidateStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = OTPValidateStage
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
]
|
||||
|
||||
|
||||
class OTPValidateStageViewSet(ModelViewSet):
|
||||
"""OTPValidateStage Viewset"""
|
||||
|
||||
queryset = OTPValidateStage.objects.all()
|
||||
serializer_class = OTPValidateStageSerializer
|
|
@ -1,10 +0,0 @@
|
|||
"""OTP Validation Stage"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthentikStageOTPValidateConfig(AppConfig):
|
||||
"""OTP Validation Stage"""
|
||||
|
||||
name = "authentik.stages.otp_validate"
|
||||
label = "authentik_stages_otp_validate"
|
||||
verbose_name = "authentik Stages.OTP.Validate"
|
|
@ -1,44 +0,0 @@
|
|||
"""OTP Validation Stage"""
|
||||
from typing import Type
|
||||
|
||||
from django.db import models
|
||||
from django.forms import ModelForm
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.flows.models import NotConfiguredAction, Stage
|
||||
|
||||
|
||||
class OTPValidateStage(Stage):
|
||||
"""Validate user's configured OTP Device."""
|
||||
|
||||
not_configured_action = models.TextField(
|
||||
choices=NotConfiguredAction.choices, default=NotConfiguredAction.SKIP
|
||||
)
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
from authentik.stages.otp_validate.api import OTPValidateStageSerializer
|
||||
|
||||
return OTPValidateStageSerializer
|
||||
|
||||
@property
|
||||
def type(self) -> Type[View]:
|
||||
from authentik.stages.otp_validate.stage import OTPValidateStageView
|
||||
|
||||
return OTPValidateStageView
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from authentik.stages.otp_validate.forms import OTPValidateStageForm
|
||||
|
||||
return OTPValidateStageForm
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"OTP Validation Stage {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("OTP Validation Stage")
|
||||
verbose_name_plural = _("OTP Validation Stages")
|
|
@ -1,19 +1,17 @@
|
|||
"""PasswordStage API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.password.models import PasswordStage
|
||||
|
||||
|
||||
class PasswordStageSerializer(ModelSerializer):
|
||||
class PasswordStageSerializer(StageSerializer):
|
||||
"""PasswordStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = PasswordStage
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"backends",
|
||||
"configure_flow",
|
||||
"failed_attempts_before_cancel",
|
||||
|
|
|
@ -3,10 +3,11 @@ from rest_framework.serializers import CharField, ModelSerializer
|
|||
from rest_framework.validators import UniqueValidator
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.prompt.models import Prompt, PromptStage
|
||||
|
||||
|
||||
class PromptStageSerializer(ModelSerializer):
|
||||
class PromptStageSerializer(StageSerializer):
|
||||
"""PromptStage Serializer"""
|
||||
|
||||
name = CharField(validators=[UniqueValidator(queryset=PromptStage.objects.all())])
|
||||
|
@ -14,9 +15,7 @@ class PromptStageSerializer(ModelSerializer):
|
|||
class Meta:
|
||||
|
||||
model = PromptStage
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"fields",
|
||||
"validation_policies",
|
||||
]
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
"""User Delete Stage API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.user_delete.models import UserDeleteStage
|
||||
|
||||
|
||||
class UserDeleteStageSerializer(ModelSerializer):
|
||||
class UserDeleteStageSerializer(StageSerializer):
|
||||
"""UserDeleteStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = UserDeleteStage
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
]
|
||||
fields = StageSerializer.Meta.fields
|
||||
|
||||
|
||||
class UserDeleteStageViewSet(ModelViewSet):
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
"""Login Stage API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.user_login.models import UserLoginStage
|
||||
|
||||
|
||||
class UserLoginStageSerializer(ModelSerializer):
|
||||
class UserLoginStageSerializer(StageSerializer):
|
||||
"""UserLoginStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = UserLoginStage
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"session_duration",
|
||||
]
|
||||
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
"""Logout Stage API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.user_logout.models import UserLogoutStage
|
||||
|
||||
|
||||
class UserLogoutStageSerializer(ModelSerializer):
|
||||
class UserLogoutStageSerializer(StageSerializer):
|
||||
"""UserLogoutStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = UserLogoutStage
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
]
|
||||
fields = StageSerializer.Meta.fields
|
||||
|
||||
|
||||
class UserLogoutStageViewSet(ModelViewSet):
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
"""User Write Stage API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.flows.api import StageSerializer
|
||||
from authentik.stages.user_write.models import UserWriteStage
|
||||
|
||||
|
||||
class UserWriteStageSerializer(ModelSerializer):
|
||||
class UserWriteStageSerializer(StageSerializer):
|
||||
"""UserWriteStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = UserWriteStage
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
]
|
||||
fields = StageSerializer.Meta.fields
|
||||
|
||||
|
||||
class UserWriteStageViewSet(ModelViewSet):
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# flake8: noqa
|
||||
from lifecycle.migrate import BaseMigration
|
||||
|
||||
SQL_STATEMENT = """BEGIN TRANSACTION;
|
||||
ALTER TABLE authentik_stages_otp_static_otpstaticstage RENAME TO authentik_stages_authenticator_static_otpstaticstage;
|
||||
UPDATE django_migrations SET app = replace(app, 'authentik_stages_otp_static', 'authentik_stages_authenticator_static');
|
||||
UPDATE django_content_type SET app_label = replace(app_label, 'authentik_stages_otp_static', 'authentik_stages_authenticator_static');
|
||||
|
||||
ALTER TABLE authentik_stages_otp_time_otptimestage RENAME TO authentik_stages_authenticator_totp_otptimestage;
|
||||
UPDATE django_migrations SET app = replace(app, 'authentik_stages_otp_time', 'authentik_stages_authenticator_totp');
|
||||
UPDATE django_content_type SET app_label = replace(app_label, 'authentik_stages_otp_time', 'authentik_stages_authenticator_totp');
|
||||
|
||||
ALTER TABLE authentik_stages_otp_validate_otpvalidatestage RENAME TO authentik_stages_authenticator_validate_otpvalidatestage;
|
||||
UPDATE django_migrations SET app = replace(app, 'authentik_stages_otp_validate', 'authentik_stages_authenticator_validate');
|
||||
UPDATE django_content_type SET app_label = replace(app_label, 'authentik_stages_otp_validate', 'authentik_stages_authenticator_validate');
|
||||
|
||||
END TRANSACTION;"""
|
||||
|
||||
|
||||
class Migration(BaseMigration):
|
||||
def needs_migration(self) -> bool:
|
||||
self.cur.execute(
|
||||
"select * from information_schema.tables where table_name = 'authentik_stages_otp_static_otpstaticstage';"
|
||||
)
|
||||
return bool(self.cur.rowcount)
|
||||
|
||||
def run(self):
|
||||
self.cur.execute(SQL_STATEMENT)
|
||||
self.con.commit()
|
1301
swagger.yaml
1301
swagger.yaml
File diff suppressed because it is too large
Load Diff
|
@ -13,18 +13,18 @@ from selenium.webdriver.common.keys import Keys
|
|||
from selenium.webdriver.support import expected_conditions as ec
|
||||
|
||||
from authentik.flows.models import Flow, FlowStageBinding
|
||||
from authentik.stages.otp_static.models import OTPStaticStage
|
||||
from authentik.stages.otp_time.models import OTPTimeStage
|
||||
from authentik.stages.otp_validate.models import OTPValidateStage
|
||||
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
|
||||
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
|
||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
||||
from tests.e2e.utils import USER, SeleniumTestCase, retry
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestFlowsOTP(SeleniumTestCase):
|
||||
class TestFlowsAuthenticator(SeleniumTestCase):
|
||||
"""test flow with otp stages"""
|
||||
|
||||
@retry()
|
||||
def test_otp_validate(self):
|
||||
def test_totp_validate(self):
|
||||
"""test flow with otp stages"""
|
||||
sleep(1)
|
||||
# Setup TOTP Device
|
||||
|
@ -32,10 +32,8 @@ class TestFlowsOTP(SeleniumTestCase):
|
|||
device = TOTPDevice.objects.create(user=user, confirmed=True, digits=6)
|
||||
|
||||
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
||||
# Move the user_login stage to order 3
|
||||
FlowStageBinding.objects.filter(target=flow, order=2).update(order=3)
|
||||
FlowStageBinding.objects.create(
|
||||
target=flow, order=2, stage=OTPValidateStage.objects.create()
|
||||
target=flow, order=30, stage=AuthenticatorValidateStage.objects.create()
|
||||
)
|
||||
|
||||
self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
|
||||
|
@ -53,7 +51,7 @@ class TestFlowsOTP(SeleniumTestCase):
|
|||
self.assert_user(USER())
|
||||
|
||||
@retry()
|
||||
def test_otp_totp_setup(self):
|
||||
def test_totp_setup(self):
|
||||
"""test TOTP Setup stage"""
|
||||
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
||||
|
||||
|
@ -69,7 +67,7 @@ class TestFlowsOTP(SeleniumTestCase):
|
|||
self.driver.get(
|
||||
self.url(
|
||||
"authentik_flows:configure",
|
||||
stage_uuid=OTPTimeStage.objects.first().stage_uuid,
|
||||
stage_uuid=AuthenticatorTOTPStage.objects.first().stage_uuid,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -96,7 +94,7 @@ class TestFlowsOTP(SeleniumTestCase):
|
|||
self.assertTrue(TOTPDevice.objects.filter(user=USER(), confirmed=True).exists())
|
||||
|
||||
@retry()
|
||||
def test_otp_static_setup(self):
|
||||
def test_static_setup(self):
|
||||
"""test Static OTP Setup stage"""
|
||||
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
||||
|
||||
|
@ -112,7 +110,7 @@ class TestFlowsOTP(SeleniumTestCase):
|
|||
self.driver.get(
|
||||
self.url(
|
||||
"authentik_flows:configure",
|
||||
stage_uuid=OTPStaticStage.objects.first().stage_uuid,
|
||||
stage_uuid=AuthenticatorStaticStage.objects.first().stage_uuid,
|
||||
)
|
||||
)
|
||||
|
|
@ -725,6 +725,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
"@sentry/tracing": "^6.1.0",
|
||||
"@types/chart.js": "^2.9.30",
|
||||
"@types/codemirror": "0.0.108",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^2.9.4",
|
||||
"codemirror": "^5.59.2",
|
||||
"construct-style-sheets-polyfill": "^2.4.16",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import resolve from "rollup-plugin-node-resolve";
|
||||
import commonjs from "rollup-plugin-commonjs";
|
||||
import minifyHTML from "rollup-plugin-minify-html-literals";
|
||||
import { terser } from "rollup-plugin-terser";
|
||||
import sourcemaps from "rollup-plugin-sourcemaps";
|
||||
import typescript from "@rollup/plugin-typescript";
|
||||
|
@ -38,7 +37,6 @@ export default [
|
|||
resolve({ browser: true }),
|
||||
commonjs(),
|
||||
sourcemaps(),
|
||||
minifyHTML(),
|
||||
terser(),
|
||||
copy({
|
||||
targets: [...resources],
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue