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:
Jens L 2021-02-17 20:49:58 +01:00 committed by GitHub
parent e020b8bf32
commit 8708e487ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
128 changed files with 2949 additions and 874 deletions

View File

@ -45,6 +45,7 @@ kubernetes = "*"
docker = "*" docker = "*"
xmlsec = "*" xmlsec = "*"
geoip2 = "*" geoip2 = "*"
webauthn = "*"
[requires] [requires]
python_version = "3.9" python_version = "3.9"

114
Pipfile.lock generated
View File

@ -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": {

View File

@ -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 %}

View File

@ -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)

View File

@ -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"""

View File

@ -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,
}, },
) )

View File

@ -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):

View File

@ -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):

View File

@ -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)

View File

@ -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"),
], ],

View File

@ -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"),
], ],

View File

@ -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"),

View File

@ -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.",
),
),
]

View File

@ -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.",
),
),
]

View File

@ -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",

View File

@ -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

View File

@ -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/"

View File

@ -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 = {

View File

@ -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",),
), ),

View File

@ -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 = [

View File

@ -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 = []

View File

@ -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",
},
),
]

View File

@ -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),
]

View File

@ -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")

View File

@ -1,4 +1,4 @@
"""OTP Static settings""" """Static Authenticator settings"""
INSTALLED_APPS = [ INSTALLED_APPS = [
"django_otp.plugins.otp_static", "django_otp.plugins.otp_static",

View File

@ -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)

View File

@ -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>

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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/"

View File

@ -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 = {

View File

@ -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 = [

View File

@ -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 = [

View File

@ -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 = []

View File

@ -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",
},
),
]

View File

@ -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 = [

View File

@ -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")

View File

@ -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)

View File

@ -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>

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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),
} }

View File

@ -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",
},
),
]

View File

@ -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,
),
),
]

View File

@ -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")

View File

@ -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)

View File

@ -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

View File

@ -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/"

View File

@ -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(),
}

View File

@ -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),
),
]

View File

@ -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 = [

View File

@ -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)

View File

@ -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()

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"
),
]

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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",

View File

@ -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",

View File

@ -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",
] ]

View File

@ -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

View File

@ -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/"

View File

@ -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")

View File

@ -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

View File

@ -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/"

View File

@ -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

View File

@ -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"

View File

@ -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")

View File

@ -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",

View File

@ -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",
] ]

View File

@ -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):

View File

@ -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",
] ]

View File

@ -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):

View File

@ -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):

View File

@ -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()

File diff suppressed because it is too large Load Diff

View File

@ -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,
) )
) )

5
web/package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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