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 = "*"
|
docker = "*"
|
||||||
xmlsec = "*"
|
xmlsec = "*"
|
||||||
geoip2 = "*"
|
geoip2 = "*"
|
||||||
|
webauthn = "*"
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.9"
|
python_version = "3.9"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "7151710a45e6ca0bd25335b14be005aa5179eb91de361de93686022c9b71c3d1"
|
"sha256": "933685b75680e3a06d2f523239d848b14d1507d385de42401863f4fb6345366c"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
@ -56,6 +56,7 @@
|
||||||
"sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245",
|
"sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245",
|
||||||
"sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1"
|
"sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==3.7.3"
|
"version": "==3.7.3"
|
||||||
},
|
},
|
||||||
"aioredis": {
|
"aioredis": {
|
||||||
|
@ -70,6 +71,7 @@
|
||||||
"sha256:1e759a7f202d910939de6eca45c23a107f6b71111f41d1282c648e9ac3d21901",
|
"sha256:1e759a7f202d910939de6eca45c23a107f6b71111f41d1282c648e9ac3d21901",
|
||||||
"sha256:affdd263d8b8eb3c98170b78bf83867cdb6a14901d586e00ddb65bfe2f0c4e60"
|
"sha256:affdd263d8b8eb3c98170b78bf83867cdb6a14901d586e00ddb65bfe2f0c4e60"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==5.0.5"
|
"version": "==5.0.5"
|
||||||
},
|
},
|
||||||
"asgiref": {
|
"asgiref": {
|
||||||
|
@ -77,6 +79,7 @@
|
||||||
"sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
|
"sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
|
||||||
"sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
|
"sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==3.3.1"
|
"version": "==3.3.1"
|
||||||
},
|
},
|
||||||
"async-timeout": {
|
"async-timeout": {
|
||||||
|
@ -84,6 +87,7 @@
|
||||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||||
],
|
],
|
||||||
|
"markers": "python_full_version >= '3.5.3'",
|
||||||
"version": "==3.0.1"
|
"version": "==3.0.1"
|
||||||
},
|
},
|
||||||
"attrs": {
|
"attrs": {
|
||||||
|
@ -91,6 +95,7 @@
|
||||||
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
|
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
|
||||||
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
|
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==20.3.0"
|
"version": "==20.3.0"
|
||||||
},
|
},
|
||||||
"autobahn": {
|
"autobahn": {
|
||||||
|
@ -98,6 +103,7 @@
|
||||||
"sha256:41a3a3f89cde48643baf4e105d9491c566295f9abee951379e59121784044b8b",
|
"sha256:41a3a3f89cde48643baf4e105d9491c566295f9abee951379e59121784044b8b",
|
||||||
"sha256:7e6b1bf95196b733978bab2d54a7ab8899c16ce11be369dc58422c07b7eea726"
|
"sha256:7e6b1bf95196b733978bab2d54a7ab8899c16ce11be369dc58422c07b7eea726"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==21.2.1"
|
"version": "==21.2.1"
|
||||||
},
|
},
|
||||||
"automat": {
|
"automat": {
|
||||||
|
@ -116,7 +122,8 @@
|
||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:877f204dabe1bfa21aa9cfaacc72bd4b70a897d0fdcea799afa5c4743b6fc7ac"
|
"sha256:877f204dabe1bfa21aa9cfaacc72bd4b70a897d0fdcea799afa5c4743b6fc7ac",
|
||||||
|
"sha256:3a8412020a59509e783755b5c9b910a4fc7f6b6f2b9473e7cd1e07b67672e0d1"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.17.9"
|
"version": "==1.17.9"
|
||||||
|
@ -126,6 +133,7 @@
|
||||||
"sha256:c8614c230e7a8e042a8c07d47caea50ad21cb51415289bd34fa6d0382beddad7",
|
"sha256:c8614c230e7a8e042a8c07d47caea50ad21cb51415289bd34fa6d0382beddad7",
|
||||||
"sha256:d725840b881be62fc52e8e24a6ada651128cf7f1ed1639b87322a7a213ffdbad"
|
"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"
|
"version": "==1.20.9"
|
||||||
},
|
},
|
||||||
"cachetools": {
|
"cachetools": {
|
||||||
|
@ -133,8 +141,15 @@
|
||||||
"sha256:1d9d5f567be80f7c07d765e21b814326d78c61eb0c3a637dffc0e5d1796cb2e2",
|
"sha256:1d9d5f567be80f7c07d765e21b814326d78c61eb0c3a637dffc0e5d1796cb2e2",
|
||||||
"sha256:f469e29e7aa4cff64d8de4aad95ce76de8ea1125a16c68e0d93f65c3c3dc92e9"
|
"sha256:f469e29e7aa4cff64d8de4aad95ce76de8ea1125a16c68e0d93f65c3c3dc92e9"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version ~= '3.5'",
|
||||||
"version": "==4.2.1"
|
"version": "==4.2.1"
|
||||||
},
|
},
|
||||||
|
"cbor2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a33aa2e5534fd74401ac95686886e655e3b2ce6383b3f958199b6e70a87c94bf"
|
||||||
|
],
|
||||||
|
"version": "==5.2.0"
|
||||||
|
},
|
||||||
"celery": {
|
"celery": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5e8d364e058554e83bbb116e8377d90c79be254785f357cb2cec026e79febe13",
|
"sha256:5e8d364e058554e83bbb116e8377d90c79be254785f357cb2cec026e79febe13",
|
||||||
|
@ -220,6 +235,7 @@
|
||||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
"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"
|
"version": "==7.1.2"
|
||||||
},
|
},
|
||||||
"click-didyoumean": {
|
"click-didyoumean": {
|
||||||
|
@ -288,6 +304,7 @@
|
||||||
"sha256:0052c9887600c57054a5867d4b0240159fa009faa3bcf6a1627271d9cdcb005a",
|
"sha256:0052c9887600c57054a5867d4b0240159fa009faa3bcf6a1627271d9cdcb005a",
|
||||||
"sha256:c22b692707f514de9013651ecb687f2abe4f35cf6fe292ece634e9f1737bc7e3"
|
"sha256:c22b692707f514de9013651ecb687f2abe4f35cf6fe292ece634e9f1737bc7e3"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==3.0.1"
|
"version": "==3.0.1"
|
||||||
},
|
},
|
||||||
"defusedxml": {
|
"defusedxml": {
|
||||||
|
@ -428,6 +445,7 @@
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==0.18.2"
|
"version": "==0.18.2"
|
||||||
},
|
},
|
||||||
"geoip2": {
|
"geoip2": {
|
||||||
|
@ -443,6 +461,7 @@
|
||||||
"sha256:1b461d079b5650efe492a7814e95c536ffa9e7a96e39a6d16189c1604f18554f",
|
"sha256:1b461d079b5650efe492a7814e95c536ffa9e7a96e39a6d16189c1604f18554f",
|
||||||
"sha256:8ce6862cf4e9252de10045f05fa80393fde831da9c2b45c39288edeee3cde7f2"
|
"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"
|
"version": "==1.26.1"
|
||||||
},
|
},
|
||||||
"gunicorn": {
|
"gunicorn": {
|
||||||
|
@ -458,6 +477,7 @@
|
||||||
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
|
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
|
||||||
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
|
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==0.12.0"
|
"version": "==0.12.0"
|
||||||
},
|
},
|
||||||
"hiredis": {
|
"hiredis": {
|
||||||
|
@ -509,6 +529,7 @@
|
||||||
"sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390",
|
"sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390",
|
||||||
"sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298"
|
"sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==1.1.0"
|
"version": "==1.1.0"
|
||||||
},
|
},
|
||||||
"httptools": {
|
"httptools": {
|
||||||
|
@ -554,6 +575,7 @@
|
||||||
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
||||||
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
|
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==0.5.1"
|
"version": "==0.5.1"
|
||||||
},
|
},
|
||||||
"itypes": {
|
"itypes": {
|
||||||
|
@ -568,6 +590,7 @@
|
||||||
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419",
|
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419",
|
||||||
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"
|
"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"
|
"version": "==2.11.3"
|
||||||
},
|
},
|
||||||
"jmespath": {
|
"jmespath": {
|
||||||
|
@ -575,6 +598,7 @@
|
||||||
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
|
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
|
||||||
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
|
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==0.10.0"
|
"version": "==0.10.0"
|
||||||
},
|
},
|
||||||
"jsonschema": {
|
"jsonschema": {
|
||||||
|
@ -589,6 +613,7 @@
|
||||||
"sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006",
|
"sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006",
|
||||||
"sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c"
|
"sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==5.0.2"
|
"version": "==5.0.2"
|
||||||
},
|
},
|
||||||
"kubernetes": {
|
"kubernetes": {
|
||||||
|
@ -601,8 +626,11 @@
|
||||||
},
|
},
|
||||||
"ldap3": {
|
"ldap3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91",
|
"sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57",
|
||||||
"sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57"
|
"sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056",
|
||||||
|
"sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59",
|
||||||
|
"sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c",
|
||||||
|
"sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.9"
|
"version": "==2.9"
|
||||||
|
@ -705,12 +733,14 @@
|
||||||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
|
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
|
||||||
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
|
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==1.1.1"
|
"version": "==1.1.1"
|
||||||
},
|
},
|
||||||
"maxminddb": {
|
"maxminddb": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
|
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==2.0.3"
|
"version": "==2.0.3"
|
||||||
},
|
},
|
||||||
"msgpack": {
|
"msgpack": {
|
||||||
|
@ -786,6 +816,7 @@
|
||||||
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
|
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
|
||||||
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
|
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==5.1.0"
|
"version": "==5.1.0"
|
||||||
},
|
},
|
||||||
"oauthlib": {
|
"oauthlib": {
|
||||||
|
@ -793,6 +824,7 @@
|
||||||
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
|
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
|
||||||
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
|
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==3.1.0"
|
"version": "==3.1.0"
|
||||||
},
|
},
|
||||||
"packaging": {
|
"packaging": {
|
||||||
|
@ -815,6 +847,7 @@
|
||||||
"sha256:0fa02fa80363844a4ab4b8d6891f62dd0645ba672723130423ca4037b80c1974",
|
"sha256:0fa02fa80363844a4ab4b8d6891f62dd0645ba672723130423ca4037b80c1974",
|
||||||
"sha256:62c811e46bd09130fb11ab759012a4ae385ce4fb2073442d1898867a824183bd"
|
"sha256:62c811e46bd09130fb11ab759012a4ae385ce4fb2073442d1898867a824183bd"
|
||||||
],
|
],
|
||||||
|
"markers": "python_full_version >= '3.6.1'",
|
||||||
"version": "==3.0.16"
|
"version": "==3.0.16"
|
||||||
},
|
},
|
||||||
"psycopg2-binary": {
|
"psycopg2-binary": {
|
||||||
|
@ -860,15 +893,37 @@
|
||||||
},
|
},
|
||||||
"pyasn1": {
|
"pyasn1": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
|
||||||
|
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
|
||||||
|
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
|
||||||
|
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
|
||||||
|
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
|
||||||
|
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
|
||||||
|
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
|
||||||
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
|
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
|
||||||
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
|
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
|
||||||
|
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
|
||||||
|
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
|
||||||
|
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3",
|
||||||
|
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"
|
||||||
],
|
],
|
||||||
"version": "==0.4.8"
|
"version": "==0.4.8"
|
||||||
},
|
},
|
||||||
"pyasn1-modules": {
|
"pyasn1-modules": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
"sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
|
||||||
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
|
"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"
|
"version": "==0.2.8"
|
||||||
},
|
},
|
||||||
|
@ -877,6 +932,7 @@
|
||||||
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
|
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
|
||||||
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
|
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==2.20"
|
"version": "==2.20"
|
||||||
},
|
},
|
||||||
"pycryptodome": {
|
"pycryptodome": {
|
||||||
|
@ -948,6 +1004,7 @@
|
||||||
"sha256:f933ecf4cb736c7af60a6a533db2bf569717f2318b265f92907acff1db43bc34",
|
"sha256:f933ecf4cb736c7af60a6a533db2bf569717f2318b265f92907acff1db43bc34",
|
||||||
"sha256:fc9c55dc1ed57db76595f2d19a479fc1c3a1be2c9da8de798a93d286c5f65f38"
|
"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"
|
"version": "==3.10.1"
|
||||||
},
|
},
|
||||||
"pyhamcrest": {
|
"pyhamcrest": {
|
||||||
|
@ -955,6 +1012,7 @@
|
||||||
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
|
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
|
||||||
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
|
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==2.0.2"
|
"version": "==2.0.2"
|
||||||
},
|
},
|
||||||
"pyjwkest": {
|
"pyjwkest": {
|
||||||
|
@ -976,12 +1034,14 @@
|
||||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==2.4.7"
|
"version": "==2.4.7"
|
||||||
},
|
},
|
||||||
"pyrsistent": {
|
"pyrsistent": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
|
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==0.17.3"
|
"version": "==0.17.3"
|
||||||
},
|
},
|
||||||
"python-dateutil": {
|
"python-dateutil": {
|
||||||
|
@ -989,6 +1049,7 @@
|
||||||
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
|
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
|
||||||
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
|
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==2.8.1"
|
"version": "==2.8.1"
|
||||||
},
|
},
|
||||||
"python-dotenv": {
|
"python-dotenv": {
|
||||||
|
@ -1045,6 +1106,7 @@
|
||||||
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
|
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
|
||||||
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
|
"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"
|
"version": "==3.5.3"
|
||||||
},
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
|
@ -1052,11 +1114,13 @@
|
||||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
"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"
|
"version": "==2.25.1"
|
||||||
},
|
},
|
||||||
"requests-oauthlib": {
|
"requests-oauthlib": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
|
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
|
||||||
|
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc",
|
||||||
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
|
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
|
@ -1105,6 +1169,7 @@
|
||||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==1.15.0"
|
"version": "==1.15.0"
|
||||||
},
|
},
|
||||||
"sqlparse": {
|
"sqlparse": {
|
||||||
|
@ -1112,6 +1177,7 @@
|
||||||
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
||||||
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==0.4.1"
|
"version": "==0.4.1"
|
||||||
},
|
},
|
||||||
"structlog": {
|
"structlog": {
|
||||||
|
@ -1159,6 +1225,7 @@
|
||||||
"sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467",
|
"sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467",
|
||||||
"sha256:f3c19e5bd42bbe4bf345704ad7c326c74d3fd7a1b3844987853bef180be638d4"
|
"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"
|
"version": "==20.3.0"
|
||||||
},
|
},
|
||||||
"txaio": {
|
"txaio": {
|
||||||
|
@ -1166,6 +1233,7 @@
|
||||||
"sha256:1488d31d564a116538cc1265ac3f7979fb6223bb5a9e9f1479436ee2c17d8549",
|
"sha256:1488d31d564a116538cc1265ac3f7979fb6223bb5a9e9f1479436ee2c17d8549",
|
||||||
"sha256:a8676d6c68aea1f0e2548c4afdb8e6253873af3bc2659bb5bcd9f39dff7ff90f"
|
"sha256:a8676d6c68aea1f0e2548c4afdb8e6253873af3bc2659bb5bcd9f39dff7ff90f"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==20.12.1"
|
"version": "==20.12.1"
|
||||||
},
|
},
|
||||||
"typing-extensions": {
|
"typing-extensions": {
|
||||||
|
@ -1181,6 +1249,7 @@
|
||||||
"sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
|
"sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
|
||||||
"sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
|
"sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==3.0.1"
|
"version": "==3.0.1"
|
||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
|
@ -1225,6 +1294,7 @@
|
||||||
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
|
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
|
||||||
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
|
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==5.0.0"
|
"version": "==5.0.0"
|
||||||
},
|
},
|
||||||
"watchgod": {
|
"watchgod": {
|
||||||
|
@ -1241,6 +1311,14 @@
|
||||||
],
|
],
|
||||||
"version": "==0.2.5"
|
"version": "==0.2.5"
|
||||||
},
|
},
|
||||||
|
"webauthn": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:238391b2e2cc60fb51a2cd2d2d6be149920b9af6184651353d9f95856617a9e7",
|
||||||
|
"sha256:8ad9072ff1d6169f3be30d4dc8733ea563dd266962397bc58b40f674a6af74ac"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.4.7"
|
||||||
|
},
|
||||||
"websocket-client": {
|
"websocket-client": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549",
|
"sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549",
|
||||||
|
@ -1334,6 +1412,7 @@
|
||||||
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
|
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
|
||||||
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
|
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==1.6.3"
|
"version": "==1.6.3"
|
||||||
},
|
},
|
||||||
"zope.interface": {
|
"zope.interface": {
|
||||||
|
@ -1391,6 +1470,7 @@
|
||||||
"sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd",
|
"sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd",
|
||||||
"sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537"
|
"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"
|
"version": "==5.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1407,6 +1487,7 @@
|
||||||
"sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
|
"sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
|
||||||
"sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
|
"sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==3.3.1"
|
"version": "==3.3.1"
|
||||||
},
|
},
|
||||||
"astroid": {
|
"astroid": {
|
||||||
|
@ -1414,6 +1495,7 @@
|
||||||
"sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1",
|
"sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1",
|
||||||
"sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38"
|
"sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==2.4.1"
|
"version": "==2.4.1"
|
||||||
},
|
},
|
||||||
"attrs": {
|
"attrs": {
|
||||||
|
@ -1421,6 +1503,7 @@
|
||||||
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
|
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
|
||||||
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
|
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==20.3.0"
|
"version": "==20.3.0"
|
||||||
},
|
},
|
||||||
"autopep8": {
|
"autopep8": {
|
||||||
|
@ -1451,6 +1534,7 @@
|
||||||
"sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410",
|
"sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410",
|
||||||
"sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"
|
"sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==1.0.1"
|
"version": "==1.0.1"
|
||||||
},
|
},
|
||||||
"bumpversion": {
|
"bumpversion": {
|
||||||
|
@ -1466,6 +1550,7 @@
|
||||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
"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"
|
"version": "==7.1.2"
|
||||||
},
|
},
|
||||||
"colorama": {
|
"colorama": {
|
||||||
|
@ -1559,6 +1644,7 @@
|
||||||
"sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839",
|
"sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839",
|
||||||
"sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"
|
"sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==3.8.4"
|
"version": "==3.8.4"
|
||||||
},
|
},
|
||||||
"flake8-polyfill": {
|
"flake8-polyfill": {
|
||||||
|
@ -1573,6 +1659,7 @@
|
||||||
"sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac",
|
"sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac",
|
||||||
"sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"
|
"sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.4'",
|
||||||
"version": "==4.0.5"
|
"version": "==4.0.5"
|
||||||
},
|
},
|
||||||
"gitpython": {
|
"gitpython": {
|
||||||
|
@ -1580,6 +1667,7 @@
|
||||||
"sha256:8621a7e777e276a5ec838b59280ba5272dd144a18169c36c903d8b38b99f750a",
|
"sha256:8621a7e777e276a5ec838b59280ba5272dd144a18169c36c903d8b38b99f750a",
|
||||||
"sha256:c5347c81d232d9b8e7f47b68a83e5dc92e7952127133c5f2df9133f2c75a1b29"
|
"sha256:c5347c81d232d9b8e7f47b68a83e5dc92e7952127133c5f2df9133f2c75a1b29"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.4'",
|
||||||
"version": "==3.1.13"
|
"version": "==3.1.13"
|
||||||
},
|
},
|
||||||
"iniconfig": {
|
"iniconfig": {
|
||||||
|
@ -1594,6 +1682,7 @@
|
||||||
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
|
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
|
||||||
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
|
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==4.3.21"
|
"version": "==4.3.21"
|
||||||
},
|
},
|
||||||
"lazy-object-proxy": {
|
"lazy-object-proxy": {
|
||||||
|
@ -1620,6 +1709,7 @@
|
||||||
"sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4",
|
"sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4",
|
||||||
"sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"
|
"sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==1.4.3"
|
"version": "==1.4.3"
|
||||||
},
|
},
|
||||||
"mccabe": {
|
"mccabe": {
|
||||||
|
@ -1656,6 +1746,7 @@
|
||||||
"sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9",
|
"sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9",
|
||||||
"sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"
|
"sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.6'",
|
||||||
"version": "==5.5.1"
|
"version": "==5.5.1"
|
||||||
},
|
},
|
||||||
"pep8-naming": {
|
"pep8-naming": {
|
||||||
|
@ -1670,6 +1761,7 @@
|
||||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==0.13.1"
|
"version": "==0.13.1"
|
||||||
},
|
},
|
||||||
"prospector": {
|
"prospector": {
|
||||||
|
@ -1684,6 +1776,7 @@
|
||||||
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
|
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
|
||||||
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
|
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==1.10.0"
|
"version": "==1.10.0"
|
||||||
},
|
},
|
||||||
"pycodestyle": {
|
"pycodestyle": {
|
||||||
|
@ -1691,6 +1784,7 @@
|
||||||
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
|
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
|
||||||
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
|
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==2.6.0"
|
"version": "==2.6.0"
|
||||||
},
|
},
|
||||||
"pydocstyle": {
|
"pydocstyle": {
|
||||||
|
@ -1698,6 +1792,7 @@
|
||||||
"sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325",
|
"sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325",
|
||||||
"sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"
|
"sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==5.1.1"
|
"version": "==5.1.1"
|
||||||
},
|
},
|
||||||
"pyflakes": {
|
"pyflakes": {
|
||||||
|
@ -1705,6 +1800,7 @@
|
||||||
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
|
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
|
||||||
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
|
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==2.2.0"
|
"version": "==2.2.0"
|
||||||
},
|
},
|
||||||
"pylint": {
|
"pylint": {
|
||||||
|
@ -1747,6 +1843,7 @@
|
||||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==2.4.7"
|
"version": "==2.4.7"
|
||||||
},
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
|
@ -1870,6 +1967,7 @@
|
||||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==1.15.0"
|
"version": "==1.15.0"
|
||||||
},
|
},
|
||||||
"smmap": {
|
"smmap": {
|
||||||
|
@ -1877,6 +1975,7 @@
|
||||||
"sha256:7bfcf367828031dc893530a29cb35eb8c8f2d7c8f2d0989354d75d24c8573714",
|
"sha256:7bfcf367828031dc893530a29cb35eb8c8f2d7c8f2d0989354d75d24c8573714",
|
||||||
"sha256:84c2751ef3072d4f6b2785ec7ee40244c6f45eb934d9e543e2c51f1bd3d54c50"
|
"sha256:84c2751ef3072d4f6b2785ec7ee40244c6f45eb934d9e543e2c51f1bd3d54c50"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==3.0.5"
|
"version": "==3.0.5"
|
||||||
},
|
},
|
||||||
"snowballstemmer": {
|
"snowballstemmer": {
|
||||||
|
@ -1891,6 +1990,7 @@
|
||||||
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
||||||
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==0.4.1"
|
"version": "==0.4.1"
|
||||||
},
|
},
|
||||||
"stevedore": {
|
"stevedore": {
|
||||||
|
@ -1898,6 +1998,7 @@
|
||||||
"sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee",
|
"sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee",
|
||||||
"sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"
|
"sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==3.3.0"
|
"version": "==3.3.0"
|
||||||
},
|
},
|
||||||
"toml": {
|
"toml": {
|
||||||
|
@ -1905,6 +2006,7 @@
|
||||||
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||||
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==0.10.2"
|
"version": "==0.10.2"
|
||||||
},
|
},
|
||||||
"typed-ast": {
|
"typed-ast": {
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
<td role="cell">
|
<td role="cell">
|
||||||
<ul>
|
<ul>
|
||||||
{% for flow in stage.flow_set.all %}
|
{% for flow in stage.flow_set.all %}
|
||||||
<li>{{ flow.slug }}<</li>
|
<li>{{ flow.slug }}</li>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<li>-</li>
|
<li>-</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -57,15 +57,18 @@ from authentik.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProvide
|
||||||
from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
||||||
from authentik.sources.oauth.api import OAuthSourceViewSet
|
from authentik.sources.oauth.api import OAuthSourceViewSet
|
||||||
from authentik.sources.saml.api import SAMLSourceViewSet
|
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.captcha.api import CaptchaStageViewSet
|
||||||
from authentik.stages.consent.api import ConsentStageViewSet
|
from authentik.stages.consent.api import ConsentStageViewSet
|
||||||
from authentik.stages.dummy.api import DummyStageViewSet
|
from authentik.stages.dummy.api import DummyStageViewSet
|
||||||
from authentik.stages.email.api import EmailStageViewSet
|
from authentik.stages.email.api import EmailStageViewSet
|
||||||
from authentik.stages.identification.api import IdentificationStageViewSet
|
from authentik.stages.identification.api import IdentificationStageViewSet
|
||||||
from authentik.stages.invitation.api import InvitationStageViewSet, InvitationViewSet
|
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.password.api import PasswordStageViewSet
|
||||||
from authentik.stages.prompt.api import PromptStageViewSet, PromptViewSet
|
from authentik.stages.prompt.api import PromptStageViewSet, PromptViewSet
|
||||||
from authentik.stages.user_delete.api import UserDeleteStageViewSet
|
from authentik.stages.user_delete.api import UserDeleteStageViewSet
|
||||||
|
@ -134,15 +137,16 @@ router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
||||||
router.register("propertymappings/scope", ScopeMappingViewSet)
|
router.register("propertymappings/scope", ScopeMappingViewSet)
|
||||||
|
|
||||||
router.register("stages/all", StageViewSet)
|
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/captcha", CaptchaStageViewSet)
|
||||||
router.register("stages/consent", ConsentStageViewSet)
|
router.register("stages/consent", ConsentStageViewSet)
|
||||||
router.register("stages/email", EmailStageViewSet)
|
router.register("stages/email", EmailStageViewSet)
|
||||||
router.register("stages/identification", IdentificationStageViewSet)
|
router.register("stages/identification", IdentificationStageViewSet)
|
||||||
router.register("stages/invitation", InvitationStageViewSet)
|
router.register("stages/invitation", InvitationStageViewSet)
|
||||||
router.register("stages/invitation/invitations", InvitationViewSet)
|
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/password", PasswordStageViewSet)
|
||||||
router.register("stages/prompt/prompts", PromptViewSet)
|
router.register("stages/prompt/prompts", PromptViewSet)
|
||||||
router.register("stages/prompt/stages", PromptStageViewSet)
|
router.register("stages/prompt/stages", PromptStageViewSet)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from dataclasses import dataclass
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Model
|
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 drf_yasg2.utils import swagger_auto_schema
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
@ -18,8 +18,11 @@ from rest_framework.serializers import (
|
||||||
)
|
)
|
||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
|
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.models import Flow, FlowStageBinding, Stage
|
||||||
from authentik.flows.planner import cache_key
|
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):
|
class FlowSerializer(ModelSerializer):
|
||||||
|
@ -154,24 +157,19 @@ class FlowViewSet(ModelViewSet):
|
||||||
return Response({"diagram": diagram})
|
return Response({"diagram": diagram})
|
||||||
|
|
||||||
|
|
||||||
class StageSerializer(ModelSerializer):
|
class StageSerializer(ModelSerializer, MetaNameSerializer):
|
||||||
"""Stage Serializer"""
|
"""Stage Serializer"""
|
||||||
|
|
||||||
__type__ = SerializerMethodField(method_name="get_type")
|
object_type = SerializerMethodField()
|
||||||
verbose_name = SerializerMethodField(method_name="get_verbose_name")
|
|
||||||
|
|
||||||
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"""
|
"""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", "")
|
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:
|
class Meta:
|
||||||
|
|
||||||
model = Stage
|
model = Stage
|
||||||
fields = ["pk", "name", "__type__", "verbose_name"]
|
fields = ["pk", "name", "object_type", "verbose_name", "verbose_name_plural"]
|
||||||
|
|
||||||
|
|
||||||
class StageViewSet(ReadOnlyModelViewSet):
|
class StageViewSet(ReadOnlyModelViewSet):
|
||||||
|
@ -183,6 +181,23 @@ class StageViewSet(ReadOnlyModelViewSet):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Stage.objects.select_subclasses()
|
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):
|
class FlowStageBindingSerializer(ModelSerializer):
|
||||||
"""FlowStageBinding Serializer"""
|
"""FlowStageBinding Serializer"""
|
||||||
|
|
|
@ -50,21 +50,21 @@ def create_default_authentication_flow(
|
||||||
target=flow,
|
target=flow,
|
||||||
stage=identification_stage,
|
stage=identification_stage,
|
||||||
defaults={
|
defaults={
|
||||||
"order": 0,
|
"order": 10,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||||
target=flow,
|
target=flow,
|
||||||
stage=password_stage,
|
stage=password_stage,
|
||||||
defaults={
|
defaults={
|
||||||
"order": 1,
|
"order": 20,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||||
target=flow,
|
target=flow,
|
||||||
stage=login_stage,
|
stage=login_stage,
|
||||||
defaults={
|
defaults={
|
||||||
"order": 2,
|
"order": 100,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ class TestFlowsAPI(APITestCase):
|
||||||
def test_api_serializer(self):
|
def test_api_serializer(self):
|
||||||
"""Test that stage serializer returns the correct type"""
|
"""Test that stage serializer returns the correct type"""
|
||||||
obj = DummyStage()
|
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")
|
self.assertEqual(StageSerializer().get_verbose_name(obj), "Dummy Stage")
|
||||||
|
|
||||||
def test_api_viewset(self):
|
def test_api_viewset(self):
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
"""transfer common classes"""
|
"""transfer common classes"""
|
||||||
from dataclasses import asdict, dataclass, field, is_dataclass
|
from dataclasses import asdict, dataclass, field, is_dataclass
|
||||||
from json.encoder import JSONEncoder
|
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
from authentik.lib.models import SerializerModel
|
from authentik.lib.models import SerializerModel
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
|
|
||||||
|
@ -11,7 +12,17 @@ from authentik.lib.sentry import SentryIgnoredException
|
||||||
def get_attrs(obj: SerializerModel) -> Dict[str, Any]:
|
def get_attrs(obj: SerializerModel) -> Dict[str, Any]:
|
||||||
"""Get object's attributes via their serializer, and covert it to a normal dict"""
|
"""Get object's attributes via their serializer, and covert it to a normal dict"""
|
||||||
data = dict(obj.serializer(obj).data)
|
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:
|
for to_remove_name in to_remove:
|
||||||
if to_remove_name in data:
|
if to_remove_name in data:
|
||||||
data.pop(to_remove_name)
|
data.pop(to_remove_name)
|
||||||
|
@ -53,7 +64,7 @@ class FlowBundle:
|
||||||
entries: List[FlowBundleEntry] = field(default_factory=list)
|
entries: List[FlowBundleEntry] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class DataclassEncoder(JSONEncoder):
|
class DataclassEncoder(DjangoJSONEncoder):
|
||||||
"""Convert FlowBundleEntry to json"""
|
"""Convert FlowBundleEntry to json"""
|
||||||
|
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
|
|
|
@ -98,4 +98,5 @@ class FlowExporter:
|
||||||
|
|
||||||
def export_to_string(self) -> str:
|
def export_to_string(self) -> str:
|
||||||
"""Call export and convert it to json"""
|
"""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_logout", "authentik Stages.User Logout"),
|
||||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
("authentik.stages.user_write", "authentik Stages.User Write"),
|
||||||
("authentik.stages.otp_static", "authentik OTP.Static"),
|
("authentik.stages.otp_static", "authentik OTP.Static"),
|
||||||
("authentik.stages.otp_time", "authentik OTP.Time"),
|
("authentik.stages.authenticator_totp", "authentik OTP.Time"),
|
||||||
("authentik.stages.otp_validate", "authentik OTP.Validate"),
|
(
|
||||||
|
"authentik.stages.authenticator_validate",
|
||||||
|
"authentik OTP.Validate",
|
||||||
|
),
|
||||||
("authentik.stages.password", "authentik Stages.Password"),
|
("authentik.stages.password", "authentik Stages.Password"),
|
||||||
("authentik.core", "authentik Core"),
|
("authentik.core", "authentik Core"),
|
||||||
],
|
],
|
||||||
|
|
|
@ -67,8 +67,14 @@ class Migration(migrations.Migration):
|
||||||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
||||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
("authentik.stages.user_write", "authentik Stages.User Write"),
|
||||||
("authentik.stages.otp_static", "authentik Stages.OTP.Static"),
|
("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.stages.password", "authentik Stages.Password"),
|
||||||
("authentik.core", "authentik Core"),
|
("authentik.core", "authentik Core"),
|
||||||
],
|
],
|
||||||
|
|
|
@ -60,8 +60,14 @@ class Migration(migrations.Migration):
|
||||||
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
("authentik.stages.user_logout", "authentik Stages.User Logout"),
|
||||||
("authentik.stages.user_write", "authentik Stages.User Write"),
|
("authentik.stages.user_write", "authentik Stages.User Write"),
|
||||||
("authentik.stages.otp_static", "authentik Stages.OTP.Static"),
|
("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.stages.password", "authentik Stages.Password"),
|
||||||
("authentik.managed", "authentik Managed"),
|
("authentik.managed", "authentik Managed"),
|
||||||
("authentik.core", "authentik Core"),
|
("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_login.apps.AuthentikStageUserLoginConfig",
|
||||||
"authentik.stages.user_logout.apps.AuthentikStageUserLogoutConfig",
|
"authentik.stages.user_logout.apps.AuthentikStageUserLogoutConfig",
|
||||||
"authentik.stages.user_write.apps.AuthentikStageUserWriteConfig",
|
"authentik.stages.user_write.apps.AuthentikStageUserWriteConfig",
|
||||||
"authentik.stages.otp_static.apps.AuthentikStageOTPStaticConfig",
|
"authentik.stages.authenticator_static.apps.AuthentikStageAuthenticatorStaticConfig",
|
||||||
"authentik.stages.otp_time.apps.AuthentikStageOTPTimeConfig",
|
"authentik.stages.authenticator_totp.apps.AuthentikStageAuthenticatorTOTPConfig",
|
||||||
"authentik.stages.otp_validate.apps.AuthentikStageOTPValidateConfig",
|
"authentik.stages.authenticator_validate.apps.AuthentikStageAuthenticatorValidateConfig",
|
||||||
|
"authentik.stages.authenticator_webauthn.apps.AuthentikStageAuthenticatorWebAuthnConfig",
|
||||||
"authentik.stages.password.apps.AuthentikStagePasswordConfig",
|
"authentik.stages.password.apps.AuthentikStagePasswordConfig",
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"django_filters",
|
"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 import forms
|
||||||
from django.utils.safestring import mark_safe
|
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):
|
class StaticTokenWidget(forms.widgets.Widget):
|
||||||
|
@ -26,12 +26,12 @@ class SetupForm(forms.Form):
|
||||||
self.fields["tokens"].initial = tokens
|
self.fields["tokens"].initial = tokens
|
||||||
|
|
||||||
|
|
||||||
class OTPStaticStageForm(forms.ModelForm):
|
class AuthenticatorStaticStageForm(forms.ModelForm):
|
||||||
"""OTP Static Stage setup form"""
|
"""Static Authenticator Stage setup form"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = OTPStaticStage
|
model = AuthenticatorStaticStage
|
||||||
fields = ["name", "configure_flow", "token_count"]
|
fields = ["name", "configure_flow", "token_count"]
|
||||||
|
|
||||||
widgets = {
|
widgets = {
|
|
@ -30,8 +30,8 @@ class Migration(migrations.Migration):
|
||||||
("token_count", models.IntegerField(default=6)),
|
("token_count", models.IntegerField(default=6)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"verbose_name": "OTP Static Setup Stage",
|
"verbose_name": "Static Authenticator Setup Stage",
|
||||||
"verbose_name_plural": "OTP Static Setup Stages",
|
"verbose_name_plural": "Static Authenticator Setup Stages",
|
||||||
},
|
},
|
||||||
bases=("authentik_flows.stage",),
|
bases=("authentik_flows.stage",),
|
||||||
),
|
),
|
|
@ -8,7 +8,7 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_flows", "0013_auto_20200924_1605"),
|
("authentik_flows", "0013_auto_20200924_1605"),
|
||||||
("authentik_stages_otp_static", "0001_initial"),
|
("authentik_stages_authenticator_static", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
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 = [
|
INSTALLED_APPS = [
|
||||||
"django_otp.plugins.otp_static",
|
"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.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.stages.otp_static.forms import SetupForm
|
from authentik.stages.authenticator_static.forms import SetupForm
|
||||||
from authentik.stages.otp_static.models import OTPStaticStage
|
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
SESSION_STATIC_DEVICE = "static_device"
|
SESSION_STATIC_DEVICE = "static_device"
|
||||||
SESSION_STATIC_TOKENS = "static_device_tokens"
|
SESSION_STATIC_TOKENS = "static_device_tokens"
|
||||||
|
|
||||||
|
|
||||||
class OTPStaticStageView(FormView, StageView):
|
class AuthenticatorStaticStageView(FormView, StageView):
|
||||||
"""Static OTP Setup stage"""
|
"""Static OTP Setup stage"""
|
||||||
|
|
||||||
form_class = SetupForm
|
form_class = SetupForm
|
||||||
|
@ -38,7 +38,7 @@ class OTPStaticStageView(FormView, StageView):
|
||||||
if StaticDevice.objects.filter(user=user).exists():
|
if StaticDevice.objects.filter(user=user).exists():
|
||||||
return self.executor.stage_ok()
|
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:
|
if SESSION_STATIC_DEVICE not in self.request.session:
|
||||||
device = StaticDevice(user=user, confirmed=True)
|
device = StaticDevice(user=user, confirmed=True)
|
|
@ -22,10 +22,10 @@
|
||||||
</ul>
|
</ul>
|
||||||
{% if not state %}
|
{% if not state %}
|
||||||
{% if stage.configure_flow %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -1,7 +1,7 @@
|
||||||
"""OTP static urls"""
|
"""Static Authenticator urls"""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from authentik.stages.otp_static.views import DisableView, UserSettingsView
|
from authentik.stages.authenticator_static.views import DisableView, UserSettingsView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
|
@ -1,4 +1,4 @@
|
||||||
"""otp Static view Tokens"""
|
"""Static Authenticator view Tokens"""
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.http import HttpRequest, HttpResponse
|
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 django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||||
|
|
||||||
from authentik.events.models import Event
|
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):
|
class UserSettingsView(LoginRequiredMixin, TemplateView):
|
||||||
"""View for user settings to control OTP"""
|
"""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):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super().get_context_data(**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
|
kwargs["stage"] = stage
|
||||||
static_devices = StaticDevice.objects.filter(
|
static_devices = StaticDevice.objects.filter(
|
||||||
user=self.request.user, confirmed=True
|
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.utils.translation import gettext_lazy as _
|
||||||
from django_otp.models import Device
|
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):
|
class PictureWidget(forms.widgets.Widget):
|
||||||
|
@ -49,12 +49,12 @@ class SetupForm(forms.Form):
|
||||||
return self.cleaned_data.get("code")
|
return self.cleaned_data.get("code")
|
||||||
|
|
||||||
|
|
||||||
class OTPTimeStageForm(forms.ModelForm):
|
class AuthenticatorTOTPStageForm(forms.ModelForm):
|
||||||
"""OTP Time-based Stage setup form"""
|
"""OTP Time-based Stage setup form"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = OTPTimeStage
|
model = AuthenticatorTOTPStage
|
||||||
fields = ["name", "configure_flow", "digits"]
|
fields = ["name", "configure_flow", "digits"]
|
||||||
|
|
||||||
widgets = {
|
widgets = {
|
|
@ -6,7 +6,7 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_stages_otp_time", "0001_initial"),
|
("authentik_stages_authenticator_totp", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
|
@ -8,7 +8,7 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_flows", "0013_auto_20200924_1605"),
|
("authentik_flows", "0013_auto_20200924_1605"),
|
||||||
("authentik_stages_otp_time", "0002_auto_20200701_1900"),
|
("authentik_stages_authenticator_totp", "0002_auto_20200701_1900"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
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 django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
from authentik.flows.models import FlowDesignation
|
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):
|
def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
Flow = apps.get_model("authentik_flows", "Flow")
|
Flow = apps.get_model("authentik_flows", "Flow")
|
||||||
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
|
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
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
||||||
slug="default-otp-time-configure",
|
slug="default-authenticator-totp-setup",
|
||||||
designation=FlowDesignation.STAGE_CONFIGURATION,
|
designation=FlowDesignation.STAGE_CONFIGURATION,
|
||||||
defaults={
|
defaults={
|
||||||
"name": "default-otp-time-configure",
|
"name": "default-authenticator-totp-setup",
|
||||||
"title": "Setup Two-Factor authentication",
|
"title": "Setup Two-Factor authentication",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
stage, _ = OTPTimeStage.objects.using(db_alias).update_or_create(
|
stage, _ = AuthenticatorTOTPStage.objects.using(db_alias).update_or_create(
|
||||||
name="default-otp-time-configure", defaults={"digits": TOTPDigits.SIX}
|
name="default-authenticator-totp-setup", defaults={"digits": TOTPDigits.SIX}
|
||||||
)
|
)
|
||||||
|
|
||||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||||
target=flow, stage=stage, defaults={"order": 0}
|
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.configure_flow = flow
|
||||||
stage.save()
|
stage.save()
|
||||||
|
|
||||||
|
@ -41,7 +45,10 @@ def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_stages_otp_time", "0003_otptimestage_configure_flow"),
|
(
|
||||||
|
"authentik_stages_authenticator_totp",
|
||||||
|
"0005_auto_20210216_0838",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
|
@ -18,40 +18,42 @@ class TOTPDigits(models.IntegerChoices):
|
||||||
EIGHT = 8, _("8 digits, not compatible with apps like Google Authenticator")
|
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."""
|
"""Enroll a user's device into Time-based OTP."""
|
||||||
|
|
||||||
digits = models.IntegerField(choices=TOTPDigits.choices)
|
digits = models.IntegerField(choices=TOTPDigits.choices)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> BaseSerializer:
|
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
|
@property
|
||||||
def type(self) -> Type[View]:
|
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
|
@property
|
||||||
def form(self) -> Type[ModelForm]:
|
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
|
@property
|
||||||
def ui_user_settings(self) -> Optional[str]:
|
def ui_user_settings(self) -> Optional[str]:
|
||||||
return reverse(
|
return reverse(
|
||||||
"authentik_stages_otp_time:user-settings",
|
"authentik_stages_authenticator_totp:user-settings",
|
||||||
kwargs={"stage_uuid": self.stage_uuid},
|
kwargs={"stage_uuid": self.stage_uuid},
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"OTP Time (TOTP) Stage {self.name}"
|
return f"TOTP Authenticator Setup Stage {self.name}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("OTP Time (TOTP) Setup Stage")
|
verbose_name = _("TOTP Authenticator Setup Stage")
|
||||||
verbose_name_plural = _("OTP Time (TOTP) Setup Stages")
|
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.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.stages.otp_time.forms import SetupForm
|
from authentik.stages.authenticator_totp.forms import SetupForm
|
||||||
from authentik.stages.otp_time.models import OTPTimeStage
|
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
SESSION_TOTP_DEVICE = "totp_device"
|
SESSION_TOTP_DEVICE = "totp_device"
|
||||||
|
|
||||||
|
|
||||||
class OTPTimeStageView(FormView, StageView):
|
class AuthenticatorTOTPStageView(FormView, StageView):
|
||||||
"""OTP totp Setup stage"""
|
"""OTP totp Setup stage"""
|
||||||
|
|
||||||
form_class = SetupForm
|
form_class = SetupForm
|
||||||
|
@ -50,7 +50,7 @@ class OTPTimeStageView(FormView, StageView):
|
||||||
if TOTPDevice.objects.filter(user=user).exists():
|
if TOTPDevice.objects.filter(user=user).exists():
|
||||||
return self.executor.stage_ok()
|
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:
|
if SESSION_TOTP_DEVICE not in self.request.session:
|
||||||
device = TOTPDevice(user=user, confirmed=True, digits=stage.digits)
|
device = TOTPDevice(user=user, confirmed=True, digits=stage.digits)
|
|
@ -18,10 +18,10 @@
|
||||||
<p>
|
<p>
|
||||||
{% if not state %}
|
{% if not state %}
|
||||||
{% if stage.configure_flow %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
|
@ -1,7 +1,7 @@
|
||||||
"""OTP Time urls"""
|
"""OTP Time urls"""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from authentik.stages.otp_time.views import DisableView, UserSettingsView
|
from authentik.stages.authenticator_totp.views import DisableView, UserSettingsView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
|
@ -8,17 +8,17 @@ from django.views.generic import TemplateView
|
||||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||||
|
|
||||||
from authentik.events.models import Event
|
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):
|
class UserSettingsView(LoginRequiredMixin, TemplateView):
|
||||||
"""View for user settings to control OTP"""
|
"""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):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super().get_context_data(**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
|
kwargs["stage"] = stage
|
||||||
|
|
||||||
totp_devices = TOTPDevice.objects.filter(user=self.request.user, confirmed=True)
|
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 django_otp import match_token
|
||||||
|
|
||||||
from authentik.core.models import User
|
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):
|
class ValidationForm(forms.Form):
|
||||||
|
@ -36,14 +39,15 @@ class ValidationForm(forms.Form):
|
||||||
return code
|
return code
|
||||||
|
|
||||||
|
|
||||||
class OTPValidateStageForm(forms.ModelForm):
|
class AuthenticatorValidateStageForm(forms.ModelForm):
|
||||||
"""OTP Validate stage forms"""
|
"""OTP Validate stage forms"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = OTPValidateStage
|
model = AuthenticatorValidateStage
|
||||||
fields = ["name"]
|
fields = ["name", "device_classes"]
|
||||||
|
|
||||||
widgets = {
|
widgets = {
|
||||||
"name": forms.TextInput(),
|
"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.models import NotConfiguredAction
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.stages.otp_validate.forms import ValidationForm
|
from authentik.stages.authenticator_validate.forms import ValidationForm
|
||||||
from authentik.stages.otp_validate.models import OTPValidateStage
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class OTPValidateStageView(FormView, StageView):
|
class AuthenticatorValidateStageView(FormView, StageView):
|
||||||
"""OTP Validation"""
|
"""OTP Validation"""
|
||||||
|
|
||||||
form_class = ValidationForm
|
form_class = ValidationForm
|
||||||
|
@ -31,11 +31,11 @@ class OTPValidateStageView(FormView, StageView):
|
||||||
LOGGER.debug("No pending user, continuing")
|
LOGGER.debug("No pending user, continuing")
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
has_devices = user_has_device(user)
|
has_devices = user_has_device(user)
|
||||||
stage: OTPValidateStage = self.executor.current_stage
|
stage: AuthenticatorValidateStage = self.executor.current_stage
|
||||||
|
|
||||||
if not has_devices:
|
if not has_devices:
|
||||||
if stage.not_configured_action == NotConfiguredAction.SKIP:
|
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 self.executor.stage_ok()
|
||||||
return super().get(request, *args, **kwargs)
|
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")
|
Flow = apps.get_model("authentik_flows", "Flow")
|
||||||
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
|
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
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
||||||
slug="default-otp-static-configure",
|
slug="default-authenticator-webuahtn-setup",
|
||||||
designation=FlowDesignation.STAGE_CONFIGURATION,
|
designation=FlowDesignation.STAGE_CONFIGURATION,
|
||||||
defaults={
|
defaults={
|
||||||
"name": "default-otp-static-configure",
|
"name": "default-authenticator-webuahtn-setup",
|
||||||
"title": "Setup Static OTP Tokens",
|
"title": "Setup Static OTP Tokens",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
stage, _ = OTPStaticStage.objects.using(db_alias).update_or_create(
|
stage, _ = AuthenticateWebAuthnStage.objects.using(db_alias).update_or_create(
|
||||||
name="default-otp-static-configure", defaults={"token_count": 6}
|
name="default-authenticator-webuahtn-setup"
|
||||||
)
|
)
|
||||||
|
|
||||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||||
target=flow, stage=stage, defaults={"order": 0}
|
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.configure_flow = flow
|
||||||
stage.save()
|
stage.save()
|
||||||
|
|
||||||
|
@ -40,7 +44,10 @@ def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_stages_otp_static", "0002_otpstaticstage_configure_flow"),
|
(
|
||||||
|
"authentik_stages_authenticator_webauthn",
|
||||||
|
"0001_initial",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
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"""
|
"""CaptchaStage API Views"""
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.flows.api import StageSerializer
|
||||||
from authentik.stages.captcha.models import CaptchaStage
|
from authentik.stages.captcha.models import CaptchaStage
|
||||||
|
|
||||||
|
|
||||||
class CaptchaStageSerializer(ModelSerializer):
|
class CaptchaStageSerializer(StageSerializer):
|
||||||
"""CaptchaStage Serializer"""
|
"""CaptchaStage Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = CaptchaStage
|
model = CaptchaStage
|
||||||
fields = ["pk", "name", "public_key", "private_key"]
|
fields = StageSerializer.Meta.fields + ["public_key", "private_key"]
|
||||||
|
|
||||||
|
|
||||||
class CaptchaStageViewSet(ModelViewSet):
|
class CaptchaStageViewSet(ModelViewSet):
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
"""ConsentStage API Views"""
|
"""ConsentStage API Views"""
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.flows.api import StageSerializer
|
||||||
from authentik.stages.consent.models import ConsentStage
|
from authentik.stages.consent.models import ConsentStage
|
||||||
|
|
||||||
|
|
||||||
class ConsentStageSerializer(ModelSerializer):
|
class ConsentStageSerializer(StageSerializer):
|
||||||
"""ConsentStage Serializer"""
|
"""ConsentStage Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = ConsentStage
|
model = ConsentStage
|
||||||
fields = ["pk", "name", "mode", "consent_expire_in"]
|
fields = StageSerializer.Meta.fields + ["mode", "consent_expire_in"]
|
||||||
|
|
||||||
|
|
||||||
class ConsentStageViewSet(ModelViewSet):
|
class ConsentStageViewSet(ModelViewSet):
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
"""DummyStage API Views"""
|
"""DummyStage API Views"""
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.flows.api import StageSerializer
|
||||||
from authentik.stages.dummy.models import DummyStage
|
from authentik.stages.dummy.models import DummyStage
|
||||||
|
|
||||||
|
|
||||||
class DummyStageSerializer(ModelSerializer):
|
class DummyStageSerializer(StageSerializer):
|
||||||
"""DummyStage Serializer"""
|
"""DummyStage Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = DummyStage
|
model = DummyStage
|
||||||
fields = ["pk", "name"]
|
fields = StageSerializer.Meta.fields
|
||||||
|
|
||||||
|
|
||||||
class DummyStageViewSet(ModelViewSet):
|
class DummyStageViewSet(ModelViewSet):
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
"""EmailStage API Views"""
|
"""EmailStage API Views"""
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.flows.api import StageSerializer
|
||||||
from authentik.stages.email.models import EmailStage, get_template_choices
|
from authentik.stages.email.models import EmailStage, get_template_choices
|
||||||
|
|
||||||
|
|
||||||
class EmailStageSerializer(ModelSerializer):
|
class EmailStageSerializer(StageSerializer):
|
||||||
"""EmailStage Serializer"""
|
"""EmailStage Serializer"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwrags):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwrags)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields["template"].choices = get_template_choices()
|
self.fields["template"].choices = get_template_choices()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = EmailStage
|
model = EmailStage
|
||||||
fields = [
|
fields = StageSerializer.Meta.fields + [
|
||||||
"pk",
|
|
||||||
"name",
|
|
||||||
"use_global_settings",
|
"use_global_settings",
|
||||||
"host",
|
"host",
|
||||||
"port",
|
"port",
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
"""Identification Stage API Views"""
|
"""Identification Stage API Views"""
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.flows.api import StageSerializer
|
||||||
from authentik.stages.identification.models import IdentificationStage
|
from authentik.stages.identification.models import IdentificationStage
|
||||||
|
|
||||||
|
|
||||||
class IdentificationStageSerializer(ModelSerializer):
|
class IdentificationStageSerializer(StageSerializer):
|
||||||
"""IdentificationStage Serializer"""
|
"""IdentificationStage Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = IdentificationStage
|
model = IdentificationStage
|
||||||
fields = [
|
fields = StageSerializer.Meta.fields + [
|
||||||
"pk",
|
|
||||||
"name",
|
|
||||||
"user_fields",
|
"user_fields",
|
||||||
"case_insensitive_matching",
|
"case_insensitive_matching",
|
||||||
"show_matched_user",
|
"show_matched_user",
|
||||||
|
|
|
@ -2,18 +2,17 @@
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.flows.api import StageSerializer
|
||||||
from authentik.stages.invitation.models import Invitation, InvitationStage
|
from authentik.stages.invitation.models import Invitation, InvitationStage
|
||||||
|
|
||||||
|
|
||||||
class InvitationStageSerializer(ModelSerializer):
|
class InvitationStageSerializer(StageSerializer):
|
||||||
"""InvitationStage Serializer"""
|
"""InvitationStage Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = InvitationStage
|
model = InvitationStage
|
||||||
fields = [
|
fields = StageSerializer.Meta.fields + [
|
||||||
"pk",
|
|
||||||
"name",
|
|
||||||
"continue_flow_without_invitation",
|
"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"""
|
"""PasswordStage API Views"""
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.flows.api import StageSerializer
|
||||||
from authentik.stages.password.models import PasswordStage
|
from authentik.stages.password.models import PasswordStage
|
||||||
|
|
||||||
|
|
||||||
class PasswordStageSerializer(ModelSerializer):
|
class PasswordStageSerializer(StageSerializer):
|
||||||
"""PasswordStage Serializer"""
|
"""PasswordStage Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = PasswordStage
|
model = PasswordStage
|
||||||
fields = [
|
fields = StageSerializer.Meta.fields + [
|
||||||
"pk",
|
|
||||||
"name",
|
|
||||||
"backends",
|
"backends",
|
||||||
"configure_flow",
|
"configure_flow",
|
||||||
"failed_attempts_before_cancel",
|
"failed_attempts_before_cancel",
|
||||||
|
|
|
@ -3,10 +3,11 @@ from rest_framework.serializers import CharField, ModelSerializer
|
||||||
from rest_framework.validators import UniqueValidator
|
from rest_framework.validators import UniqueValidator
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.flows.api import StageSerializer
|
||||||
from authentik.stages.prompt.models import Prompt, PromptStage
|
from authentik.stages.prompt.models import Prompt, PromptStage
|
||||||
|
|
||||||
|
|
||||||
class PromptStageSerializer(ModelSerializer):
|
class PromptStageSerializer(StageSerializer):
|
||||||
"""PromptStage Serializer"""
|
"""PromptStage Serializer"""
|
||||||
|
|
||||||
name = CharField(validators=[UniqueValidator(queryset=PromptStage.objects.all())])
|
name = CharField(validators=[UniqueValidator(queryset=PromptStage.objects.all())])
|
||||||
|
@ -14,9 +15,7 @@ class PromptStageSerializer(ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = PromptStage
|
model = PromptStage
|
||||||
fields = [
|
fields = StageSerializer.Meta.fields + [
|
||||||
"pk",
|
|
||||||
"name",
|
|
||||||
"fields",
|
"fields",
|
||||||
"validation_policies",
|
"validation_policies",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
"""User Delete Stage API Views"""
|
"""User Delete Stage API Views"""
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.flows.api import StageSerializer
|
||||||
from authentik.stages.user_delete.models import UserDeleteStage
|
from authentik.stages.user_delete.models import UserDeleteStage
|
||||||
|
|
||||||
|
|
||||||
class UserDeleteStageSerializer(ModelSerializer):
|
class UserDeleteStageSerializer(StageSerializer):
|
||||||
"""UserDeleteStage Serializer"""
|
"""UserDeleteStage Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = UserDeleteStage
|
model = UserDeleteStage
|
||||||
fields = [
|
fields = StageSerializer.Meta.fields
|
||||||
"pk",
|
|
||||||
"name",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class UserDeleteStageViewSet(ModelViewSet):
|
class UserDeleteStageViewSet(ModelViewSet):
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
"""Login Stage API Views"""
|
"""Login Stage API Views"""
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.flows.api import StageSerializer
|
||||||
from authentik.stages.user_login.models import UserLoginStage
|
from authentik.stages.user_login.models import UserLoginStage
|
||||||
|
|
||||||
|
|
||||||
class UserLoginStageSerializer(ModelSerializer):
|
class UserLoginStageSerializer(StageSerializer):
|
||||||
"""UserLoginStage Serializer"""
|
"""UserLoginStage Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = UserLoginStage
|
model = UserLoginStage
|
||||||
fields = [
|
fields = StageSerializer.Meta.fields + [
|
||||||
"pk",
|
|
||||||
"name",
|
|
||||||
"session_duration",
|
"session_duration",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
"""Logout Stage API Views"""
|
"""Logout Stage API Views"""
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.flows.api import StageSerializer
|
||||||
from authentik.stages.user_logout.models import UserLogoutStage
|
from authentik.stages.user_logout.models import UserLogoutStage
|
||||||
|
|
||||||
|
|
||||||
class UserLogoutStageSerializer(ModelSerializer):
|
class UserLogoutStageSerializer(StageSerializer):
|
||||||
"""UserLogoutStage Serializer"""
|
"""UserLogoutStage Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = UserLogoutStage
|
model = UserLogoutStage
|
||||||
fields = [
|
fields = StageSerializer.Meta.fields
|
||||||
"pk",
|
|
||||||
"name",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class UserLogoutStageViewSet(ModelViewSet):
|
class UserLogoutStageViewSet(ModelViewSet):
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
"""User Write Stage API Views"""
|
"""User Write Stage API Views"""
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.flows.api import StageSerializer
|
||||||
from authentik.stages.user_write.models import UserWriteStage
|
from authentik.stages.user_write.models import UserWriteStage
|
||||||
|
|
||||||
|
|
||||||
class UserWriteStageSerializer(ModelSerializer):
|
class UserWriteStageSerializer(StageSerializer):
|
||||||
"""UserWriteStage Serializer"""
|
"""UserWriteStage Serializer"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = UserWriteStage
|
model = UserWriteStage
|
||||||
fields = [
|
fields = StageSerializer.Meta.fields
|
||||||
"pk",
|
|
||||||
"name",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class UserWriteStageViewSet(ModelViewSet):
|
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 selenium.webdriver.support import expected_conditions as ec
|
||||||
|
|
||||||
from authentik.flows.models import Flow, FlowStageBinding
|
from authentik.flows.models import Flow, FlowStageBinding
|
||||||
from authentik.stages.otp_static.models import OTPStaticStage
|
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
|
||||||
from authentik.stages.otp_time.models import OTPTimeStage
|
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
|
||||||
from authentik.stages.otp_validate.models import OTPValidateStage
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
||||||
from tests.e2e.utils import USER, SeleniumTestCase, retry
|
from tests.e2e.utils import USER, SeleniumTestCase, retry
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
class TestFlowsOTP(SeleniumTestCase):
|
class TestFlowsAuthenticator(SeleniumTestCase):
|
||||||
"""test flow with otp stages"""
|
"""test flow with otp stages"""
|
||||||
|
|
||||||
@retry()
|
@retry()
|
||||||
def test_otp_validate(self):
|
def test_totp_validate(self):
|
||||||
"""test flow with otp stages"""
|
"""test flow with otp stages"""
|
||||||
sleep(1)
|
sleep(1)
|
||||||
# Setup TOTP Device
|
# Setup TOTP Device
|
||||||
|
@ -32,10 +32,8 @@ class TestFlowsOTP(SeleniumTestCase):
|
||||||
device = TOTPDevice.objects.create(user=user, confirmed=True, digits=6)
|
device = TOTPDevice.objects.create(user=user, confirmed=True, digits=6)
|
||||||
|
|
||||||
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
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(
|
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}/")
|
self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
|
||||||
|
@ -53,7 +51,7 @@ class TestFlowsOTP(SeleniumTestCase):
|
||||||
self.assert_user(USER())
|
self.assert_user(USER())
|
||||||
|
|
||||||
@retry()
|
@retry()
|
||||||
def test_otp_totp_setup(self):
|
def test_totp_setup(self):
|
||||||
"""test TOTP Setup stage"""
|
"""test TOTP Setup stage"""
|
||||||
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
||||||
|
|
||||||
|
@ -69,7 +67,7 @@ class TestFlowsOTP(SeleniumTestCase):
|
||||||
self.driver.get(
|
self.driver.get(
|
||||||
self.url(
|
self.url(
|
||||||
"authentik_flows:configure",
|
"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())
|
self.assertTrue(TOTPDevice.objects.filter(user=USER(), confirmed=True).exists())
|
||||||
|
|
||||||
@retry()
|
@retry()
|
||||||
def test_otp_static_setup(self):
|
def test_static_setup(self):
|
||||||
"""test Static OTP Setup stage"""
|
"""test Static OTP Setup stage"""
|
||||||
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
||||||
|
|
||||||
|
@ -112,7 +110,7 @@ class TestFlowsOTP(SeleniumTestCase):
|
||||||
self.driver.get(
|
self.driver.get(
|
||||||
self.url(
|
self.url(
|
||||||
"authentik_flows:configure",
|
"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": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
"@sentry/tracing": "^6.1.0",
|
"@sentry/tracing": "^6.1.0",
|
||||||
"@types/chart.js": "^2.9.30",
|
"@types/chart.js": "^2.9.30",
|
||||||
"@types/codemirror": "0.0.108",
|
"@types/codemirror": "0.0.108",
|
||||||
|
"base64-js": "^1.5.1",
|
||||||
"chart.js": "^2.9.4",
|
"chart.js": "^2.9.4",
|
||||||
"codemirror": "^5.59.2",
|
"codemirror": "^5.59.2",
|
||||||
"construct-style-sheets-polyfill": "^2.4.16",
|
"construct-style-sheets-polyfill": "^2.4.16",
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import resolve from "rollup-plugin-node-resolve";
|
import resolve from "rollup-plugin-node-resolve";
|
||||||
import commonjs from "rollup-plugin-commonjs";
|
import commonjs from "rollup-plugin-commonjs";
|
||||||
import minifyHTML from "rollup-plugin-minify-html-literals";
|
|
||||||
import { terser } from "rollup-plugin-terser";
|
import { terser } from "rollup-plugin-terser";
|
||||||
import sourcemaps from "rollup-plugin-sourcemaps";
|
import sourcemaps from "rollup-plugin-sourcemaps";
|
||||||
import typescript from "@rollup/plugin-typescript";
|
import typescript from "@rollup/plugin-typescript";
|
||||||
|
@ -38,7 +37,6 @@ export default [
|
||||||
resolve({ browser: true }),
|
resolve({ browser: true }),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
sourcemaps(),
|
sourcemaps(),
|
||||||
minifyHTML(),
|
|
||||||
terser(),
|
terser(),
|
||||||
copy({
|
copy({
|
||||||
targets: [...resources],
|
targets: [...resources],
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue