diff --git a/Makefile b/Makefile
index 3dc4030fa..3bd0b715e 100644
--- a/Makefile
+++ b/Makefile
@@ -52,6 +52,16 @@ py-lint: ## Run python linting scanners
pipenv run flake8 .
pipenv run isort --check-only ./app ./tests
+.PHONY: avg-complexity
+avg-complexity:
+ echo "*** Shows average complexity in radon of all code ***"
+ pipenv run radon cc ./app -a -na
+
+.PHONY: too-complex
+too-complex:
+ echo "*** Shows code that got a rating of C, D or F in radon ***"
+ pipenv run radon cc ./app -a -nc
+
.PHONY: py-test
py-test: export NEW_RELIC_ENVIRONMENT=test
py-test: ## Run python unit tests
diff --git a/Pipfile b/Pipfile
index 19d52f7d8..47946e22d 100644
--- a/Pipfile
+++ b/Pipfile
@@ -6,8 +6,7 @@ name = "pypi"
[packages]
ago = "~=0.0.95"
blinker = "~=1.4"
-exceptiongroup = "==1.1.2"
-fido2 = "~=0.9"
+exceptiongroup = "==1.1.3"
flask = "~=2.3"
flask-basicauth = "~=0.2"
flask-login = "~=0.6"
@@ -16,7 +15,7 @@ gds-metrics = {version = "==0.2.4", ref = "6f1840a57b6fb1ee40b7e84f2f18ec229de8a
govuk-bank-holidays = "==0.13"
govuk-frontend-jinja = {version = "==0.5.8-alpha", git = "https://github.com/alphagov/govuk-frontend-jinja.git"}
gunicorn = {version = "==20.1.0", extras = ["eventlet"], ref = "1299ea9e967a61ae2edebe191082fd169b864c64", git = "https://github.com/benoitc/gunicorn.git"}
-humanize = "~=4.1"
+humanize = "~=4.8"
itsdangerous = "~=2.1"
jinja2 = "~=3.1"
notifications-python-client = "==8.0.1"
@@ -39,12 +38,14 @@ notifications-utils = {editable = true, ref = "main", git = "https://github.com/
coverage = "*"
pytest-playwright = "*"
vulture = "==2.7"
+radon = "==6.0.1"
[dev-packages]
isort = "==5.12.0"
pytest = "==7.4.0"
pytest-env = "==0.8.2"
pytest-mock = "==3.11.1"
+pytest-playwright = "==0.4.2"
pytest-xdist = "==3.3.1"
beautifulsoup4 = "==4.12.2"
freezegun = "==1.2.2"
diff --git a/Pipfile.lock b/Pipfile.lock
index 8811c29ff..f5bb946b1 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "0d69ab20cc57f9e6a67aaf4a1f5e7fd1c9562ba75b3386cbb14368fe986fdf62"
+ "sha256": "1ba88d2c8e70243162c4fd03b8ddf1ec9c5542d0d2ccee7b4e8f4fd543912179"
},
"pipfile-spec": 6,
"requires": {
@@ -26,18 +26,18 @@
},
"async-timeout": {
"hashes": [
- "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15",
- "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"
+ "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f",
+ "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"
],
"markers": "python_full_version <= '3.11.2'",
- "version": "==4.0.2"
+ "version": "==4.0.3"
},
"bleach": {
"hashes": [
"sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414",
"sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==6.0.0"
},
"blinker": {
@@ -50,26 +50,26 @@
},
"boto3": {
"hashes": [
- "sha256:87ecac82d2a68430c0292b7946512c8b1f01ea6971b43dc5832582fcb176c0dd",
- "sha256:f2ec3e6f173fe8d141d512ea7d90138db5a58af130773e26ce8e72bdbfd2cddc"
+ "sha256:2761f3249fe25c3ec1a8cd6b95fca2317747503e6f1d127daf6a3d2cdeb25680",
+ "sha256:dc6d72470f6d8926b8cdc10ee7708d7ccdd36d6313c7aa298bc1cf6bedb8921e"
],
- "markers": "python_version >= '3.7'",
- "version": "==1.28.18"
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==1.28.31"
},
"botocore": {
"hashes": [
- "sha256:909db57f5d6ca765fc9dc9dcae962a87566d0123da1d2bd5be32432493d5785e",
- "sha256:c4c01fae2ba32c242ce62175cad719aa49415618560d6e215ed76dab91991dc5"
+ "sha256:1eef14ae98e8662e43f7cf6d993c732793def02644e2d489c5171d3b9269e900",
+ "sha256:950a49c5286fe1f6d72cfbe2910b9ddbdfbb907975ddc41cf38ac9709b4d1291"
],
- "markers": "python_version >= '3.7'",
- "version": "==1.31.18"
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==1.31.31"
},
"cachetools": {
"hashes": [
"sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590",
"sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==5.3.1"
},
"certifi": {
@@ -154,7 +154,7 @@
"sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7",
"sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==5.2.0"
},
"charset-normalizer": {
@@ -235,82 +235,82 @@
"sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
"sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==3.2.0"
},
"click": {
"hashes": [
- "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd",
- "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"
+ "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
+ "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
],
- "markers": "python_version >= '3.7'",
- "version": "==8.1.6"
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==8.1.7"
+ },
+ "colorama": {
+ "hashes": [
+ "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
+ "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
+ ],
+ "markers": "python_version >= '3.5'",
+ "version": "==0.4.6"
},
"coverage": {
"hashes": [
- "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f",
- "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2",
- "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a",
- "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a",
- "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01",
- "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6",
- "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7",
- "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f",
- "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02",
- "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c",
- "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063",
- "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a",
- "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5",
- "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959",
- "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97",
- "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6",
- "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f",
- "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9",
- "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5",
- "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f",
- "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562",
- "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe",
- "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9",
- "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f",
- "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb",
- "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb",
- "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1",
- "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb",
- "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250",
- "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e",
- "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511",
- "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5",
- "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59",
- "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2",
- "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d",
- "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3",
- "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4",
- "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de",
- "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9",
- "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833",
- "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0",
- "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9",
- "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d",
- "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050",
- "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d",
- "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6",
- "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353",
- "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb",
- "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e",
- "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8",
- "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495",
- "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2",
- "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd",
- "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27",
- "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1",
- "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818",
- "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4",
- "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e",
- "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850",
- "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"
+ "sha256:07ea61bcb179f8f05ffd804d2732b09d23a1238642bf7e51dad62082b5019b34",
+ "sha256:1084393c6bda8875c05e04fce5cfe1301a425f758eb012f010eab586f1f3905e",
+ "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7",
+ "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b",
+ "sha256:2d22172f938455c156e9af2612650f26cceea47dc86ca048fa4e0b2d21646ad3",
+ "sha256:34f9f0763d5fa3035a315b69b428fe9c34d4fc2f615262d6be3d3bf3882fb985",
+ "sha256:3558e5b574d62f9c46b76120a5c7c16c4612dc2644c3d48a9f4064a705eaee95",
+ "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2",
+ "sha256:37d5576d35fcb765fca05654f66aa71e2808d4237d026e64ac8b397ffa66a56a",
+ "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74",
+ "sha256:438856d3f8f1e27f8e79b5410ae56650732a0dcfa94e756df88c7e2d24851fcd",
+ "sha256:477c9430ad5d1b80b07f3c12f7120eef40bfbf849e9e7859e53b9c93b922d2af",
+ "sha256:49ab200acf891e3dde19e5aa4b0f35d12d8b4bd805dc0be8792270c71bd56c54",
+ "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865",
+ "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214",
+ "sha256:4eddd3153d02204f22aef0825409091a91bf2a20bce06fe0f638f5c19a85de54",
+ "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe",
+ "sha256:5492a6ce3bdb15c6ad66cb68a0244854d9917478877a25671d70378bdc8562d0",
+ "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321",
+ "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446",
+ "sha256:60f64e2007c9144375dd0f480a54d6070f00bb1a28f65c408370544091c9bc9e",
+ "sha256:63c5b8ecbc3b3d5eb3a9d873dec60afc0cd5ff9d9f1c75981d8c31cfe4df8527",
+ "sha256:68d8a0426b49c053013e631c0cdc09b952d857efa8f68121746b339912d27a12",
+ "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f",
+ "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f",
+ "sha256:7df91fb24c2edaabec4e0eee512ff3bc6ec20eb8dccac2e77001c1fe516c0c84",
+ "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479",
+ "sha256:80501d1b2270d7e8daf1b64b895745c3e234289e00d5f0e30923e706f110334e",
+ "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873",
+ "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70",
+ "sha256:8f39c49faf5344af36042b293ce05c0d9004270d811c7080610b3e713251c9b0",
+ "sha256:90b6e2f0f66750c5a1178ffa9370dec6c508a8ca5265c42fbad3ccac210a7977",
+ "sha256:96d7d761aea65b291a98c84e1250cd57b5b51726821a6f2f8df65db89363be51",
+ "sha256:97af9554a799bd7c58c0179cc8dbf14aa7ab50e1fd5fa73f90b9b7215874ba28",
+ "sha256:97c44f4ee13bce914272589b6b41165bbb650e48fdb7bd5493a38bde8de730a1",
+ "sha256:a67e6bbe756ed458646e1ef2b0778591ed4d1fcd4b146fc3ba2feb1a7afd4254",
+ "sha256:ac0dec90e7de0087d3d95fa0533e1d2d722dcc008bc7b60e1143402a04c117c1",
+ "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd",
+ "sha256:b3eb0c93e2ea6445b2173da48cb548364f8f65bf68f3d090404080d338e3a689",
+ "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d",
+ "sha256:b859128a093f135b556b4765658d5d2e758e1fae3e7cc2f8c10f26fe7005e543",
+ "sha256:bac329371d4c0d456e8d5f38a9b0816b446581b5f278474e416ea0c68c47dcd9",
+ "sha256:c02cfa6c36144ab334d556989406837336c1d05215a9bdf44c0bc1d1ac1cb637",
+ "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071",
+ "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482",
+ "sha256:ce2ee86ca75f9f96072295c5ebb4ef2a43cecf2870b0ca5e7a1cbdd929cf67e1",
+ "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b",
+ "sha256:db76a1bcb51f02b2007adacbed4c88b6dee75342c37b05d1822815eed19edee5",
+ "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a",
+ "sha256:e61260ec93f99f2c2d93d264b564ba912bec502f679793c56f678ba5251f0393",
+ "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a",
+ "sha256:fc0ed8d310afe013db1eedd37176d0839dc66c96bcfcce8f6607a73ffea2d6ba"
],
"index": "pypi",
- "version": "==7.2.7"
+ "version": "==7.3.0"
},
"cryptography": {
"hashes": [
@@ -338,16 +338,16 @@
"sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e",
"sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==41.0.3"
},
"dnspython": {
"hashes": [
- "sha256:5b7488477388b8c0b70a8ce93b227c5603bc7b77f1565afe8e729c36c51447d7",
- "sha256:c33971c79af5be968bb897e95c2448e11a645ee84d93b265ce0b7aabe5dfdca8"
+ "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8",
+ "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"
],
"markers": "python_version >= '3.8' and python_version < '4.0'",
- "version": "==2.4.1"
+ "version": "==2.4.2"
},
"docopt": {
"hashes": [
@@ -372,26 +372,19 @@
},
"exceptiongroup": {
"hashes": [
- "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5",
- "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"
+ "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9",
+ "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"
],
"index": "pypi",
- "version": "==1.1.2"
- },
- "fido2": {
- "hashes": [
- "sha256:b45e89a6109cfcb7f1bb513776aa2d6408e95c4822f83a253918b944083466ec"
- ],
- "index": "pypi",
- "version": "==0.9.3"
+ "version": "==1.1.3"
},
"flask": {
"hashes": [
- "sha256:77fd4e1249d8c9923de34907236b747ced06e5467ecac1a7bb7115ae0e9670b0",
- "sha256:8c2f9abd47a9e8df7f0c3f091ce9497d011dc3b31effcf4c85a6e2b50f4114ef"
+ "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc",
+ "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b"
],
"index": "pypi",
- "version": "==2.3.2"
+ "version": "==2.3.3"
},
"flask-basicauth": {
"hashes": [
@@ -442,7 +435,7 @@
"sha256:e49df982b204ed481e4c1236c57f587adf71537301cf8faf7120ab27d73c7568",
"sha256:ff3d75acab60b1e66504a11f7ea12c104bad32ff3c410a807788663b966dee4a"
],
- "markers": "python_version < '3.12' and python_version >= '3.7'",
+ "markers": "python_version < '3.12' and python_full_version >= '3.7.0'",
"version": "==3.0.1"
},
"govuk-bank-holidays": {
@@ -534,11 +527,11 @@
},
"humanize": {
"hashes": [
- "sha256:7ca0e43e870981fa684acb5b062deb307218193bca1a01f2b2676479df849b3a",
- "sha256:df7c429c2d27372b249d3f26eb53b07b166b661326e0325793e0a988082e3889"
+ "sha256:8bc9e2bb9315e61ec06bf690151ae35aeb65651ab091266941edf97c90836404",
+ "sha256:9783373bf1eec713a770ecaa7c2d7a7902c98398009dfa3d8a2df91eec9311e8"
],
"index": "pypi",
- "version": "==4.7.0"
+ "version": "==4.8.0"
},
"idna": {
"hashes": [
@@ -561,7 +554,7 @@
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==2.0.0"
},
"itsdangerous": {
@@ -585,7 +578,7 @@
"sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980",
"sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==1.0.1"
},
"lml": {
@@ -693,6 +686,13 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.9.3"
},
+ "mando": {
+ "hashes": [
+ "sha256:18baa999b4b613faefb00eac4efadcf14f510b59b924b66e08289aa1de8c3500",
+ "sha256:26ef1d70928b6057ee3ca12583d73c63e05c49de8972d620c278a7b206581a8a"
+ ],
+ "version": "==0.7.1"
+ },
"markupsafe": {
"hashes": [
"sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e",
@@ -746,7 +746,7 @@
"sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc",
"sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==2.1.3"
},
"mistune": {
@@ -758,24 +758,24 @@
},
"newrelic": {
"hashes": [
- "sha256:13662a5eec5b64a5ec5e220fa1a7ac99c5e7ff6eb868bb303273689e7c90d762",
- "sha256:23450caafa8da15a8a6632642b64a6b1921335a4edd58caeb9f244d740e190b9",
- "sha256:260d9536d4a2c910387aa13c91621e88f96567bb4acf7278608cd1905c25808b",
- "sha256:4f86037cd179a21efaef52784567c3fc951b400f72fe08567b19d47ab31a6a9d",
- "sha256:54c607fb502582484f3bb0e134b19a3634d9b07e79474082d1cde9ff31616844",
- "sha256:57159c99ee6a37f1e487ac5ab68fd67afd1407da1689705b3560f7a903867453",
- "sha256:732dd981bcda6030c9c377eb8765c60ba5f510ba2052b689cb92a964b16290c0",
- "sha256:755d210314f208735a4e53a8ba124ada00fda1fd3ccbdc6d8f01a0d521711c1f",
- "sha256:7eb535500073363d25e14ff4a30379957972339530c600673a7bccb26f009aef",
- "sha256:a8e8cdfd5ac5f8e58aa11909921e65b4da8afb721de996cd4ecd5c372e339c3c",
- "sha256:d9b1ad14417e424b8db24c2da9f4f095b4ea3fac27cf6d5efdeebd140ad07086",
- "sha256:ec36f973d6835eb6a0f27871c0dc8127e8bbc4118c3456dbf3f8ef4aae7d707a",
- "sha256:f2200b2d4e70b0544a5b99b5103c96eecdf204cc2bbbf499106ad93ebd8f6469",
- "sha256:f249f06dbc8b1a160532ece3d6b1989be1ed63c2fe3f569a8913d7c2f0ee9a7e",
- "sha256:fa53d9610ba18a577a3bb62e3fc9e22c13e8b99a88ef8f8f6ebc5538f0390ff0"
+ "sha256:2567ba9e29fd7b9f4c23cf16a5a149097eb0e5da587734c5a40732d75aaec189",
+ "sha256:365d3b1a10d1021217beeb28a93c1356a9feb94bd24f02972691dc71227e40dc",
+ "sha256:4ed36fb91f152128825459eae9a52da364352ea95bcd78b405b0a5b8057b2ed7",
+ "sha256:55a64d2abadf69bbc7bb01178332c4f25247689a97b01a62125d162ea7ec8974",
+ "sha256:722072d57e2d416de68b650235878583a2a8809ea39c7dd5c8c11a19089b7665",
+ "sha256:8a2271b76ea684a63936302579d6085d46a2b54042cb91dc9b0d71a0cd4dd38b",
+ "sha256:9601d886669fe1e0c23bbf91fb68ab23086011816ba96c6dd714c60dc0a74088",
+ "sha256:b6cddd869ac8f7f32f6de8212ae878a21c9e63f2183601d239a76d38c5d5a366",
+ "sha256:cf3b67327e64d2b50aec855821199b2bc46bc0c2d142df269d420748dd49b31b",
+ "sha256:d9af0130e1f1ca032c606d15a6d5558d27273a063b7c53702218b3beccd50b23",
+ "sha256:dbda843100c99ac3291701c0a70fedb705c0b0707800c60b93657d3985aae357",
+ "sha256:ecd0666557419dbe11b04e3b38480b3113b3c4670d42619420d60352a1956dd8",
+ "sha256:f2fd24b32dbf510e4e3fe40b71ad395dd73a4bb9f5eaf59eb5ff22ed76ba2d41",
+ "sha256:f9c9f7842234a51e4a2fdafe42c42ebe0b6b1966279f2f91ec8a9c16480c2236",
+ "sha256:fc975c29548e25805ead794d9de7ab3cb8ba4a6a106098646e1ab03112d1432e"
],
"index": "pypi",
- "version": "==8.9.0"
+ "version": "==8.10.0"
},
"notifications-python-client": {
"hashes": [
@@ -787,7 +787,7 @@
"notifications-utils": {
"editable": true,
"git": "https://github.com/GSA/notifications-utils.git",
- "ref": "86ebeec8985cc94ac9e402e1305fbd28c02b02d6"
+ "ref": "9197f35d9bf07983cd632a8b50f098d5eba762f0"
},
"numpy": {
"hashes": [
@@ -839,35 +839,35 @@
"sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61",
"sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==23.1"
},
"phonenumbers": {
"hashes": [
- "sha256:89671217c706cbaa3ced101deefafa779836feac3e059434d886ac31f09f32c0",
- "sha256:e8ffd86b2e0b844fd6189fdb0927dbe8707cb03b59102cba5532b3ea305cc1bd"
+ "sha256:3d802739a22592e4127139349937753dee9b6a20bdd5d56847cd885bdc766b1f",
+ "sha256:b360c756252805d44b447b5bca6d250cf6bd6c69b6f0f4258f3bfe5ab81bef69"
],
- "version": "==8.13.17"
+ "version": "==8.13.18"
},
"playwright": {
"hashes": [
- "sha256:428a719a6c7e40781c19860ed813840ac2d63678f7587abe12e800ea030d4b7e",
- "sha256:4e396853034742b76654cdab27422155d238f46e4dc6369ea75854fafb935586",
- "sha256:72e80076e595f5fcd8ebd89bf6635ad78e4bafa633119faed8b2568d17dbd398",
- "sha256:84213339f179fd2a70f77ea7faea0616d74871349d556c53a1ecb7dd5097973c",
- "sha256:89ca2261bb00b67d3dff97691cf18f4347ee0529a11e431e47df67b703d4d8fa",
- "sha256:b7c6ddfca2b141b0385387cc56c125b14ea867902c39e3fc650ddd6c429b17da",
- "sha256:ffbb927679b62fad5071439d5fe0840af46ad1844bc44bf80e1a0ad706140c98"
+ "sha256:41f0280472af94c426e941f6a969ff6a7ea156dc15fd01d09ac4b8f092e2346e",
+ "sha256:428fdf9bfff586b73f96df53692d50d422afb93ca4650624f61e8181f548fed2",
+ "sha256:678b9926be2df06321d11a525d4bf08d9f4a5b151354a3b82fe2ac14476322d5",
+ "sha256:68d56efe5ce916bab349177e90726837a6f0cae77ebd6a5200f5333b787b25fb",
+ "sha256:8b5d96aae54289129ab19d3d0e2e431171ae3e5d88d49a10900dcbe569a27d43",
+ "sha256:b476f63251876f1625f490af8d58ec0db90b555c623b7f54105f91d33878c06d",
+ "sha256:b574889ef97b7f44a633aa10d72b8966a850a4354d915fd0bc7e8658e825dd63"
],
"markers": "python_version >= '3.8'",
- "version": "==1.36.0"
+ "version": "==1.37.0"
},
"pluggy": {
"hashes": [
"sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849",
"sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==1.2.0"
},
"prometheus-client": {
@@ -944,17 +944,9 @@
"sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de",
"sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==2.8.0"
},
- "pypdf2": {
- "hashes": [
- "sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440",
- "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==3.0.1"
- },
"pyproj": {
"hashes": [
"sha256:00fab048596c17572fa8980014ef117dbb2a445e6f7ba3b9ddfcc683efc598e7",
@@ -991,7 +983,7 @@
"sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32",
"sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==7.4.0"
},
"pytest-base-url": {
@@ -999,16 +991,16 @@
"sha256:e1e88a4fd221941572ccdcf3bf6c051392d2f8b6cef3e0bc7da95abec4b5346e",
"sha256:ed36fd632c32af9f1c08f2c2835dcf42ca8fcd097d6ed44a09f253d365ad8297"
],
- "markers": "python_version >= '3.7' and python_version < '4.0'",
+ "markers": "python_version < '4.0' and python_full_version >= '3.7.0'",
"version": "==2.0.0"
},
"pytest-playwright": {
"hashes": [
- "sha256:83a896b1b28bfaa081ca9ea27229a06a114e106e2e62fb3d5f06544748fbc1fe",
- "sha256:9bf79c633c97dd1405308b8d3600e6c8c2a200a733e2f36c5a150ba4701936f8"
+ "sha256:68dd0069e2dbf8e6024c6237d51d0277626687d94ba0dd387cea2a94ae4005b9",
+ "sha256:9e9622e5507f8d27a3c7fd07aadcf43122f0086b69d1b3e6728995c8dbc0e44f"
],
"index": "pypi",
- "version": "==0.3.3"
+ "version": "==0.4.2"
},
"python-dateutil": {
"hashes": [
@@ -1039,7 +1031,7 @@
"sha256:70ca6ea68fe63ecc8fa4fcf00ae651fc8a5d02d93dcd12ae6d4fc7ca46c4d395",
"sha256:ce0d46ddb668b3be82f4ed5e503dbc33dd815d83e2eb6824211310d3fb172a27"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==8.0.1"
},
"pytz": {
@@ -1096,20 +1088,28 @@
"markers": "python_version >= '3.6'",
"version": "==6.0.1"
},
+ "radon": {
+ "hashes": [
+ "sha256:632cc032364a6f8bb1010a2f6a12d0f14bc7e5ede76585ef29dc0cecf4cd8859",
+ "sha256:d1ac0053943a893878940fedc8b19ace70386fc9c9bf0a09229a44125ebf45b5"
+ ],
+ "index": "pypi",
+ "version": "==6.0.1"
+ },
"redis": {
"hashes": [
- "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d",
- "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c"
+ "sha256:06570d0b2d84d46c21defc550afbaada381af82f5b83e5b3777600e05d8e2ed0",
+ "sha256:5cea6c0d335c9a7332a460ed8729ceabb4d0c489c7285b0a86dbbf8a017bd120"
],
- "markers": "python_version >= '3.7'",
- "version": "==4.6.0"
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==5.0.0"
},
"requests": {
"hashes": [
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
"sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==2.31.0"
},
"rtreelib": {
@@ -1122,19 +1122,19 @@
},
"s3transfer": {
"hashes": [
- "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346",
- "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"
+ "sha256:b014be3a8a2aab98cfe1abc7229cc5a9a0cf05eb9c1f2b86b230fd8df3f78084",
+ "sha256:cab66d3380cca3e70939ef2255d01cd8aece6a4907a9528740f668c4b0611861"
],
- "markers": "python_version >= '3.7'",
- "version": "==0.6.1"
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==0.6.2"
},
"setuptools": {
"hashes": [
- "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f",
- "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"
+ "sha256:3d4dfa6d95f1b101d695a6160a7626e15583af71a5f52176efa5d39a054d475d",
+ "sha256:3d8083eed2d13afc9426f227b24fd1659489ec107c0e86cec2ffdde5c92e790b"
],
- "markers": "python_version >= '3.7'",
- "version": "==68.0.0"
+ "markers": "python_version >= '3.8'",
+ "version": "==68.1.2"
},
"shapely": {
"hashes": [
@@ -1177,7 +1177,7 @@
"sha256:f32a748703e7bf6e92dfa3d2936b2fbfe76f8ce5f756e24f49ef72d17d26ad02",
"sha256:f470a130d6ddb05b810fc1776d918659407f8d025b7f56d2742a596b6dffa6c7"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==2.0.1"
},
"six": {
@@ -1221,7 +1221,7 @@
"sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36",
"sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"
],
- "markers": "python_version < '3.10'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==4.7.1"
},
"urllib3": {
@@ -1241,11 +1241,11 @@
},
"werkzeug": {
"hashes": [
- "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890",
- "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"
+ "sha256:2b8c0e447b4b9dbcc85dd97b6eeb4dcbaf6c8b6c3be0bd654e25553e0a2157d8",
+ "sha256:effc12dba7f3bd72e605ce49807bbe692bd729c3bb122a3b91747a6ae77df528"
],
"index": "pypi",
- "version": "==2.3.6"
+ "version": "==2.3.7"
},
"wtforms": {
"hashes": [
@@ -1306,19 +1306,19 @@
},
"boto3": {
"hashes": [
- "sha256:87ecac82d2a68430c0292b7946512c8b1f01ea6971b43dc5832582fcb176c0dd",
- "sha256:f2ec3e6f173fe8d141d512ea7d90138db5a58af130773e26ce8e72bdbfd2cddc"
+ "sha256:2761f3249fe25c3ec1a8cd6b95fca2317747503e6f1d127daf6a3d2cdeb25680",
+ "sha256:dc6d72470f6d8926b8cdc10ee7708d7ccdd36d6313c7aa298bc1cf6bedb8921e"
],
- "markers": "python_version >= '3.7'",
- "version": "==1.28.18"
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==1.28.31"
},
"botocore": {
"hashes": [
- "sha256:909db57f5d6ca765fc9dc9dcae962a87566d0123da1d2bd5be32432493d5785e",
- "sha256:c4c01fae2ba32c242ce62175cad719aa49415618560d6e215ed76dab91991dc5"
+ "sha256:1eef14ae98e8662e43f7cf6d993c732793def02644e2d489c5171d3b9269e900",
+ "sha256:950a49c5286fe1f6d72cfbe2910b9ddbdfbb907975ddc41cf38ac9709b4d1291"
],
- "markers": "python_version >= '3.7'",
- "version": "==1.31.18"
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==1.31.31"
},
"cachecontrol": {
"extras": [
@@ -1486,7 +1486,7 @@
"sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
"sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==3.2.0"
},
"cryptography": {
@@ -1515,7 +1515,7 @@
"sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e",
"sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==41.0.3"
},
"cyclonedx-python-lib": {
@@ -1536,11 +1536,11 @@
},
"exceptiongroup": {
"hashes": [
- "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5",
- "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"
+ "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9",
+ "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"
],
"index": "pypi",
- "version": "==1.1.2"
+ "version": "==1.1.3"
},
"execnet": {
"hashes": [
@@ -1605,6 +1605,72 @@
"markers": "python_version >= '3.7'",
"version": "==3.1.32"
},
+ "greenlet": {
+ "hashes": [
+ "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a",
+ "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a",
+ "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43",
+ "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33",
+ "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8",
+ "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088",
+ "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca",
+ "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343",
+ "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645",
+ "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db",
+ "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df",
+ "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3",
+ "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86",
+ "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2",
+ "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a",
+ "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf",
+ "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7",
+ "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394",
+ "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40",
+ "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3",
+ "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6",
+ "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74",
+ "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0",
+ "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3",
+ "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91",
+ "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5",
+ "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9",
+ "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8",
+ "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b",
+ "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6",
+ "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb",
+ "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73",
+ "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b",
+ "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df",
+ "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9",
+ "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f",
+ "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0",
+ "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857",
+ "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a",
+ "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249",
+ "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30",
+ "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292",
+ "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b",
+ "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d",
+ "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b",
+ "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c",
+ "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca",
+ "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7",
+ "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75",
+ "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae",
+ "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b",
+ "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470",
+ "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564",
+ "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9",
+ "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099",
+ "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0",
+ "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5",
+ "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19",
+ "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1",
+ "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==2.0.2"
+ },
"html5lib": {
"hashes": [
"sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d",
@@ -1626,7 +1692,7 @@
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==2.0.0"
},
"isort": {
@@ -1661,7 +1727,7 @@
"sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980",
"sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==1.0.1"
},
"markdown-it-py": {
@@ -1669,7 +1735,7 @@
"sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1",
"sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"
],
- "markers": "python_full_version >= '3.8.0'",
+ "markers": "python_version >= '3.8'",
"version": "==3.0.0"
},
"markupsafe": {
@@ -1725,7 +1791,7 @@
"sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc",
"sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==2.1.3"
},
"mccabe": {
@@ -1733,7 +1799,7 @@
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
],
- "markers": "python_full_version >= '3.6.0'",
+ "markers": "python_version >= '3.6'",
"version": "==0.7.0"
},
"mdurl": {
@@ -1833,7 +1899,7 @@
"sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61",
"sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==23.1"
},
"pbr": {
@@ -1873,15 +1939,28 @@
"sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526",
"sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3"
],
- "markers": "python_full_version >= '3.6.0'",
+ "markers": "python_version >= '3.6'",
"version": "==32.0.1"
},
+ "playwright": {
+ "hashes": [
+ "sha256:41f0280472af94c426e941f6a969ff6a7ea156dc15fd01d09ac4b8f092e2346e",
+ "sha256:428fdf9bfff586b73f96df53692d50d422afb93ca4650624f61e8181f548fed2",
+ "sha256:678b9926be2df06321d11a525d4bf08d9f4a5b151354a3b82fe2ac14476322d5",
+ "sha256:68d56efe5ce916bab349177e90726837a6f0cae77ebd6a5200f5333b787b25fb",
+ "sha256:8b5d96aae54289129ab19d3d0e2e431171ae3e5d88d49a10900dcbe569a27d43",
+ "sha256:b476f63251876f1625f490af8d58ec0db90b555c623b7f54105f91d33878c06d",
+ "sha256:b574889ef97b7f44a633aa10d72b8966a850a4354d915fd0bc7e8658e825dd63"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.37.0"
+ },
"pluggy": {
"hashes": [
"sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849",
"sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==1.2.0"
},
"py-serializable": {
@@ -1897,7 +1976,7 @@
"sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0",
"sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8"
],
- "markers": "python_full_version >= '3.8.0'",
+ "markers": "python_version >= '3.8'",
"version": "==2.11.0"
},
"pycparser": {
@@ -1907,21 +1986,28 @@
],
"version": "==2.21"
},
+ "pyee": {
+ "hashes": [
+ "sha256:2770c4928abc721f46b705e6a72b0c59480c4a69c9a83ca0b00bb994f1ea4b32",
+ "sha256:9f066570130c554e9cc12de5a9d86f57c7ee47fece163bbdaa3e9c933cfbdfa5"
+ ],
+ "version": "==9.0.4"
+ },
"pyflakes": {
"hashes": [
"sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774",
"sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"
],
- "markers": "python_full_version >= '3.8.0'",
+ "markers": "python_version >= '3.8'",
"version": "==3.1.0"
},
"pygments": {
"hashes": [
- "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c",
- "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"
+ "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692",
+ "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"
],
"markers": "python_version >= '3.7'",
- "version": "==2.15.1"
+ "version": "==2.16.1"
},
"pyparsing": {
"hashes": [
@@ -1936,9 +2022,17 @@
"sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32",
"sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==7.4.0"
},
+ "pytest-base-url": {
+ "hashes": [
+ "sha256:e1e88a4fd221941572ccdcf3bf6c051392d2f8b6cef3e0bc7da95abec4b5346e",
+ "sha256:ed36fd632c32af9f1c08f2c2835dcf42ca8fcd097d6ed44a09f253d365ad8297"
+ ],
+ "markers": "python_version < '4.0' and python_full_version >= '3.7.0'",
+ "version": "==2.0.0"
+ },
"pytest-env": {
"hashes": [
"sha256:5e533273f4d9e6a41c3a3120e0c7944aae5674fa773b329f00a5eb1f23c53a38",
@@ -1955,6 +2049,14 @@
"index": "pypi",
"version": "==3.11.1"
},
+ "pytest-playwright": {
+ "hashes": [
+ "sha256:68dd0069e2dbf8e6024c6237d51d0277626687d94ba0dd387cea2a94ae4005b9",
+ "sha256:9e9622e5507f8d27a3c7fd07aadcf43122f0086b69d1b3e6728995c8dbc0e44f"
+ ],
+ "index": "pypi",
+ "version": "==0.4.2"
+ },
"pytest-xdist": {
"hashes": [
"sha256:d5ee0520eb1b7bcca50a60a518ab7a7707992812c578198f8b44fdfac78e8c93",
@@ -1971,6 +2073,14 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.2"
},
+ "python-slugify": {
+ "hashes": [
+ "sha256:70ca6ea68fe63ecc8fa4fcf00ae651fc8a5d02d93dcd12ae6d4fc7ca46c4d395",
+ "sha256:ce0d46ddb668b3be82f4ed5e503dbc33dd815d83e2eb6824211310d3fb172a27"
+ ],
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==8.0.1"
+ },
"pyyaml": {
"hashes": [
"sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
@@ -2022,7 +2132,7 @@
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
"sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
],
- "markers": "python_version >= '3.7'",
+ "markers": "python_full_version >= '3.7.0'",
"version": "==2.31.0"
},
"requests-mock": {
@@ -2051,11 +2161,11 @@
},
"s3transfer": {
"hashes": [
- "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346",
- "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"
+ "sha256:b014be3a8a2aab98cfe1abc7229cc5a9a0cf05eb9c1f2b86b230fd8df3f78084",
+ "sha256:cab66d3380cca3e70939ef2255d01cd8aece6a4907a9528740f668c4b0611861"
],
- "markers": "python_version >= '3.7'",
- "version": "==0.6.1"
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==0.6.2"
},
"six": {
"hashes": [
@@ -2070,7 +2180,7 @@
"sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94",
"sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"
],
- "markers": "python_full_version >= '3.6.0'",
+ "markers": "python_version >= '3.6'",
"version": "==5.0.0"
},
"sortedcontainers": {
@@ -2093,9 +2203,16 @@
"sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d",
"sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c"
],
- "markers": "python_full_version >= '3.8.0'",
+ "markers": "python_version >= '3.8'",
"version": "==5.1.0"
},
+ "text-unidecode": {
+ "hashes": [
+ "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8",
+ "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"
+ ],
+ "version": "==1.3"
+ },
"toml": {
"hashes": [
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
@@ -2119,6 +2236,14 @@
],
"version": "==6.0.12.11"
},
+ "typing-extensions": {
+ "hashes": [
+ "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36",
+ "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"
+ ],
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==4.7.1"
+ },
"urllib3": {
"hashes": [
"sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f",
@@ -2136,11 +2261,11 @@
},
"werkzeug": {
"hashes": [
- "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890",
- "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"
+ "sha256:2b8c0e447b4b9dbcc85dd97b6eeb4dcbaf6c8b6c3be0bd654e25553e0a2157d8",
+ "sha256:effc12dba7f3bd72e605ce49807bbe692bd729c3bb122a3b91747a6ae77df528"
],
"index": "pypi",
- "version": "==2.3.6"
+ "version": "==2.3.7"
},
"xmltodict": {
"hashes": [
diff --git a/app/__init__.py b/app/__init__.py
index 035f585d4..5f244f630 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -34,7 +34,7 @@ from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
from werkzeug.exceptions import abort
from werkzeug.local import LocalProxy
-from app import proxy_fix, webauthn_server
+from app import proxy_fix
from app.asset_fingerprinter import asset_fingerprinter
from app.config import configs
from app.custom_auth import CustomBasicAuth
@@ -257,7 +257,6 @@ def create_app(application):
force_https=(application.config['HTTP_PROTOCOL'] == 'https')
)
logging.init_app(application)
- webauthn_server.init_app(application)
login_manager.login_view = 'main.sign_in'
login_manager.login_message_category = 'default'
diff --git a/app/assets/javascripts/authenticateSecurityKey.js b/app/assets/javascripts/authenticateSecurityKey.js
deleted file mode 100644
index 2d68555b1..000000000
--- a/app/assets/javascripts/authenticateSecurityKey.js
+++ /dev/null
@@ -1,74 +0,0 @@
-(function (window) {
- "use strict";
-
- window.GOVUK.Modules.AuthenticateSecurityKey = function () {
- this.start = function (component) {
- $(component)
- .on('click', function (event) {
- event.preventDefault();
-
- // hide any existing error prompt
- window.GOVUK.ErrorBanner.hideBanner();
-
- fetch('/webauthn/authenticate')
- .then(response => {
- if (!response.ok) {
- throw Error(response.statusText);
- }
-
- return response.arrayBuffer();
- })
- .then(data => {
- var options = window.CBOR.decode(data);
- // triggers browser dialogue to login with authenticator
- return window.navigator.credentials.get(options);
- })
- .then(credential => {
- const currentURL = new URL(window.location.href);
-
- // create authenticateURL from admin hostname plus /webauthn/authenticate path
- const authenticateURL = new URL('/webauthn/authenticate', window.location.href);
-
- const nextUrl = currentURL.searchParams.get('next');
- if (nextUrl) {
- // takes nextUrl from the query string on the current browser URL
- // (which should be /two-factor-webauthn) and pass it through to
- // the POST. put it in a query string so it's consistent with how
- // the other login flows manage it
- authenticateURL.searchParams.set('next', nextUrl);
- }
-
- return fetch(authenticateURL, {
- method: 'POST',
- headers: { 'X-CSRFToken': component.data('csrfToken') },
- body: window.CBOR.encode({
- credentialId: new Uint8Array(credential.rawId),
- authenticatorData: new Uint8Array(credential.response.authenticatorData),
- signature: new Uint8Array(credential.response.signature),
- clientDataJSON: new Uint8Array(credential.response.clientDataJSON),
- })
- });
- })
- .then(response => {
- if (!response.ok) {
- throw Error(response.statusText);
- }
-
- return response.arrayBuffer();
- })
- .then(cbor => {
- return Promise.resolve(window.CBOR.decode(cbor));
- })
- .then(data => {
- window.location.assign(data.redirect_url);
- })
- .catch(error => {
- console.error(error);
- // some browsers will show an error dialogue for some
- // errors; to be safe we always display an error message on the page.
- window.GOVUK.ErrorBanner.showBanner();
- });
- });
- };
- };
-}) (window);
diff --git a/app/assets/javascripts/registerSecurityKey.js b/app/assets/javascripts/registerSecurityKey.js
deleted file mode 100644
index b7c93bef0..000000000
--- a/app/assets/javascripts/registerSecurityKey.js
+++ /dev/null
@@ -1,58 +0,0 @@
-(function(window) {
- "use strict";
-
- window.GOVUK.Modules.RegisterSecurityKey = function() {
- this.start = function(component) {
- $(component)
- .on('click', function(event) {
- event.preventDefault();
-
- // hide any existing error prompt
- window.GOVUK.ErrorBanner.hideBanner();
-
- fetch('/webauthn/register')
- .then((response) => {
- if (!response.ok) {
- throw Error(response.statusText);
- }
-
- return response.arrayBuffer();
- })
- .then((data) => {
- var options = window.CBOR.decode(data);
- // triggers browser dialogue to select authenticator
- return window.navigator.credentials.create(options);
- })
- .then((credential) => {
- return postWebAuthnCreateResponse(
- credential.response, component.data('csrfToken')
- );
- })
- .then((response) => {
- if (!response.ok) {
- throw Error(response.statusText);
- }
-
- window.location.reload();
- })
- .catch((error) => {
- console.error(error);
- // some browsers will show an error dialogue for some
- // errors; to be safe we always display an error message on the page.
- window.GOVUK.ErrorBanner.showBanner();
- });
- });
- };
- };
-
- function postWebAuthnCreateResponse(response, csrf_token) {
- return fetch('/webauthn/register', {
- method: 'POST',
- headers: { 'X-CSRFToken': csrf_token },
- body: window.CBOR.encode({
- attestationObject: new Uint8Array(response.attestationObject),
- clientDataJSON: new Uint8Array(response.clientDataJSON),
- })
- });
- }
-})(window);
diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss
index 1d7c104db..0b97e96b1 100644
--- a/app/assets/stylesheets/main.scss
+++ b/app/assets/stylesheets/main.scss
@@ -80,7 +80,6 @@ $path: '/static/images/';
@import 'views/send';
@import 'views/get_started';
@import 'views/history';
-@import 'views/webauthn';
// TODO: break this up
@import 'app';
diff --git a/app/assets/stylesheets/views/webauthn.scss b/app/assets/stylesheets/views/webauthn.scss
deleted file mode 100644
index 33d1ee24e..000000000
--- a/app/assets/stylesheets/views/webauthn.scss
+++ /dev/null
@@ -1,40 +0,0 @@
-.webauthn__no-js {
- .js-enabled & {
- display: none;
- }
-}
-
-.webauthn__api-missing {
- display: none;
-
- .js-enabled & {
- display: block;
- }
-
- .js-enabled.webauthn-api-enabled & {
- display: none;
- }
-}
-
-.webauthn__api-required {
- display: none;
-
- .webauthn-api-enabled & {
- display: block;
- }
-}
-
-.webauthn-illustration {
-
- box-sizing: border-box;
- width: 100%;
- height: 100%;
- margin: govuk-spacing(6) auto 0 auto;
- padding: 0 govuk-spacing(9) 0 govuk-spacing(9);
-
- @include govuk-media-query($from: tablet) {
- margin: govuk-spacing(9) auto 0 auto;
- padding: 0;
- }
-
-}
diff --git a/app/formatters.py b/app/formatters.py
index 6525ead4b..c1eadb657 100644
--- a/app/formatters.py
+++ b/app/formatters.py
@@ -487,7 +487,6 @@ def format_auth_type(auth_type, with_indefinite_article=False):
indefinite_article, auth_type = {
'email_auth': ('an', 'Email link'),
'sms_auth': ('a', 'Text message code'),
- 'webauthn_auth': ('a', 'Security key'),
}[auth_type]
if with_indefinite_article:
diff --git a/app/main/__init__.py b/app/main/__init__.py
index 9d2e0730b..9ab666c27 100644
--- a/app/main/__init__.py
+++ b/app/main/__init__.py
@@ -39,5 +39,4 @@ from app.main.views import ( # noqa isort:skip
uploads,
user_profile,
verify,
- webauthn_credentials,
)
diff --git a/app/main/forms.py b/app/main/forms.py
index f396e2b51..94fd57433 100644
--- a/app/main/forms.py
+++ b/app/main/forms.py
@@ -1058,10 +1058,6 @@ class BasePermissionsForm(StripWhitespaceForm):
login_authentication=user.auth_type
)
- # If a user logs in with a security key, we generally don't want a service admin to be able to change this.
- # As well as enforcing this in the backend, we need to delete the auth radios to prevent validation errors.
- if user.webauthn_auth:
- del form.login_authentication
return form
diff --git a/app/main/views/find_users.py b/app/main/views/find_users.py
index d01d2c6be..2ca737234 100644
--- a/app/main/views/find_users.py
+++ b/app/main/views/find_users.py
@@ -1,4 +1,4 @@
-from flask import abort, flash, redirect, render_template, request, url_for
+from flask import flash, redirect, render_template, request, url_for
from flask_login import current_user
from notifications_python_client.errors import HTTPError
@@ -56,8 +56,6 @@ def archive_user(user_id):
@user_is_platform_admin
def change_user_auth(user_id):
user = User.from_id(user_id)
- if user.webauthn_auth:
- abort(403)
form = AuthTypeForm(auth_type=user.auth_type)
diff --git a/app/main/views/manage_users.py b/app/main/views/manage_users.py
index b85e8980d..cfd6b5488 100644
--- a/app/main/views/manage_users.py
+++ b/app/main/views/manage_users.py
@@ -141,9 +141,8 @@ def edit_user_permissions(service_id, user_id):
folder_permissions=form.folder_permissions.data,
set_by_id=current_user.id,
)
- # Only change the auth type if this is supported for a service. If a user logs in with a
- # security key, we generally don't want them to be able to use something less secure.
- if service_has_email_auth and not user.auth_type == 'webauthn_auth':
+ # Only change the auth type if this is supported for a service.
+ if service_has_email_auth:
user.update(auth_type=form.login_authentication.data)
return redirect(url_for('.manage_users', service_id=service_id))
diff --git a/app/main/views/new_password.py b/app/main/views/new_password.py
index b44d1e8b1..55895e13d 100644
--- a/app/main/views/new_password.py
+++ b/app/main/views/new_password.py
@@ -47,8 +47,6 @@ def new_password(token):
if user.email_auth:
# they've just clicked an email link, so have done an email auth journey anyway. Just log them in.
return log_in_user(user.id)
- elif user.webauthn_auth:
- return redirect(url_for('main.two_factor_webauthn', next=request.args.get('next')))
else:
# send user a 2fa sms code
user.send_verify_code()
diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py
index edf3d4950..05ce1fe6a 100644
--- a/app/main/views/sign_in.py
+++ b/app/main/views/sign_in.py
@@ -59,8 +59,6 @@ def sign_in():
return redirect(url_for('.two_factor_sms', next=redirect_url))
if user.email_auth:
return redirect(url_for('.two_factor_email_sent', next=redirect_url))
- if user.webauthn_auth:
- return redirect(url_for('.two_factor_webauthn', next=redirect_url))
# Vague error message for login in case of user not known, locked, inactive or password not verified
flash(Markup(
diff --git a/app/main/views/two_factor.py b/app/main/views/two_factor.py
index b2d9ae004..1f9e52ebb 100644
--- a/app/main/views/two_factor.py
+++ b/app/main/views/two_factor.py
@@ -1,7 +1,6 @@
import json
from flask import (
- abort,
current_app,
redirect,
render_template,
@@ -88,18 +87,6 @@ def two_factor_sms():
return render_template('views/two-factor-sms.html', form=form, redirect_url=redirect_url)
-@main.route('/two-factor-webauthn', methods=['GET'])
-@redirect_to_sign_in
-def two_factor_webauthn():
- user_id = session['user_details']['id']
- user = User.from_id(user_id)
-
- if not user.webauthn_auth:
- abort(403)
-
- return render_template('views/two-factor-webauthn.html')
-
-
@main.route('/re-validate-email', methods=['GET'])
def revalidate_email_sent():
title = 'Email resent' if request.args.get('email_resent') else 'Check your email'
diff --git a/app/main/views/user_profile.py b/app/main/views/user_profile.py
index d461b72bc..34558b238 100644
--- a/app/main/views/user_profile.py
+++ b/app/main/views/user_profile.py
@@ -11,7 +11,6 @@ from flask import (
url_for,
)
from flask_login import current_user
-from notifications_python_client.errors import HTTPError
from notifications_utils.url_safe_token import check_token
from app import user_api_client
@@ -25,7 +24,6 @@ from app.main.forms import (
ChangeMobileNumberForm,
ChangeNameForm,
ChangePasswordForm,
- ChangeSecurityKeyNameForm,
ConfirmPasswordForm,
ServiceOnOffSettingForm,
TwoFactorForm,
@@ -266,77 +264,3 @@ def user_profile_disable_platform_admin_view():
'views/user-profile/disable-platform-admin-view.html',
form=form
)
-
-
-@main.route("/user-profile/security-keys", methods=['GET'])
-@user_is_logged_in
-def user_profile_security_keys():
- if not current_user.can_use_webauthn:
- abort(403)
-
- return render_template(
- 'views/user-profile/security-keys.html',
- )
-
-
-@main.route(
- "/user-profile/security-keys//manage",
- methods=['GET', 'POST'],
- endpoint="user_profile_manage_security_key"
-)
-@main.route(
- "/user-profile/security-keys//delete",
- methods=['GET'],
- endpoint="user_profile_confirm_delete_security_key"
-)
-@user_is_logged_in
-def user_profile_manage_security_key(key_id):
- if not current_user.can_use_webauthn:
- abort(403)
-
- security_key = current_user.webauthn_credentials.by_id(key_id)
-
- if not security_key:
- abort(404)
-
- form = ChangeSecurityKeyNameForm(security_key_name=security_key.name)
-
- if form.validate_on_submit():
- if form.security_key_name.data != security_key.name:
- user_api_client.update_webauthn_credential_name_for_user(
- user_id=current_user.id,
- credential_id=key_id,
- new_name_for_credential=form.security_key_name.data
- )
- return redirect(url_for('.user_profile_security_keys'))
-
- if (request.endpoint == "main.user_profile_confirm_delete_security_key"):
- flash("Are you sure you want to delete this security key?", 'delete')
-
- return render_template(
- 'views/user-profile/manage-security-key.html',
- security_key=security_key,
- form=form
- )
-
-
-@main.route("/user-profile/security-keys//delete", methods=['POST'])
-@user_is_logged_in
-def user_profile_delete_security_key(key_id):
- if not current_user.can_use_webauthn:
- abort(403)
-
- try:
- user_api_client.delete_webauthn_credential_for_user(
- user_id=current_user.id,
- credential_id=key_id
- )
- except HTTPError as e:
- message = "Cannot delete last remaining webauthn credential for user"
- if e.message == message:
- flash("You cannot delete your last security key.")
- return redirect(url_for('.user_profile_manage_security_key', key_id=key_id))
- else:
- raise e
-
- return redirect(url_for('.user_profile_security_keys'))
diff --git a/app/main/views/webauthn_credentials.py b/app/main/views/webauthn_credentials.py
deleted file mode 100644
index 38895ac08..000000000
--- a/app/main/views/webauthn_credentials.py
+++ /dev/null
@@ -1,161 +0,0 @@
-from fido2 import cbor
-from fido2.client import ClientData
-from fido2.ctap2 import AuthenticatorData
-from flask import abort, current_app, flash, redirect, request, session, url_for
-from flask_login import current_user
-
-from app.main import main
-from app.models.user import User
-from app.models.webauthn_credential import RegistrationError, WebAuthnCredential
-from app.notify_client.user_api_client import user_api_client
-from app.utils.login import (
- email_needs_revalidating,
- log_in_user,
- redirect_to_sign_in,
-)
-from app.utils.user import user_is_logged_in
-
-
-@main.route('/webauthn/register')
-@user_is_logged_in
-def webauthn_begin_register():
- if not current_user.can_use_webauthn:
- abort(403)
-
- server = current_app.webauthn_server
-
- registration_data, state = server.register_begin(
- {
- "id": bytes(current_user.id, 'utf-8'),
- "name": current_user.email_address,
- "displayName": current_user.name,
- },
- credentials=current_user.webauthn_credentials.as_cbor,
- user_verification="discouraged", # don't ask for PIN
- authenticator_attachment="cross-platform",
- )
-
- session["webauthn_registration_state"] = state
- return cbor.encode(registration_data)
-
-
-@main.route('/webauthn/register', methods=['POST'])
-@user_is_logged_in
-def webauthn_complete_register():
- if 'webauthn_registration_state' not in session:
- return cbor.encode("No registration in progress"), 400
-
- try:
- credential = WebAuthnCredential.from_registration(
- session.pop("webauthn_registration_state"),
- cbor.decode(request.get_data()),
- )
- except RegistrationError as e:
- current_app.logger.info(f'User {current_user.id} could not register a new webauthn token - {e}')
- abort(400)
-
- current_user.create_webauthn_credential(credential)
- current_user.update(auth_type='webauthn_auth')
-
- flash((
- 'Registration complete. Next time you sign in to Notify '
- 'you’ll be asked to use your security key.'
- ), 'default_with_tick')
-
- return cbor.encode('')
-
-
-@main.route('/webauthn/authenticate', methods=['GET'])
-@redirect_to_sign_in
-def webauthn_begin_authentication():
- """
- Initiate the authentication flow. This is called after the user clicks the "Check security key" button.
-
- 1. Get the user's credentials out of the database to present to the browser. The browser will only let you use a
- credential in that list.
- 2. Call webauthn_server.authenticate_begin. This returns the authentication data, which includes the challenge and
- the origin domain to authenticate with. This also returns the state, which we store in the cookie so we can ensure
- the challenge is correct in webauthn_complete_authentication
- """
- # get user from session
- user_to_login = User.from_id(session['user_details']['id'])
-
- if not user_to_login.webauthn_auth:
- abort(403)
-
- authentication_data, state = current_app.webauthn_server.authenticate_begin(
- credentials=user_to_login.webauthn_credentials.as_cbor,
- user_verification="discouraged", # don't ask for PIN
- )
- session["webauthn_authentication_state"] = state
- return cbor.encode(authentication_data)
-
-
-@main.route('/webauthn/authenticate', methods=['POST'])
-@redirect_to_sign_in
-def webauthn_complete_authentication():
- """
- Complete the authentication flow. This is called after the user taps on their security key.
-
- 1. Try verifying the signed challenge returned from the browser with each public key we have in the database for
- that user.
- 2. If succesful, log the user in, setting up the session etc. Then return the URL they should be redirected to.
- """
- user_id = session['user_details']['id']
- user_to_login = User.from_id(user_id)
-
- _verify_webauthn_authentication(user_to_login)
- redirect = _complete_webauthn_login_attempt(user_to_login)
-
- return cbor.encode({'redirect_url': redirect.location}), 200
-
-
-def _verify_webauthn_authentication(user):
- """
- Check that the presented security key is valid, has signed the right challenge, and belongs to the user
- we're trying to log in.
- """
- state = session.pop("webauthn_authentication_state")
- request_data = cbor.decode(request.get_data())
-
- try:
- current_app.webauthn_server.authenticate_complete(
- state=state,
- credentials=user.webauthn_credentials.as_cbor,
- credential_id=request_data['credentialId'],
- client_data=ClientData(request_data['clientDataJSON']),
- auth_data=AuthenticatorData(request_data['authenticatorData']),
- signature=request_data['signature']
- )
- except ValueError as exc:
- # We don't expect to reach this case in normal situations - normally errors (such as using the wrong
- # security key) will be caught in the browser inside `window.navigator.credentials.get`, and the js will
- # error first meaning it doesn't send the POST request to this method. If this method is called but the key
- # couldn't be authenticated, something went wrong along the way, probably:
- # * The browser didn't implement the webauthn standard correctly, and let something through it shouldn't have
- # * The key itself is in some way corrupted, or of lower security standard
- current_app.logger.info(f'User {user.id} could not sign in using their webauthn token - {exc}')
- user.complete_webauthn_login_attempt(is_successful=False)
- abort(403)
-
-
-def _complete_webauthn_login_attempt(user):
- """
- * check the user hasn't gone over their max logins
- * check that the user's email is validated
- * if succesful, update current_session_id, log in date, and then redirect
- """
- redirect_url = request.args.get('next')
-
- # normally API handles this when verifying an sms or email code but since the webauthn logic happens in the
- # admin we need a separate call that just finalises the login in the database
- logged_in, _ = user.complete_webauthn_login_attempt()
- if not logged_in:
- # user account is locked as too many failed logins
- abort(403)
-
- if email_needs_revalidating(user):
- user_api_client.send_verify_code(user.id, 'email', None, redirect_url)
- return redirect(url_for('.revalidate_email_sent', next=redirect_url))
-
- return log_in_user(user.id)
diff --git a/app/models/user.py b/app/models/user.py
index e62006348..b4a30002c 100644
--- a/app/models/user.py
+++ b/app/models/user.py
@@ -11,7 +11,6 @@ from app.event_handlers import (
)
from app.models import JSONModel, ModelList
from app.models.organization import Organization, Organizations
-from app.models.webauthn_credential import WebAuthnCredentials
from app.notify_client import InviteTokenError
from app.notify_client.invite_api_client import invite_api_client
from app.notify_client.org_invite_api_client import org_invite_api_client
@@ -37,7 +36,6 @@ class User(JSONModel, UserMixin):
MAX_FAILED_LOGIN_COUNT = 10
ALLOWED_PROPERTIES = {
- 'can_use_webauthn',
'id',
'name',
'email_address',
@@ -180,10 +178,6 @@ class User(JSONModel, UserMixin):
def email_auth(self):
return self.auth_type == 'email_auth'
- @property
- def webauthn_auth(self):
- return self.auth_type == 'webauthn_auth'
-
def reset_failed_login_count(self):
user_api_client.reset_failed_login_count(self.id)
@@ -371,15 +365,6 @@ class User(JSONModel, UserMixin):
'@nhs.uk', '.nhs.uk', '@nhs.net', '.nhs.net',
))
- @property
- def webauthn_credentials(self):
- return WebAuthnCredentials(self.id)
-
- def create_webauthn_credential(self, credential):
- user_api_client.create_webauthn_credential_for_user(
- self.id, credential
- )
-
def serialize(self):
dct = {
"id": self.id,
@@ -456,9 +441,6 @@ class User(JSONModel, UserMixin):
self.id,
)
- def complete_webauthn_login_attempt(self, is_successful=True):
- return user_api_client.complete_webauthn_login_attempt(self.id, is_successful)
-
def is_editable_by(self, other_user):
if other_user == self:
return False
diff --git a/app/models/webauthn_credential.py b/app/models/webauthn_credential.py
deleted file mode 100644
index c5bee7a71..000000000
--- a/app/models/webauthn_credential.py
+++ /dev/null
@@ -1,76 +0,0 @@
-import base64
-
-from fido2 import cbor
-from fido2.client import ClientData
-from fido2.cose import UnsupportedKey
-from fido2.ctap2 import AttestationObject, AttestedCredentialData
-from flask import current_app
-
-from app.models import JSONModel, ModelList
-from app.notify_client.user_api_client import user_api_client
-
-
-class RegistrationError(Exception):
- pass
-
-
-class WebAuthnCredential(JSONModel):
- ALLOWED_PROPERTIES = {
- 'id',
- 'name',
- 'credential_data', # contains public key and credential ID for auth
- 'registration_response', # sent to API for later auditing (not used)
- 'created_at',
- 'updated_at'
- }
-
- @classmethod
- def from_registration(cls, state, response):
- server = current_app.webauthn_server
-
- try:
- auth_data = server.register_complete(
- state,
- ClientData(response["clientDataJSON"]),
- AttestationObject(response["attestationObject"]),
- )
- except ValueError as e:
- raise RegistrationError(e)
-
- if isinstance(auth_data.credential_data.public_key, UnsupportedKey):
- raise RegistrationError("Encryption algorithm not supported")
-
- return cls({
- 'name': 'Unnamed key',
- 'credential_data': base64.b64encode(
- cbor.encode(auth_data.credential_data),
- ).decode('utf-8'),
- 'registration_response': base64.b64encode(
- cbor.encode(response),
- ).decode('utf-8')
- })
-
- def to_credential_data(self):
- return AttestedCredentialData(
- cbor.decode(base64.b64decode(self.credential_data.encode()))
- )
-
- def serialize(self):
- return {
- 'name': self.name,
- 'credential_data': self.credential_data,
- 'registration_response': self.registration_response,
- }
-
-
-class WebAuthnCredentials(ModelList):
-
- model = WebAuthnCredential
- client_method = user_api_client.get_webauthn_credentials_for_user
-
- @property
- def as_cbor(self):
- return [credential.to_credential_data() for credential in self]
-
- def by_id(self, key_id):
- return next((key for key in self if key.id == key_id), None)
diff --git a/app/navigation.py b/app/navigation.py
index d2af07e68..1fe7ba3ad 100644
--- a/app/navigation.py
+++ b/app/navigation.py
@@ -118,7 +118,6 @@ class HeaderNavigation(Navigation):
'two_factor_email',
'two_factor_email_sent',
'two_factor_email_interstitial',
- 'two_factor_webauthn',
'verify',
'verify_email',
},
diff --git a/app/notify_client/user_api_client.py b/app/notify_client/user_api_client.py
index 55071175f..326882f1c 100644
--- a/app/notify_client/user_api_client.py
+++ b/app/notify_client/user_api_client.py
@@ -132,20 +132,6 @@ class UserApiClient(NotifyAdminAPIClient):
return False, e.message
raise
- @cache.delete('user-{user_id}')
- def complete_webauthn_login_attempt(self, user_id, is_successful):
- data = {'successful': is_successful}
- endpoint = f'/user/{user_id}/complete/webauthn-login'
- try:
- current_app.logger.warn(f"Sending webauthn-login for {user_id}")
- self.post(endpoint, data=data)
- return True, ''
- except HTTPError as e:
- if e.status_code == 403:
- current_app.logger.error(f"Webauthn-login attempt for {user_id} was invalid")
- return False, e.message
- raise
-
def get_users_for_service(self, service_id):
endpoint = '/service/{}/users'.format(service_id)
return self.get(endpoint)['data']
@@ -219,25 +205,5 @@ class UserApiClient(NotifyAdminAPIClient):
endpoint = '/user/{}/organizations-and-services'.format(user_id)
return self.get(endpoint)
- def get_webauthn_credentials_for_user(self, user_id):
- endpoint = f'/user/{user_id}/webauthn'
-
- return self.get(endpoint)['data']
-
- def create_webauthn_credential_for_user(self, user_id, credential):
- endpoint = f'/user/{user_id}/webauthn'
-
- return self.post(endpoint, data=credential.serialize())
-
- def update_webauthn_credential_name_for_user(self, *, user_id, credential_id, new_name_for_credential):
- endpoint = f'/user/{user_id}/webauthn/{credential_id}'
-
- return self.post(endpoint, data={"name": new_name_for_credential})
-
- def delete_webauthn_credential_for_user(self, *, user_id, credential_id):
- endpoint = f'/user/{user_id}/webauthn/{credential_id}'
-
- return self.delete(endpoint)
-
user_api_client = UserApiClient()
diff --git a/app/s3_client/__init__.py b/app/s3_client/__init__.py
index 4b64f0f5c..540366200 100644
--- a/app/s3_client/__init__.py
+++ b/app/s3_client/__init__.py
@@ -1,7 +1,18 @@
import botocore
from boto3 import Session
+from botocore.config import Config
from flask import current_app
+AWS_CLIENT_CONFIG = Config(
+ # This config is required to enable S3 to connect to FIPS-enabled
+ # endpoints. See https://aws.amazon.com/compliance/fips/ for more
+ # information.
+ s3={
+ 'addressing_style': 'virtual',
+ },
+ use_fips_endpoint=True
+)
+
def get_s3_object(
bucket_name,
@@ -11,8 +22,12 @@ def get_s3_object(
region,
):
# To inspect contents: obj.get()['Body'].read().decode('utf-8')
- session = Session(aws_access_key_id=access_key, aws_secret_access_key=secret_key, region_name=region)
- s3 = session.resource('s3')
+ session = Session(
+ aws_access_key_id=access_key,
+ aws_secret_access_key=secret_key,
+ region_name=region
+ )
+ s3 = session.resource('s3', config=AWS_CLIENT_CONFIG)
obj = s3.Object(bucket_name, filename)
return obj
diff --git a/app/templates/components/webauthn-api-check.html b/app/templates/components/webauthn-api-check.html
deleted file mode 100644
index 19bc56bc5..000000000
--- a/app/templates/components/webauthn-api-check.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{% macro webauthn_api_check() %}
-
-{% endmacro %}
diff --git a/app/templates/views/check/ok.html b/app/templates/views/check/ok.html
index e62e9298e..3b8ca5fcd 100644
--- a/app/templates/views/check/ok.html
+++ b/app/templates/views/check/ok.html
@@ -93,8 +93,4 @@
Only showing the first {{ count_of_displayed_recipients }} rows
{% endif %}
-
-
Messages remaining today / Messages left if list is sent
-
{{ remaining_messages }} / {{ remaining_messages - count_of_recipients }}
-
{% endblock %}
diff --git a/app/templates/views/find-users/user-information.html b/app/templates/views/find-users/user-information.html
index d4ba0bf15..52c7efbb0 100644
--- a/app/templates/views/find-users/user-information.html
+++ b/app/templates/views/find-users/user-information.html
@@ -49,11 +49,9 @@
Authentication
{{ user.auth_type | format_auth_type }}
- {% if user.auth_type != 'webauthn_auth' %}
-
- Change authentication for this user
-
- {% endif %}
+
+ Change authentication for this user
+
Last login
{% if not user.logged_in_at %}
diff --git a/app/templates/views/manage-users/permissions.html b/app/templates/views/manage-users/permissions.html
index 4a7365233..1075adb71 100644
--- a/app/templates/views/manage-users/permissions.html
+++ b/app/templates/views/manage-users/permissions.html
@@ -11,11 +11,7 @@
{% endif %}
{% if service_has_email_auth %}
- {% if user.webauthn_auth %}
-
- This user will login with a security key.
-
- {% elif not mobile_number %}
+ {% if not mobile_number %}
{{ radios(
form.login_authentication,
disable=['sms_auth'],
diff --git a/app/templates/views/send.html b/app/templates/views/send.html
index 1671c199c..3c1664da0 100644
--- a/app/templates/views/send.html
+++ b/app/templates/views/send.html
@@ -56,9 +56,4 @@
Your file will populate this template ({{ template.name }})
{{ template|string }}
-
-
Messages Remaining Today / Daily Message Limit
-
{{ daily_remaining_messages }} / {{ current_service.message_limit }}
-
-
{% endblock %}
diff --git a/app/templates/views/two-factor-webauthn.html b/app/templates/views/two-factor-webauthn.html
deleted file mode 100644
index 7b155f556..000000000
--- a/app/templates/views/two-factor-webauthn.html
+++ /dev/null
@@ -1,71 +0,0 @@
-{% extends "withoutnav_template.html" %}
-{% from "components/page-header.html" import page_header %}
-{% from "components/uk_components/button/macro.njk" import govukButton %}
-{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
-{% from "components/webauthn-api-check.html" import webauthn_api_check %}
-{% from "components/uk_components/error-message/macro.njk" import govukErrorMessage %}
-
-{% set page_title = 'Get your security key' %}
-
-{% block extra_javascripts_before_body %}
- {{ webauthn_api_check() }}
-{% endblock %}
-
-{% block per_page_title %}
- {{ page_title }}
-{% endblock %}
-
-{% block backLink %}
- {{ govukBackLink({ "href": url_for('.user_profile') }) }}
-{% endblock %}
-
-{% block maincolumn_content %}
-
- {{ govukErrorMessage({
- "classes": "banner-dangerous govuk-!-display-none",
- "html": (
- 'There’s a problem with your security key' +
- 'Check you have the right key and try again. ' +
- 'If this does not work, ' +
- 'contact us." +
- '
'
- ),
- "attributes": {
- "aria-live": "polite",
- "tabindex": '-1'
- }
- }) }}
-
-
-
- {{ page_header(page_title) }}
-
-
- You need to have your security key to sign in.
-
-
- {{ govukButton({
- "element": "button",
- "text": "Check security key",
- "classes": "govuk-button--secondary webauthn__api-required",
- "attributes": {
- "data-module": "authenticate-security-key",
- "data-csrf-token": csrf_token(),
- }
- }) }}
-
- {{ govukErrorMessage({
- "classes": "webauthn__api-missing",
- "text": "Your browser does not support security keys. Try signing in to Notify using a different browser."
- }) }}
-
- {{ govukErrorMessage({
- "classes": "webauthn__no-js",
- "text": "JavaScript is not available for this page. Security keys need JavaScript to work."
- }) }}
-
-
-
 }})
-
-
-{% endblock %}
diff --git a/app/templates/views/user-profile.html b/app/templates/views/user-profile.html
index b3cf15385..b603b7828 100644
--- a/app/templates/views/user-profile.html
+++ b/app/templates/views/user-profile.html
@@ -65,22 +65,6 @@
}}
{% endcall %}
- {% if current_user.can_use_webauthn %}
- {% call row(id='security-keys') %}
- {{ text_field('Security keys') }}
- {{ optional_text_field(
- ('{} registered'.format(current_user.webauthn_credentials|length)) if current_user.webauthn_credentials else None,
- default='None registered'
- ) }}
- {{ edit_field(
- 'Change',
- url_for('.user_profile_security_keys'),
- suffix='security keys'
- )
- }}
- {% endcall %}
- {% endif %}
-
{% if current_user.platform_admin or session.get('disable_platform_admin_view') %}
{% call row(id='disable-platform-admin') %}
{{ text_field('Use platform admin view') }}
diff --git a/app/templates/views/user-profile/manage-security-key.html b/app/templates/views/user-profile/manage-security-key.html
deleted file mode 100644
index 67f8d2506..000000000
--- a/app/templates/views/user-profile/manage-security-key.html
+++ /dev/null
@@ -1,34 +0,0 @@
-{% extends "withoutnav_template.html" %}
-{% from "components/page-header.html" import page_header %}
-{% from "components/page-footer.html" import page_footer %}
-{% from "components/form.html" import form_wrapper %}
-{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
-
-{% set page_title = 'Manage ' + '‘' + security_key.name + '’' %}
-
-{% block per_page_title %}
- {{ page_title }}
-{% endblock %}
-
-{% block backLink %}
- {{ govukBackLink({ "href": url_for('.user_profile_security_keys') }) }}
-{% endblock %}
-
-{% block maincolumn_content %}
- {{ page_header(page_title) }}
-
-
-
- {% call form_wrapper(autocomplete=True) %}
- {{ form.security_key_name }}
- {{ page_footer(
- 'Save',
- delete_link=url_for(
- '.user_profile_confirm_delete_security_key', key_id=security_key.id),
- delete_link_text='Delete'
- ) }}
- {% endcall %}
-
-
-
-{% endblock %}
diff --git a/app/templates/views/user-profile/security-keys.html b/app/templates/views/user-profile/security-keys.html
deleted file mode 100644
index 82603c69c..000000000
--- a/app/templates/views/user-profile/security-keys.html
+++ /dev/null
@@ -1,107 +0,0 @@
-{% extends "withoutnav_template.html" %}
-{% from "components/page-header.html" import page_header %}
-{% from "components/uk_components/button/macro.njk" import govukButton %}
-{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
-{% from "components/table.html" import edit_field, mapping_table, row, field, row_heading %}
-{% from "components/webauthn-api-check.html" import webauthn_api_check %}
-{% from "components/uk_components/error-message/macro.njk" import govukErrorMessage %}
-
-{% set page_title = 'Security keys' %}
-{% set credentials = current_user.webauthn_credentials %}
-
-{% block extra_javascripts_before_body %}
- {{ webauthn_api_check() }}
-{% endblock %}
-
-{% block per_page_title %}
- {{ page_title }}
-{% endblock %}
-
-{% block backLink %}
- {{ govukBackLink({ "href": url_for('.user_profile') }) }}
-{% endblock %}
-
-{% block maincolumn_content %}
-
-{% set webauthn_button %}
- {{ govukButton({
- "element": "button",
- "text": "Register a key",
- "classes": "govuk-button--secondary webauthn__api-required",
- "attributes": {
- "data-module": "register-security-key",
- "data-csrf-token": csrf_token(),
- }
- }) }}
-
- {{ govukErrorMessage({
- "classes": "webauthn__api-missing",
- "text": "Your browser does not support security keys. Try signing in to Notify using a different browser."
- }) }}
-
- {{ govukErrorMessage({
- "classes": "webauthn__no-js",
- "text": "JavaScript is not available for this page. Security keys need JavaScript to work."
- }) }}
-{% endset %}
-
-
-
- {{ govukErrorMessage({
- "classes": "banner-dangerous govuk-!-display-none",
- "html": (
- 'There’s a problem with your security key' +
- '
Check you have the right key and try again. ' +
- 'If this does not work, ' +
- 'contact us." +
- '
'
- ),
- "attributes": {
- "aria-live": "polite",
- "tabindex": '-1'
- }
- }) }}
-
- {% if credentials %}
-
- {{ page_header(page_title) }}
-
- {% call mapping_table(
- caption=page_title,
- field_headings=['Security key details', 'Action'],
- field_headings_visible=False,
- caption_visible=False,
- ) %}
- {% for credential in credentials %}
- {% call row() %}
- {% call field() %}
-
{{ credential.name }}
-
Registered {{ credential.created_at|format_delta }}
- {% endcall %}
- {{ edit_field('Manage', url_for('.user_profile_manage_security_key', key_id=credential.id)) }}
- {% endcall %}
- {% endfor %}
- {% endcall %}
-
- {{ webauthn_button }}
-
- {% else %}
-
- {{ page_header(page_title) }}
-
- Security keys are an alternative way of signing in to Notify,
- instead of getting a code in a text message
-
-
- You can buy any key that’s compatible with the WebAuthn
- standard.
-
- {{ webauthn_button }}
-
-
-
 }})
-
- {% endif %}
-
-
-{% endblock %}
diff --git a/app/webauthn_server.py b/app/webauthn_server.py
deleted file mode 100644
index b9c4780f7..000000000
--- a/app/webauthn_server.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from urllib.parse import urlparse
-
-from fido2.server import Fido2Server
-from fido2.webauthn import PublicKeyCredentialRpEntity
-
-
-def init_app(app):
- base_url = urlparse(app.config["ADMIN_BASE_URL"])
- verify_origin_callback = None
-
- # stub verification in dev (to avoid need for HTTPS)
- if app.config["NOTIFY_ENVIRONMENT"] == "development":
- verify_origin_callback = stub_origin_checker
-
- relying_party = PublicKeyCredentialRpEntity(
- id=base_url.hostname,
- name="U.S. Notify",
- )
-
- app.webauthn_server = Fido2Server(
- relying_party,
- attestation="direct",
- verify_origin=verify_origin_callback,
- )
-
- # some browsers don't seem to have a default timeout
- # 30 seconds seems like a generous amount of time
- app.webauthn_server.timeout = 30_000
-
-
-def stub_origin_checker(*args):
- return True
diff --git a/docs/sprint-goals.md b/docs/sprint-goals.md
index 50d6139e4..f774da9c5 100644
--- a/docs/sprint-goals.md
+++ b/docs/sprint-goals.md
@@ -1,6 +1,16 @@
# Notify Sprint Goals Log
-## Sprint: P (8/3/23)
+## Sprint: Q (8/17/23)
+
+| | Goals | Impact |
+|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
+| Engineering | Continue app stabilization efforts, including: [Completing UK stylesheet removal](https://github.com/GSA/notifications-admin/issues/686), [Removing GDS libraries](https://github.com/GSA/notifications-admin/issues/674), and working to improve documentation | Increase app reliability and simplicity prior to first partner use |
+| UX | Hone a visual brand strategy for proposal to leadership, ensure that USWDS elements are rendering properly | Consistent look and feel for Notify.gov, smooth, non-glitchy experience for users |
+| Security | Begin scoping and planning for addressing LATO findings | Complete resolving POA&Ms in their designated timeline|
+| Content | Use research and user test information to begin crafting more concise content | Have content ready for later addition to the site
+
+
+## Sprint: Puffin (8/3/23)
| | Goals | Impact |
|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
diff --git a/gulpfile.js b/gulpfile.js
index 4d9689be0..70e569291 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -124,8 +124,6 @@ const javascripts = () => {
paths.src + 'javascripts/templateFolderForm.js',
paths.src + 'javascripts/collapsibleCheckboxes.js',
paths.src + 'javascripts/radioSlider.js',
- paths.src + 'javascripts/registerSecurityKey.js',
- paths.src + 'javascripts/authenticateSecurityKey.js',
paths.src + 'javascripts/updateStatus.js',
paths.src + 'javascripts/errorBanner.js',
paths.src + 'javascripts/homepage.js',
@@ -243,7 +241,7 @@ const defaultTask = parallel(
series(
javascripts
),
- sass,
+ sass,
uswds.compile,
uswds.copyAssets,
copy.gtm
@@ -295,4 +293,4 @@ exports.init = uswds.init;
exports.compile = uswds.compile;
exports.copyAll = uswds.copyAll;
exports.watch = uswds.watch;
-exports.copyAssets = uswds.copyAssets;
\ No newline at end of file
+exports.copyAssets = uswds.copyAssets;
diff --git a/tests/app/main/views/test_find_users.py b/tests/app/main/views/test_find_users.py
index 2b36ccb9c..566e01644 100644
--- a/tests/app/main/views/test_find_users.py
+++ b/tests/app/main/views/test_find_users.py
@@ -163,28 +163,6 @@ def test_user_information_page_shows_change_auth_type_link(
assert normalize_spaces(link.text) == 'Change authentication for this user'
-def test_user_information_page_doesnt_show_change_auth_type_link_if_user_on_webauthn(
- client_request,
- platform_admin_user,
- api_user_active,
- mock_get_organizations_and_services_for_user,
- mocker
-):
- client_request.login(platform_admin_user)
- mocker.patch('app.user_api_client.get_user', side_effect=[
- platform_admin_user,
- user_json(id_=api_user_active['id'], name="Apple Bloom", auth_type='webauthn_auth')
- ], autospec=True)
-
- page = client_request.get(
- 'main.user_information', user_id=api_user_active['id']
- )
- change_auth_url = url_for('main.change_user_auth', user_id=api_user_active['id'])
-
- link = page.find_all('a', {'href': change_auth_url})
- assert len(link) == 0
-
-
@pytest.mark.parametrize('current_auth_type', ['email_auth', 'sms_auth'])
def test_change_user_auth_preselects_current_auth_type(
client_request,
diff --git a/tests/app/main/views/test_manage_users.py b/tests/app/main/views/test_manage_users.py
index e672c1a6b..efc154438 100644
--- a/tests/app/main/views/test_manage_users.py
+++ b/tests/app/main/views/test_manage_users.py
@@ -770,62 +770,6 @@ def test_edit_user_permissions_shows_authentication_for_email_auth_service(
assert not any(button.has_attr("disabled") for button in radio_buttons)
-def test_edit_user_permissions_hides_authentication_for_webauthn_user(
- client_request,
- service_one,
- mock_get_users_by_service,
- mock_get_template_folders,
- active_user_with_permissions,
-):
- active_user_with_permissions['auth_type'] = 'webauthn_auth'
- service_one['permissions'].append('email_auth')
-
- page = client_request.get(
- 'main.edit_user_permissions',
- service_id=SERVICE_ONE_ID,
- user_id=active_user_with_permissions['id'],
- )
-
- assert 'This user will login with a security key' in str(page)
- assert page.select_one('#login_authentication') is None
-
-
-@pytest.mark.parametrize('new_auth_type', ['sms_auth', 'email_auth'])
-def test_edit_user_permissions_preserves_auth_type_for_webauthn_user(
- client_request,
- service_one,
- active_user_with_permissions,
- mock_get_users_by_service,
- mock_get_invites_for_service,
- mock_set_user_permissions,
- mock_update_user_attribute,
- mock_get_template_folders,
- new_auth_type,
-):
- active_user_with_permissions['auth_type'] = 'webauthn_auth'
- service_one['permissions'].append('email_auth')
-
- client_request.post(
- 'main.edit_user_permissions',
- service_id=SERVICE_ONE_ID,
- user_id=active_user_with_permissions['id'],
- _data={
- 'email_address': active_user_with_permissions['email_address'],
- 'permissions_field': [],
- 'login_authentication': new_auth_type,
- },
- _expected_status=302,
- )
-
- mock_set_user_permissions.assert_called_with(
- str(active_user_with_permissions['id']),
- SERVICE_ONE_ID,
- permissions=set(),
- folder_permissions=[],
- )
- mock_update_user_attribute.assert_not_called()
-
-
def test_should_show_page_for_inviting_user(
client_request,
mock_get_template_folders,
diff --git a/tests/app/main/views/test_new_password.py b/tests/app/main/views/test_new_password.py
index 9fcd608ba..0e4fca2e7 100644
--- a/tests/app/main/views/test_new_password.py
+++ b/tests/app/main/views/test_new_password.py
@@ -77,34 +77,6 @@ def test_should_redirect_to_two_factor_when_password_reset_is_successful(
mock_get_user_by_email_request_password_reset.assert_called_once_with(user['email_address'])
-@pytest.mark.parametrize('redirect_url', [
- None,
- f'/services/{SERVICE_ONE_ID}/templates',
-])
-def test_should_redirect_to_two_factor_webauthn_when_password_reset_is_successful(
- notify_admin,
- client_request,
- mock_get_user_by_email_request_password_reset,
- mock_send_verify_code,
- mock_reset_failed_login_count,
- redirect_url
-):
- client_request.logout()
- user = mock_get_user_by_email_request_password_reset.return_value
- user['auth_type'] = 'webauthn_auth'
- data = json.dumps({'email': user['email_address'], 'created_at': str(datetime.utcnow())})
- token = generate_token(data, notify_admin.config['SECRET_KEY'], notify_admin.config['DANGEROUS_SALT'])
- client_request.post_url(
- url_for_endpoint_with_token('.new_password', token=token, next=redirect_url),
- _data={'new_password': 'a-new_password'},
- _expected_redirect=url_for('.two_factor_webauthn', next=redirect_url),
- )
- mock_get_user_by_email_request_password_reset.assert_called_once_with(user['email_address'])
-
- assert not mock_send_verify_code.called
- assert mock_reset_failed_login_count.called
-
-
def test_should_redirect_index_if_user_has_already_changed_password(
notify_admin,
client_request,
diff --git a/tests/app/main/views/test_sign_in.py b/tests/app/main/views/test_sign_in.py
index 4fcbd6784..b2929573b 100644
--- a/tests/app/main/views/test_sign_in.py
+++ b/tests/app/main/views/test_sign_in.py
@@ -192,34 +192,6 @@ def test_process_email_auth_sign_in_return_2fa_template(
mock_verify_password.assert_called_with(api_user_active_email_auth['id'], 'val1dPassw0rd!')
-@pytest.mark.parametrize('redirect_url', [
- None,
- f'/services/{SERVICE_ONE_ID}/templates',
-])
-def test_process_webauthn_auth_sign_in_redirects_to_webauthn_with_next_redirect(
- client_request,
- api_user_active,
- mocker,
- mock_verify_password,
- redirect_url
-):
- client_request.logout()
- api_user_active['auth_type'] = 'webauthn_auth'
- mock_get_user_by_email = mocker.patch('app.user_api_client.get_user_by_email', return_value=api_user_active)
-
- client_request.post(
- 'main.sign_in',
- next=redirect_url,
- _data={
- 'email_address': 'valid@example.gsa.gov',
- 'password': 'val1dPassw0rd!',
- },
- _expected_redirect=url_for('.two_factor_webauthn', next=redirect_url)
- )
-
- mock_get_user_by_email.assert_called_once_with('valid@example.gsa.gov')
-
-
def test_should_return_locked_out_true_when_user_is_locked(
client_request,
mock_get_user_by_email_locked,
diff --git a/tests/app/main/views/test_two_factor.py b/tests/app/main/views/test_two_factor.py
index d4705006c..8144faa19 100644
--- a/tests/app/main/views/test_two_factor.py
+++ b/tests/app/main/views/test_two_factor.py
@@ -295,54 +295,6 @@ def test_two_factor_sms_post_should_redirect_to_sign_in_if_user_not_in_session(
)
-@pytest.mark.parametrize('endpoint', ['main.two_factor_webauthn', 'main.two_factor_sms'])
-def test_two_factor_endpoints_get_should_redirect_to_sign_in_if_user_not_in_session(
- client_request,
- endpoint,
-):
- client_request.get(
- endpoint,
- _expected_redirect=url_for('main.sign_in')
- )
-
-
-def test_two_factor_webauthn_should_have_auth_signin_button(
- client_request,
- platform_admin_user,
- mocker,
-):
- client_request.logout()
- mock_get_user = mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
- with client_request.session_transaction() as session:
- session['user_details'] = {'id': platform_admin_user['id'], 'email': platform_admin_user['email_address']}
-
- page = client_request.get('main.two_factor_webauthn')
-
- button = page.select_one("button[data-module=authenticate-security-key]")
-
- assert button.text.strip() == 'Check security key'
-
- assert button.name == 'button'
- mock_get_user.assert_called_once_with(platform_admin_user['id'])
-
-
-def test_two_factor_webauthn_should_reject_non_webauthn_auth_users(
- client_request,
- platform_admin_user,
- mocker,
-):
- client_request.logout()
- platform_admin_user['auth_type'] = 'sms_auth'
- mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
- with client_request.session_transaction() as session:
- session['user_details'] = {'id': platform_admin_user['id'], 'email': platform_admin_user['email_address']}
-
- client_request.get(
- 'main.two_factor_webauthn',
- _expected_status=403,
- )
-
-
def test_two_factor_sms_should_activate_pending_user(
client_request,
mocker,
diff --git a/tests/app/main/views/test_user_profile.py b/tests/app/main/views/test_user_profile.py
index ead6cd4c6..ca2f4cb2f 100644
--- a/tests/app/main/views/test_user_profile.py
+++ b/tests/app/main/views/test_user_profile.py
@@ -3,13 +3,8 @@ import uuid
import pytest
from flask import url_for
-from notifications_python_client.errors import HTTPError
from notifications_utils.url_safe_token import generate_token
-from app.models.webauthn_credential import (
- WebAuthnCredential,
- WebAuthnCredentials,
-)
from tests.conftest import (
create_api_user_active,
create_user,
@@ -29,10 +24,8 @@ def test_should_show_overview_page(
def test_overview_page_shows_disable_for_platform_admin(
client_request,
- platform_admin_user,
- mocker
+ platform_admin_user
):
- mocker.patch('app.models.webauthn_credential.WebAuthnCredentials.client_method')
client_request.login(platform_admin_user)
page = client_request.get('main.user_profile')
assert page.select_one('h1').text.strip() == 'Your profile'
@@ -41,30 +34,6 @@ def test_overview_page_shows_disable_for_platform_admin(
'Use platform admin view Yes Change whether to use platform admin view'
-@pytest.mark.parametrize('key_count, expected_row_text', [
- (0, 'Security keys None registered Change security keys'),
- (1, 'Security keys 1 registered Change security keys'),
- (2, 'Security keys 2 registered Change security keys'),
-])
-def test_overview_page_shows_security_keys_if_user_they_can_use_webauthn(
- mocker,
- client_request,
- platform_admin_user,
- webauthn_credential,
- key_count,
- expected_row_text,
-):
- client_request.login(platform_admin_user)
- credentials = [webauthn_credential for _ in range(key_count)]
- mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredentials.client_method',
- return_value=credentials,
- )
- page = client_request.get('main.user_profile')
- security_keys_row = page.select_one('#security-keys')
- assert ' '.join(security_keys_row.text.split()) == expected_row_text
-
-
def test_should_show_name_page(
client_request
):
@@ -448,291 +417,3 @@ def test_can_reenable_platform_admin(client_request, platform_admin_user):
with client_request.session_transaction() as session:
assert session['disable_platform_admin_view'] is False
-
-
-def test_user_doesnt_see_security_keys_unless_they_can_use_webauthn(
- client_request,
- platform_admin_user
-):
- platform_admin_user['can_use_webauthn'] = False
- client_request.login(platform_admin_user)
-
- client_request.get(
- '.user_profile_security_keys',
- _expected_status=403,
- )
-
-
-def test_should_show_security_keys_page(
- mocker,
- client_request,
- platform_admin_user,
- webauthn_credential,
-):
- client_request.login(platform_admin_user)
-
- mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredentials.client_method',
- return_value=[webauthn_credential],
- )
-
- page = client_request.get('.user_profile_security_keys')
- assert page.select_one('h1').text.strip() == 'Security keys'
-
- credential_row = page.select('tr')[-1]
- assert 'Test credential' in credential_row.text
- assert "Manage" in credential_row.find('a').text
- assert credential_row.find('a')["href"] == url_for(
- '.user_profile_manage_security_key',
- key_id=webauthn_credential['id']
- )
-
- register_button = page.select_one("[data-module='register-security-key']")
- assert register_button.text.strip() == 'Register a key'
-
-
-def test_get_key_from_list_of_keys(
- mocker,
- webauthn_credential,
- webauthn_credential_2,
- fake_uuid,
-):
- mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredentials.client_method',
- return_value=[webauthn_credential, webauthn_credential_2],
- )
- assert WebAuthnCredentials(fake_uuid).by_id(webauthn_credential["id"]) == WebAuthnCredential(webauthn_credential)
-
-
-def test_should_show_manage_security_key_page(
- mocker,
- client_request,
- platform_admin_user,
- webauthn_credential,
-):
- client_request.login(platform_admin_user)
-
- mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredentials.client_method',
- return_value=[webauthn_credential],
- )
-
- page = client_request.get('.user_profile_manage_security_key', key_id=webauthn_credential['id'])
- assert page.select_one('h1').text.strip() == f'Manage ‘{webauthn_credential["name"]}’'
-
- assert page.select_one('.usa-back-link').text.strip() == 'Back'
- assert page.select_one('.usa-back-link')['href'] == url_for('.user_profile_security_keys')
-
- assert page.select_one('#security_key_name')["value"] == webauthn_credential["name"]
-
-
-def test_manage_security_key_page_404s_when_key_not_found(
- mocker,
- client_request,
- platform_admin_user,
- webauthn_credential,
- webauthn_credential_2
-):
- client_request.login(platform_admin_user)
-
- mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredentials.client_method',
- return_value=[webauthn_credential_2],
- )
- client_request.get(
- '.user_profile_manage_security_key',
- key_id=webauthn_credential['id'],
- _expected_status=404,
- )
-
-
-@pytest.mark.parametrize('endpoint,method', [
- (".user_profile_manage_security_key", "get"),
- (".user_profile_manage_security_key", "post"),
- (".user_profile_confirm_delete_security_key", "get"),
- (".user_profile_confirm_delete_security_key", "post"),
- (".user_profile_delete_security_key", "post"),
-])
-def test_cant_manage_security_keys_unless_can_use_webauthn(
- client_request,
- platform_admin_user,
- webauthn_credential,
- endpoint,
- method
-):
- platform_admin_user['can_use_webauthn'] = False
- client_request.login(platform_admin_user)
-
- if method == "get":
- client_request.get(
- endpoint,
- key_id=webauthn_credential['id'],
- _expected_status=403,
- )
-
- else:
- client_request.post(
- endpoint,
- key_id=webauthn_credential['id'],
- _expected_status=403,
- )
-
-
-def test_should_redirect_after_change_of_security_key_name(
- client_request,
- platform_admin_user,
- webauthn_credential,
- mocker
-):
- client_request.login(platform_admin_user)
-
- mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredentials.client_method',
- return_value=[webauthn_credential],
- )
-
- mock_update = mocker.patch('app.user_api_client.update_webauthn_credential_name_for_user')
-
- client_request.post(
- 'main.user_profile_manage_security_key',
- key_id=webauthn_credential['id'],
- _data={'security_key_name': "new name"},
- _expected_status=302,
- _expected_redirect=url_for(
- 'main.user_profile_security_keys',
- )
- )
-
- mock_update.assert_called_once_with(
- credential_id=webauthn_credential['id'],
- new_name_for_credential="new name",
- user_id=platform_admin_user["id"]
- )
-
-
-def test_user_profile_manage_security_key_should_not_call_api_if_key_name_stays_the_same(
- client_request,
- platform_admin_user,
- webauthn_credential,
- mocker
-):
- client_request.login(platform_admin_user)
-
- mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredentials.client_method',
- return_value=[webauthn_credential],
- )
-
- mock_update = mocker.patch('app.user_api_client.update_webauthn_credential_name_for_user')
-
- client_request.post(
- 'main.user_profile_manage_security_key',
- key_id=webauthn_credential['id'],
- _data={'security_key_name': webauthn_credential['name']},
- _expected_status=302,
- _expected_redirect=url_for(
- 'main.user_profile_security_keys',
- )
- )
-
- assert not mock_update.called
-
-
-def test_shows_delete_link_for_security_key(
- mocker,
- client_request,
- platform_admin_user,
- webauthn_credential,
-):
- client_request.login(platform_admin_user)
-
- mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredentials.client_method',
- return_value=[webauthn_credential],
- )
-
- page = client_request.get('.user_profile_manage_security_key', key_id=webauthn_credential['id'])
- assert page.select_one('h1').text.strip() == f'Manage ‘{webauthn_credential["name"]}’'
-
- link = page.select_one('.page-footer a')
- assert normalize_spaces(link.text) == 'Delete'
- assert link['href'] == url_for('.user_profile_confirm_delete_security_key', key_id=webauthn_credential['id'])
-
-
-def test_confirm_delete_security_key(
- client_request,
- platform_admin_user,
- webauthn_credential,
- mocker
-):
- client_request.login(platform_admin_user)
-
- mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredentials.client_method',
- return_value=[webauthn_credential],
- )
-
- page = client_request.get(
- '.user_profile_confirm_delete_security_key',
- key_id=webauthn_credential['id'],
- _test_page_title=False,
- )
-
- assert normalize_spaces(page.select_one('.banner-dangerous').text) == (
- 'Are you sure you want to delete this security key? '
- 'Yes, delete'
- )
- assert 'action' not in page.select_one('.banner-dangerous form')
- assert page.select_one('.banner-dangerous form')['method'] == 'post'
-
-
-def test_delete_security_key(
- client_request,
- platform_admin_user,
- webauthn_credential,
- mocker
-):
- client_request.login(platform_admin_user)
- mock_delete = mocker.patch('app.user_api_client.delete_webauthn_credential_for_user')
-
- client_request.post(
- '.user_profile_delete_security_key',
- key_id=webauthn_credential['id'],
- _expected_redirect=url_for(
- '.user_profile_security_keys',
- )
- )
- mock_delete.assert_called_once_with(
- credential_id=webauthn_credential['id'],
- user_id=platform_admin_user["id"]
- )
-
-
-def test_delete_security_key_handles_last_credential_error(
- client_request,
- platform_admin_user,
- webauthn_credential,
- mocker,
-):
- client_request.login(platform_admin_user)
- mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredentials.client_method',
- return_value=[webauthn_credential],
- )
-
- mocker.patch(
- 'app.user_api_client.delete_webauthn_credential_for_user',
- side_effect=HTTPError(
- response={},
- message='Cannot delete last remaining webauthn credential for user'
- )
- )
-
- page = client_request.post(
- '.user_profile_delete_security_key',
- key_id=webauthn_credential['id'],
- _follow_redirects=True
- )
- assert 'Manage ‘Test credential’' in page.find('h1').text
- expected_message = "You cannot delete your last security key."
- assert expected_message in page.find('div', class_="banner-dangerous").text
diff --git a/tests/app/main/views/test_webauthn_credentials.py b/tests/app/main/views/test_webauthn_credentials.py
deleted file mode 100644
index 637bf5d1c..000000000
--- a/tests/app/main/views/test_webauthn_credentials.py
+++ /dev/null
@@ -1,449 +0,0 @@
-import base64
-from unittest.mock import ANY, Mock
-
-import pytest
-from fido2 import cbor
-from flask import url_for
-
-from app.models.webauthn_credential import RegistrationError, WebAuthnCredential
-
-
-@pytest.fixture
-def webauthn_authentication_post_data(fake_uuid, webauthn_credential, client_request):
-
- _set_up_webauthn_session(fake_uuid, client_request)
-
- credential_id = WebAuthnCredential(webauthn_credential).to_credential_data().credential_id
-
- return cbor.encode({
- 'credentialId': credential_id,
- 'authenticatorData': base64.b64decode(b'dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvABAAACfQ=='),
- 'clientDataJSON': b'{"challenge":"e-g-nXaRxMagEiqTJSyD82RsEc5if_6jyfJDy8bNKlw","origin":"https://webauthn.io","type":"webauthn.get"}', # noqa
- 'signature': bytes.fromhex('304502204a76f05cd52a778cdd4df1565e0004e5cc1ead360419d0f5c3a0143bf37e7f15022100932b5c308a560cfe4f244214843075b904b3eda64e85d64662a81198c386cdde'), # noqa
- })
-
-
-def _set_up_webauthn_session(user_id, client):
- """
- Sets up session, challenge, etc as if a user with uuid `fake_uuid` has logged in and touched the webauthn token
- as found in the `webauthn_credential` fixture. Sets up the session as if `begin_authentication` had been called
- so that the challenge matches and the credential will validate (provided that the key belongs to the user referenced
- in the session).
- """
- with client.session_transaction() as session:
- session['user_details'] = {'id': user_id}
- session['webauthn_authentication_state'] = {
- "challenge": "e-g-nXaRxMagEiqTJSyD82RsEc5if_6jyfJDy8bNKlw",
- "user_verification": None
- }
-
-
-def test_begin_register_forbidden_unless_can_use_webauthn(
- client_request,
- platform_admin_user,
- mocker,
-):
- platform_admin_user['can_use_webauthn'] = False
- mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
- client_request.get('main.webauthn_begin_register', _expected_status=403)
-
-
-def test_begin_register_returns_encoded_options(
- mocker,
- platform_admin_user,
- client_request,
- webauthn_dev_server,
-):
- mocker.patch('app.models.webauthn_credential.WebAuthnCredentials.client_method', return_value=[])
-
- client_request.login(platform_admin_user)
- response = client_request.get_response(
- 'main.webauthn_begin_register',
- )
-
- webauthn_options = cbor.decode(response.data)['publicKey']
- assert webauthn_options['attestation'] == 'direct'
- assert webauthn_options['timeout'] == 30_000
-
- auth_selection = webauthn_options['authenticatorSelection']
- assert auth_selection['authenticatorAttachment'] == 'cross-platform'
- assert auth_selection['userVerification'] == 'discouraged'
-
- user_options = webauthn_options['user']
- assert user_options['name'] == platform_admin_user['email_address']
- assert user_options['id'] == bytes(platform_admin_user['id'], 'utf-8')
-
- relying_party_options = webauthn_options['rp']
- assert relying_party_options['name'] == 'U.S. Notify'
- assert relying_party_options['id'] == 'webauthn.io'
-
-
-def test_begin_register_includes_existing_credentials(
- client_request,
- platform_admin_user,
- webauthn_credential,
- mocker,
-):
- mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredentials.client_method',
- return_value=[webauthn_credential, webauthn_credential]
- )
-
- client_request.login(platform_admin_user)
- response = client_request.get_response(
- 'main.webauthn_begin_register',
- )
-
- webauthn_options = cbor.decode(response.data)['publicKey']
- assert len(webauthn_options['excludeCredentials']) == 2
-
-
-def test_begin_register_stores_state_in_session(
- client_request,
- platform_admin_user,
- mocker,
-):
- mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredentials.client_method',
- return_value=[])
-
- client_request.login(platform_admin_user)
- client_request.get_response(
- 'main.webauthn_begin_register',
- )
-
- with client_request.session_transaction() as session:
- assert session['webauthn_registration_state'] is not None
-
-
-def test_complete_register_creates_credential(
- platform_admin_user,
- client_request,
- mock_update_user_attribute,
- mocker,
-):
- with client_request.session_transaction() as session:
- session['webauthn_registration_state'] = 'state'
-
- user_api_mock = mocker.patch(
- 'app.user_api_client.create_webauthn_credential_for_user'
- )
-
- credential_mock = mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredential.from_registration',
- return_value='cred'
- )
-
- client_request.login(platform_admin_user)
- client_request.post_response(
- 'main.webauthn_begin_register',
- _data=cbor.encode('public_key_credential'),
- _expected_status=200,
- )
-
- credential_mock.assert_called_once_with('state', 'public_key_credential')
- user_api_mock.assert_called_once_with(platform_admin_user['id'], 'cred')
- mock_update_user_attribute.assert_called_once_with(
- platform_admin_user['id'],
- auth_type='webauthn_auth',
- )
-
-
-def test_complete_register_clears_session(
- client_request,
- platform_admin_user,
- mocker,
-):
- with client_request.session_transaction() as session:
- session['webauthn_registration_state'] = 'state'
-
- mocker.patch('app.user_api_client.create_webauthn_credential_for_user')
- mocker.patch('app.models.webauthn_credential.WebAuthnCredential.from_registration')
-
- client_request.login(platform_admin_user)
- client_request.post(
- 'main.webauthn_complete_register',
- _data=cbor.encode('public_key_credential'),
- _expected_status=200,
- )
-
- with client_request.session_transaction() as session:
- assert 'webauthn_registration_state' not in session
- assert session['_flashes'] == [('default_with_tick', (
- 'Registration complete. Next time you sign in to Notify '
- 'you’ll be asked to use your security key.'
- ))]
-
-
-def test_complete_register_handles_library_errors(
- client_request,
- platform_admin_user,
- mocker,
-):
- with client_request.session_transaction() as session:
- session['webauthn_registration_state'] = 'state'
-
- mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredential.from_registration',
- side_effect=RegistrationError('error')
- )
-
- client_request.login(platform_admin_user)
- client_request.post_response(
- 'main.webauthn_complete_register',
- _data=cbor.encode('public_key_credential'),
- _expected_status=400,
- )
-
-
-def test_complete_register_handles_missing_state(
- client_request,
- platform_admin_user,
- mocker,
-):
- client_request.login(platform_admin_user)
- response = client_request.post_response(
- 'main.webauthn_complete_register',
- _data=cbor.encode('public_key_credential'),
- _expected_status=400,
- )
-
- assert cbor.decode(response.data) == 'No registration in progress'
-
-
-def test_begin_authentication_forbidden_for_users_without_webauthn(client_request, mocker, platform_admin_user):
- platform_admin_user['auth_type'] = 'sms_auth'
- client_request.logout()
- mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
-
- with client_request.session_transaction() as session:
- session['user_details'] = {'id': '1'}
-
- client_request.get(
- 'main.webauthn_begin_authentication',
- _expected_status=403,
- )
-
-
-def test_begin_authentication_returns_encoded_options(
- client_request,
- mocker,
- webauthn_credential,
- platform_admin_user,
-):
- client_request.login(platform_admin_user)
-
- with client_request.session_transaction() as session:
- session['user_details'] = {'id': platform_admin_user['id']}
-
- get_creds_mock = mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredentials.client_method',
- return_value=[webauthn_credential]
- )
- response = client_request.get_response('main.webauthn_begin_authentication')
-
- decoded_data = cbor.decode(response.data)
- allowed_credentials = decoded_data['publicKey']['allowCredentials']
-
- assert len(allowed_credentials) == 1
- assert decoded_data['publicKey']['timeout'] == 30000
- get_creds_mock.assert_called_once_with(platform_admin_user['id'])
-
-
-def test_begin_authentication_stores_state_in_session(
- client_request,
- mocker,
- webauthn_credential,
- platform_admin_user,
-):
- client_request.login(platform_admin_user)
-
- with client_request.session_transaction() as session:
- session['user_details'] = {'id': platform_admin_user['id']}
-
- mocker.patch(
- 'app.models.webauthn_credential.WebAuthnCredentials.client_method',
- return_value=[webauthn_credential]
- )
- client_request.get_response('main.webauthn_begin_authentication')
-
- with client_request.session_transaction() as session:
- assert 'challenge' in session['webauthn_authentication_state']
-
-
-def test_complete_authentication_checks_credentials(
- client_request,
- mocker,
- webauthn_credential,
- webauthn_dev_server,
- mock_create_event,
- webauthn_authentication_post_data,
- platform_admin_user
-):
- client_request.logout()
- _set_up_webauthn_session(platform_admin_user['id'], client_request)
-
- mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
- mocker.patch('app.models.webauthn_credential.WebAuthnCredentials.client_method', return_value=[webauthn_credential])
- mocker.patch(
- 'app.main.views.webauthn_credentials._complete_webauthn_login_attempt',
- return_value=Mock(location='/foo')
- )
-
- response = client_request.post_response(
- 'main.webauthn_complete_authentication',
- _data=webauthn_authentication_post_data,
- _expected_status=200,
- )
-
- assert cbor.decode(response.data) == {'redirect_url': '/foo'}
-
-
-def test_complete_authentication_403s_if_key_isnt_in_users_credentials(
- client_request,
- mocker,
- webauthn_credential,
- webauthn_dev_server,
- webauthn_authentication_post_data,
- platform_admin_user
-):
- client_request.logout()
- _set_up_webauthn_session(platform_admin_user['id'], client_request)
-
- mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
- # user has no keys in the database
- mocker.patch('app.models.webauthn_credential.WebAuthnCredentials.client_method', return_value=[])
- mock_verify_webauthn_login = mocker.patch('app.main.views.webauthn_credentials._complete_webauthn_login_attempt')
- mock_unsuccesful_login_api_call = mocker.patch('app.user_api_client.complete_webauthn_login_attempt')
-
- client_request.post_response(
- 'main.webauthn_complete_authentication',
- _data=webauthn_authentication_post_data,
- _expected_status=403,
- )
-
- with client_request.session_transaction() as session:
- assert session['user_details']['id'] == platform_admin_user['id']
- # user not logged in
- assert 'user_id' not in session
- # webauthn state reset so can't replay
- assert 'webauthn_authentication_state' not in session
-
- assert mock_verify_webauthn_login.called is False
- # make sure we incremented the failed login count
- mock_unsuccesful_login_api_call.assert_called_once_with(platform_admin_user['id'], False)
-
-
-def test_complete_authentication_clears_session(
- client_request,
- mocker,
- webauthn_credential,
- webauthn_dev_server,
- webauthn_authentication_post_data,
- mock_create_event,
- platform_admin_user
-):
- client_request.logout()
- mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
- mocker.patch('app.user_api_client.get_webauthn_credentials_for_user', return_value=[webauthn_credential])
- mocker.patch(
- 'app.main.views.webauthn_credentials._complete_webauthn_login_attempt',
- return_value=Mock(location='/foo')
- )
-
- client_request.post('main.webauthn_complete_authentication', _data=webauthn_authentication_post_data)
-
- with client_request.session_transaction() as session:
- # it's important that we clear the session to ensure that we don't re-use old login artifacts in future
- assert 'webauthn_authentication_state' not in session
-
-
-@pytest.mark.parametrize('url_kwargs, expected_redirect', [
- ({}, '/accounts-or-dashboard'),
- ({'next': '/bar'}, '/bar'),
-])
-def test_verify_webauthn_login_signs_user_in(
- client_request,
- mocker,
- mock_create_event,
- platform_admin_user,
- url_kwargs,
- expected_redirect,
-):
- client_request.logout()
- with client_request.session_transaction() as session:
- session['user_details'] = {
- 'id': platform_admin_user['id'],
- 'email': platform_admin_user['email_address']
- }
- client_request.login(platform_admin_user)
- mocker.patch('app.main.views.webauthn_credentials._verify_webauthn_authentication')
- mocker.patch('app.user_api_client.complete_webauthn_login_attempt', return_value=(True, None))
- mocker.patch('app.main.views.webauthn_credentials.email_needs_revalidating', return_value=False)
-
- resp = client_request.post_response(
- 'main.webauthn_complete_authentication',
- _expected_status=200,
- **url_kwargs
- )
-
- assert cbor.decode(resp.data)['redirect_url'] == expected_redirect
- # removes stuff from session
- with client_request.session_transaction() as session:
- assert 'user_details' not in session
-
- mock_create_event.assert_called_once_with('sucessful_login', ANY)
-
-
-def test_verify_webauthn_login_signs_user_in_doesnt_sign_user_in_if_api_rejects(
- client_request,
- mocker,
- platform_admin_user,
-):
-
- with client_request.session_transaction() as session:
- session['user_details'] = {
- 'id': platform_admin_user['id'],
- 'email': platform_admin_user['email_address']
- }
- client_request.login(platform_admin_user)
- mocker.patch('app.main.views.webauthn_credentials._verify_webauthn_authentication')
- mocker.patch('app.user_api_client.complete_webauthn_login_attempt', return_value=(False, None))
-
- client_request.post(
- 'main.webauthn_complete_authentication',
- _expected_status=403,
- )
-
-
-def test_verify_webauthn_login_signs_user_in_sends_revalidation_email_if_needed(
- client_request,
- mocker,
- mock_send_verify_code,
- platform_admin_user,
-):
- user_details = {
- 'id': platform_admin_user['id'],
- 'email': platform_admin_user['email_address']
- }
-
- with client_request.session_transaction() as session:
- session['user_details'] = user_details
-
- mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
- mocker.patch('app.main.views.webauthn_credentials._verify_webauthn_authentication')
- mocker.patch('app.user_api_client.complete_webauthn_login_attempt', return_value=(True, None))
- mocker.patch('app.main.views.webauthn_credentials.email_needs_revalidating', return_value=True)
-
- resp = client_request.post_response(
- 'main.webauthn_complete_authentication',
- _expected_status=200,
- )
-
- assert cbor.decode(resp.data)['redirect_url'] == url_for('main.revalidate_email_sent')
-
- with client_request.session_transaction() as session:
- # stuff stays in session so we can log them in later when they validate their email
- assert session['user_details'] == user_details
-
- mock_send_verify_code.assert_called_once_with(platform_admin_user['id'], 'email', ANY, ANY)
diff --git a/tests/app/models/test_webauthn_credential.py b/tests/app/models/test_webauthn_credential.py
deleted file mode 100644
index 8096c72c3..000000000
--- a/tests/app/models/test_webauthn_credential.py
+++ /dev/null
@@ -1,71 +0,0 @@
-import base64
-
-import pytest
-from fido2 import cbor
-from fido2.cose import ES256
-
-from app.models.webauthn_credential import RegistrationError, WebAuthnCredential
-
-# noqa adapted from https://github.com/duo-labs/py_webauthn/blob/90e3d97e0182899a35a70fc510280b4082cce19b/tests/test_webauthn.py#L14-L24
-SESSION_STATE = {'challenge': 'bPzpX3hHQtsp9evyKYkaZtVc9UN07PUdJ22vZUdDp94', 'user_verification': 'discouraged'}
-CLIENT_DATA_JSON = b'{"type": "webauthn.create", "clientExtensions": {}, "challenge": "bPzpX3hHQtsp9evyKYkaZtVc9UN07PUdJ22vZUdDp94", "origin": "https://webauthn.io"}' # noqa
-
-# had to use the cbor2 library to re-encode the attestationObject due to implementation differences
-ATTESTATION_OBJECT = base64.b64decode(b'o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEgwRgIhAI1qbvWibQos/t3zsTU05IXw1Ek3SDApATok09uc4UBwAiEAv0fB/lgb5Ot3zJ691Vje6iQLAtLhJDiA8zDxaGjcE3hjeDVjgVkCUzCCAk8wggE3oAMCAQICBDxoKU0wDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMDExLzAtBgNVBAMMJll1YmljbyBVMkYgRUUgU2VyaWFsIDIzOTI1NzM0ODExMTE3OTAxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvd9nk9t3lMNQMXHtLE1FStlzZnUaSLql2fm1ajoggXlrTt8rzXuSehSTEPvEaEdv/FeSqX22L6Aoa8ajIAIOY6M7MDkwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjUwEwYLKwYBBAGC5RwCAQEEBAMCBSAwDQYJKoZIhvcNAQELBQADggEBAKrADVEJfuwVpIazebzEg0D4Z9OXLs5qZ/ukcONgxkRZ8K04QtP/CB5x6olTlxsj+SXArQDCRzEYUgbws6kZKfuRt2a1P+EzUiqDWLjRILSr+3/o7yR7ZP/GpiFKwdm+czb94POoGD+TS1IYdfXj94mAr5cKWx4EKjh210uovu/pLdLjc8xkQciUrXzZpPR9rT2k/q9HkZhHU+NaCJzky+PTyDbq0KKnzqVhWtfkSBCGw3ezZkTS+5lrvOKbIa24lfeTgu7FST5OwTPCFn8HcfWZMXMSD/KNU+iBqJdAwTLPPDRoLLvPTl29weCAIh+HUpmBQd0UltcPOrA/LFvAf61oYXV0aERhdGFYwnSm6pITyZwvdLIkkrMgz0AmKpTBqVCgOX8pJQtghB7wQQAAAAAAAAAAAAAAAAAAAAAAAAAAAECKU1ppjl9gmhHWyDkgHsUvZmhr6oF3/lD3llzLE2SaOSgOGIsIuAQqgp8JQSUu3r/oOaP8RS44dlQjrH+ALfYtpAECAyYhWCAxnqAfESXOYjKUc2WACuXZ3ch0JHxV0VFrrTyjyjIHXCJYIFnx8H87L4bApR4M+hPcV+fHehEOeW+KCyd0H+WGY8s6') # noqa
-
-# manually adapted by working out which character in the encoded CBOR corresponds to the public key algorithm ID
-UNSUPPORTED_ATTESTATION_OBJECT = base64.b64decode(b'o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEgwRgIhAI1qbvWibQos/t3zsTU05IXw1Ek3SDApATok09uc4UBwAiEAv0fB/lgb5Ot3zJ691Vje6iQLAtLhJDiA8zDxaGjcE3hjeDVjgVkCUzCCAk8wggE3oAMCAQICBDxoKU0wDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMDExLzAtBgNVBAMMJll1YmljbyBVMkYgRUUgU2VyaWFsIDIzOTI1NzM0ODExMTE3OTAxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvd9nk9t3lMNQMXHtLE1FStlzZnUaSLql2fm1ajoggXlrTt8rzXuSehSTEPvEaEdv/FeSqX22L6Aoa8ajIAIOY6M7MDkwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjUwEwYLKwYBBAGC5RwCAQEEBAMCBSAwDQYJKoZIhvcNAQELBQADggEBAKrADVEJfuwVpIazebzEg0D4Z9OXLs5qZ/ukcONgxkRZ8K04QtP/CB5x6olTlxsj+SXArQDCRzEYUgbws6kZKfuRt2a1P+EzUiqDWLjRILSr+3/o7yR7ZP/GpiFKwdm+czb94POoGD+TS1IYdfXj94mAr5cKWx4EKjh210uovu/pLdLjc8xkQciUrXzZpPR9rT2k/q9HkZhHU+NaCJzky+PTyDbq0KKnzqVhWtfkSBCGw3ezZkTS+5lrvOKbIa24lfeTgu7FST5OwTPCFn8HcfWZMXMSD/KNU+iBqJdAwTLPPDRoLLvPTl29weCAIh+HUpmBQd0UltcPOrA/LFvAf61oYXV0aERhdGFYwnSm6pITyZwvdLIkkrMgz0AmKpTBqVCgOX8pJQtghB7wQQAAAAAAAAAAAAAAAAAAAAAAAAAAAECKU1ppjl9gmhHWyDkgHsUvZmhr6oF3/lD3llzLE2SaOSgOGIsIuAQqgp8JQSUu3r/oOaP8RS44dlQjrH+ALfYtpAECAyUhWCAxnqAfESXOYjKUc2WACuXZ3ch0JHxV0VFrrTyjyjIHXCJYIFnx8H87L4bApR4M+hPcV+fHehEOeW+KCyd0H+WGY8s6') # noqa
-
-
-def test_from_registration_verifies_response(webauthn_dev_server):
- registration_response = {
- 'clientDataJSON': CLIENT_DATA_JSON,
- 'attestationObject': ATTESTATION_OBJECT,
- }
-
- credential = WebAuthnCredential.from_registration(SESSION_STATE, registration_response)
- assert credential.name == 'Unnamed key'
- assert credential.registration_response == base64.b64encode(cbor.encode(registration_response)).decode('utf-8')
-
- credential_data = credential.to_credential_data()
- assert type(credential_data.credential_id) is bytes
- assert type(credential_data.aaguid) is bytes
- assert credential_data.public_key[3] == ES256.ALGORITHM
-
-
-def test_from_registration_encodes_as_unicode(webauthn_dev_server):
- registration_response = {
- 'clientDataJSON': CLIENT_DATA_JSON,
- 'attestationObject': ATTESTATION_OBJECT,
- }
-
- credential = WebAuthnCredential.from_registration(SESSION_STATE, registration_response)
-
- serialized_credential = credential.serialize()
-
- assert type(serialized_credential['credential_data']) == str # noqa E721
- assert type(serialized_credential['registration_response']) == str # noqa E721
-
-
-def test_from_registration_handles_library_errors():
- registration_response = {
- 'clientDataJSON': CLIENT_DATA_JSON,
- 'attestationObject': ATTESTATION_OBJECT,
- }
-
- with pytest.raises(RegistrationError) as exc_info:
- WebAuthnCredential.from_registration(SESSION_STATE, registration_response)
-
- assert 'Invalid origin' in str(exc_info.value)
-
-
-def test_from_registration_handles_unsupported_keys(webauthn_dev_server):
- registration_response = {
- 'clientDataJSON': CLIENT_DATA_JSON,
- 'attestationObject': UNSUPPORTED_ATTESTATION_OBJECT,
- }
-
- with pytest.raises(RegistrationError) as exc_info:
- WebAuthnCredential.from_registration(SESSION_STATE, registration_response)
-
- assert 'Encryption algorithm not supported' in str(exc_info.value)
diff --git a/tests/app/notify_client/test_user_client.py b/tests/app/notify_client/test_user_client.py
index e7aff5610..519dd862d 100644
--- a/tests/app/notify_client/test_user_client.py
+++ b/tests/app/notify_client/test_user_client.py
@@ -1,11 +1,9 @@
import uuid
-from unittest.mock import Mock, call
+from unittest.mock import call
import pytest
-from notifications_python_client.errors import HTTPError
from app import invite_api_client, service_api_client, user_api_client
-from app.models.webauthn_credential import WebAuthnCredential
from tests import sample_uuid
from tests.conftest import SERVICE_ONE_ID
@@ -190,7 +188,6 @@ def test_returns_value_from_cache(
(user_api_client, 'update_password', [user_id, 'hunter2'], {}),
(user_api_client, 'verify_password', [user_id, 'hunter2'], {}),
(user_api_client, 'check_verify_code', [user_id, '', ''], {}),
- (user_api_client, 'complete_webauthn_login_attempt', [user_id], {'is_successful': True}),
(user_api_client, 'add_user_to_service', [SERVICE_ONE_ID, user_id, [], []], {}),
(user_api_client, 'add_user_to_organization', [sample_uuid(), user_id], {}),
(user_api_client, 'set_user_permissions', [user_id, SERVICE_ONE_ID, []], {}),
@@ -241,71 +238,6 @@ def test_add_user_to_service_calls_correct_endpoint_and_deletes_keys_from_cache(
]
-def test_get_webauthn_credentials_for_user(mocker, webauthn_credential, fake_uuid):
-
- mock_get = mocker.patch(
- 'app.notify_client.user_api_client.UserApiClient.get',
- return_value={'data': [webauthn_credential]}
- )
-
- credentials = user_api_client.get_webauthn_credentials_for_user(fake_uuid)
-
- mock_get.assert_called_once_with(f'/user/{fake_uuid}/webauthn')
- assert len(credentials) == 1
- assert credentials[0]['name'] == 'Test credential'
-
-
-def test_create_webauthn_credential_for_user(mocker, webauthn_credential, fake_uuid):
- credential = WebAuthnCredential(webauthn_credential)
-
- mock_post = mocker.patch('app.notify_client.user_api_client.UserApiClient.post')
- expected_url = f'/user/{fake_uuid}/webauthn'
-
- user_api_client.create_webauthn_credential_for_user(fake_uuid, credential)
- mock_post.assert_called_once_with(expected_url, data=credential.serialize())
-
-
-def test_complete_webauthn_login_attempt_returns_true_and_no_message_normally(fake_uuid, mocker):
- mock_post = mocker.patch('app.notify_client.user_api_client.UserApiClient.post')
-
- resp = user_api_client.complete_webauthn_login_attempt(fake_uuid, is_successful=True)
-
- expected_data = {'successful': True}
- mock_post.assert_called_once_with(f'/user/{fake_uuid}/complete/webauthn-login', data=expected_data)
- assert resp == (True, '')
-
-
-def test_complete_webauthn_login_attempt_returns_false_and_message_on_403(fake_uuid, mocker):
- mock_post = mocker.patch(
- 'app.notify_client.user_api_client.UserApiClient.post',
- side_effect=HTTPError(
- response=Mock(
- status_code=403,
- json=Mock(
- return_value={'message': 'forbidden'}
- )
- )
- )
- )
-
- resp = user_api_client.complete_webauthn_login_attempt(fake_uuid, is_successful=True)
-
- expected_data = {'successful': True}
- mock_post.assert_called_once_with(f'/user/{fake_uuid}/complete/webauthn-login', data=expected_data)
-
- assert resp == (False, 'forbidden')
-
-
-def test_complete_webauthn_login_attempt_raises_on_api_error(fake_uuid, mocker):
- mocker.patch(
- 'app.notify_client.user_api_client.UserApiClient.post',
- side_effect=HTTPError(response=Mock(status_code=503, message='error'))
- )
-
- with pytest.raises(HTTPError):
- user_api_client.complete_webauthn_login_attempt(fake_uuid, is_successful=True)
-
-
def test_reset_password(
mocker,
fake_uuid,
diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py
index 7afce65f3..b82ff64db 100644
--- a/tests/app/test_navigation.py
+++ b/tests/app/test_navigation.py
@@ -234,27 +234,22 @@ EXCLUDED_ENDPOINTS = tuple(map(Navigation.get_endpoint_with_blueprint, {
'two_factor_email',
'two_factor_email_interstitial',
'two_factor_email_sent',
- 'two_factor_webauthn',
'update_email_branding',
'uploads',
'usage',
'user_information',
'user_profile',
'user_profile_confirm_delete_mobile_number',
- 'user_profile_confirm_delete_security_key',
- 'user_profile_delete_security_key',
'user_profile_disable_platform_admin_view',
'user_profile_email',
'user_profile_email_authenticate',
'user_profile_email_confirm',
- 'user_profile_manage_security_key',
'user_profile_mobile_number',
'user_profile_mobile_number_authenticate',
'user_profile_mobile_number_confirm',
'user_profile_mobile_number_delete',
'user_profile_name',
'user_profile_password',
- 'user_profile_security_keys',
'using_notify',
'verify',
'verify_email',
@@ -271,10 +266,6 @@ EXCLUDED_ENDPOINTS = tuple(map(Navigation.get_endpoint_with_blueprint, {
'view_template',
'view_template_version',
'view_template_versions',
- 'webauthn_begin_register',
- 'webauthn_complete_register',
- 'webauthn_begin_authentication',
- 'webauthn_complete_authentication',
'who_its_for',
}))
diff --git a/tests/app/test_webauthn_server.py b/tests/app/test_webauthn_server.py
deleted file mode 100644
index c2bc70381..000000000
--- a/tests/app/test_webauthn_server.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import pytest
-
-from app import webauthn_server
-
-
-@pytest.fixture
-def app_with_mock_config(mocker):
- app = mocker.Mock()
-
- app.config = {
- 'ADMIN_BASE_URL': 'https://www.notify.works',
- 'NOTIFY_ENVIRONMENT': 'development'
- }
-
- return app
-
-
-@pytest.mark.parametrize(('environment, allowed'), [
- ('development', True),
- ('production', False)
-])
-def test_server_origin_verification(
- app_with_mock_config,
- environment,
- allowed
-):
-
- app_with_mock_config.config['NOTIFY_ENVIRONMENT'] = environment
- webauthn_server.init_app(app_with_mock_config)
- assert app_with_mock_config.webauthn_server._verify('fake-domain') == allowed
-
-
-def test_server_relying_party_id(
- app_with_mock_config,
- mocker,
-):
- webauthn_server.init_app(app_with_mock_config)
- assert app_with_mock_config.webauthn_server.rp.id == 'www.notify.works'
diff --git a/tests/conftest.py b/tests/conftest.py
index 69ab8864e..946cf4984 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -12,7 +12,7 @@ from flask import Flask, url_for
from notifications_python_client.errors import HTTPError
from notifications_utils.url_safe_token import generate_token
-from app import create_app, webauthn_server
+from app import create_app
from . import (
TestClient,
@@ -2483,20 +2483,6 @@ def set_config_values(app, dict):
app.config[key] = old_values[key]
-@pytest.fixture
-def webauthn_dev_server(notify_admin, mocker):
- overrides = {
- 'NOTIFY_ENVIRONMENT': 'development',
- 'ADMIN_BASE_URL': 'https://webauthn.io',
- }
-
- with set_config_values(notify_admin, overrides):
- webauthn_server.init_app(notify_admin)
- yield
-
- webauthn_server.init_app(notify_admin)
-
-
@pytest.fixture(scope='function')
def valid_token(notify_admin, fake_uuid):
return generate_token(
@@ -3078,15 +3064,13 @@ def create_active_user_manage_template_permissions(with_unique_id=False):
)
-def create_platform_admin_user(with_unique_id=False, auth_type='webauthn_auth', permissions=None):
+def create_platform_admin_user(with_unique_id=False, permissions=None):
return create_user(
id=str(uuid4()) if with_unique_id else sample_uuid(),
name='Platform admin user',
email_address='platform@admin.gsa.gov',
permissions=permissions or {},
- platform_admin=True,
- auth_type=auth_type,
- can_use_webauthn=True,
+ platform_admin=True
)
@@ -3132,7 +3116,6 @@ def create_user(**overrides):
'current_session_id': None,
'logged_in_at': None,
'email_access_validated_at': None,
- 'can_use_webauthn': False,
}
user_data.update(overrides)
return user_data
@@ -3364,28 +3347,6 @@ def mock_get_invited_org_user_by_id(mocker, sample_org_invite):
)
-@pytest.fixture
-def webauthn_credential():
- return {
- 'id': str(uuid4()),
- 'name': 'Test credential',
- 'credential_data': 'WJ8AAAAAAAAAAAAAAAAAAAAAAECKU1ppjl9gmhHWyDkgHsUvZmhr6oF3/lD3llzLE2SaOSgOGIsIuAQqgp8JQSUu3r/oOaP8RS44dlQjrH+ALfYtpQECAyYgASFYIDGeoB8RJc5iMpRzZYAK5dndyHQkfFXRUWutPKPKMgdcIlggWfHwfzsvhsClHgz6E9xX58d6EQ55b4oLJ3Qf5YZjyzo=', # noqa
- 'registration_response': 'anything',
- 'created_at': '2017-10-18T16:57:14.154185Z',
- }
-
-
-@pytest.fixture
-def webauthn_credential_2():
- return {
- 'id': str(uuid4()),
- 'name': 'Another test credential',
- 'credential_data': 'WJ0AAAAAAAAAAAAAAAAAAAAAAECKU1jppl9mhgHWyDkgHsUvZmhr6oF3/lD3llzLE2SaOSgOGIsIuAQqgp8JQSUu3r/oOaP8RS44dlQjrH+ALfYtpAECAyYhWCAxnqAfESXOYjKUc2WACuXZ3ch0JHxV0VFrrTyjyjIHXCJYIFnx8L4H87bApR4M+hPcV+fHehEOeW+KCyd0H+WGY8s6', # noqa
- 'registration_response': 'stuff',
- 'created_at': '2021-05-14T16:57:14.154185Z',
- }
-
-
@pytest.fixture(scope='session')
def end_to_end_auth_context(browser):
# Create a context with HTTP Authentication credentials for Playwright E2E
diff --git a/tests/javascripts/authenticateSecurityKey.test.js b/tests/javascripts/authenticateSecurityKey.test.js
deleted file mode 100644
index 9dabd1d63..000000000
--- a/tests/javascripts/authenticateSecurityKey.test.js
+++ /dev/null
@@ -1,279 +0,0 @@
-beforeAll(() => {
- window.CBOR = require('../../node_modules/cbor-js/cbor.js')
- require('../../app/assets/javascripts/authenticateSecurityKey.js')
-
- // populate missing values to allow consistent jest.spyOn()
- window.fetch = () => { }
- window.navigator.credentials = { get: () => { } }
- window.GOVUK.ErrorBanner = {
- showBanner: () => {},
- hideBanner: () => {}
- };
-})
-
-afterAll(() => {
- require('./support/teardown.js')
-
- // restore window attributes to their original undefined state
- delete window.fetch
- delete window.navigator.credentials
-})
-
-describe('Authenticate with security key', () => {
- let button
-
- beforeEach(() => {
- // disable console.error() so we don't see it in test output
- // you might need to comment this out to debug some failures
- jest.spyOn(console, 'error').mockImplementation(() => {})
-
- document.body.innerHTML = `
- `
-
- button = document.querySelector('[data-module="authenticate-security-key"]')
- window.GOVUK.modules.start()
- })
-
- afterEach(() => {
- jest.restoreAllMocks()
- })
-
- test('authenticates a credential and redirects based on the admin app response', (done) => {
-
- jest.spyOn(window, 'fetch')
- .mockImplementationOnce((_url) => {
- // initial fetch of options from the server
- // fetch defaults to GET
- // options from the server are CBOR-encoded
- const webauthnOptions = window.CBOR.encode('someArbitraryOptions')
-
- return Promise.resolve({
- ok: true, arrayBuffer: () => webauthnOptions
- })
- })
-
- jest.spyOn(window.navigator.credentials, 'get').mockImplementation((options) => {
- expect(options).toEqual('someArbitraryOptions')
-
- // fake PublicKeyCredential response from WebAuthn API
- // all of the array properties represent Array(Buffer) objects
- const credentialsGetResponse = {
- response: {
- authenticatorData: [2, 2, 2],
- signature: [3, 3, 3],
- clientDataJSON: [4, 4, 4]
- },
- rawId: [1, 1, 1],
- type: "public-key",
- }
- return Promise.resolve(credentialsGetResponse)
- })
-
- jest.spyOn(window, 'fetch')
- .mockImplementationOnce((_url, options = {}) => {
- // subsequent POST of credential data to server
- const decodedData = window.CBOR.decode(options.body)
- expect(decodedData.credentialId).toEqual(new Uint8Array([1, 1, 1]))
- expect(decodedData.authenticatorData).toEqual(new Uint8Array([2, 2, 2]))
- expect(decodedData.signature).toEqual(new Uint8Array([3, 3, 3]))
- expect(decodedData.clientDataJSON).toEqual(new Uint8Array([4, 4, 4]))
- expect(options.headers['X-CSRFToken']).toBe('abc123')
- const loginResponse = window.CBOR.encode({ redirect_url: '/foo' })
-
- return Promise.resolve({
- ok: true, arrayBuffer: () => Promise.resolve(loginResponse)
- })
- })
-
- jest.spyOn(window.location, 'assign').mockImplementation((href) => {
- expect(href).toEqual("/foo")
- done()
- })
-
- // this will make the test fail if the error banner is displayed
- jest.spyOn(window.GOVUK.ErrorBanner, 'showBanner').mockImplementation(() => {
- done('didnt expect the banner to be shown')
- })
-
- button.click()
- })
-
- test('authenticates and passes a redirect url through to the authenticate admin endpoint', (done) => {
- window.location.href = `${window.location.href}?next=%2Ffoo%3Fbar%3Dbaz`
- jest.spyOn(window, 'fetch')
- .mockImplementationOnce((_url) => {
- // initial fetch of options from the server
- // fetch defaults to GET
- // options from the server are CBOR-encoded
- let webauthnOptions = window.CBOR.encode('someArbitraryOptions')
-
- return Promise.resolve({
- ok: true, arrayBuffer: () => webauthnOptions
- })
- })
-
- jest.spyOn(window.navigator.credentials, 'get').mockImplementation((options) => {
- let credentialsGetResponse = {
- response: {
- authenticatorData: [],
- signature: [],
- clientDataJSON: []
- },
- rawId: [],
- type: "public-key",
- }
- return Promise.resolve(credentialsGetResponse)
- })
-
- jest.spyOn(window, 'fetch')
- .mockImplementationOnce((url, options = {}) => {
- // subsequent POST of credential data to server
- expect(url.toString()).toEqual(
- 'https://www.notifications.service.gov.uk/webauthn/authenticate?next=%2Ffoo%3Fbar%3Dbaz'
- )
- return Promise.resolve({
- ok: true, arrayBuffer: () => Promise.resolve(window.CBOR.encode({ redirect_url: '/foo' }))
- })
- })
-
- jest.spyOn(window.location, 'assign').mockImplementation((href) => {
- // ensure that the fetch mock implementation above was called
- expect.assertions(1)
- done()
- })
-
- button.click()
- })
-
- test.each([
- ['network'],
- ['server'],
- ])('errors if fetching WebAuthn fails (%s error)', (errorType, done) => {
- jest.spyOn(window, 'fetch').mockImplementation((_url) => {
- if (errorType == 'network') {
- return Promise.reject('error')
- } else {
- return Promise.resolve({ ok: false, statusText: 'error' })
- }
- })
-
- jest.spyOn(window.GOVUK.ErrorBanner, 'showBanner').mockImplementation(() => {
- done()
- })
-
- button.click()
- })
-
- test('errors if comms with the authenticator fails', (done) => {
- jest.spyOn(window, 'fetch')
- .mockImplementationOnce((_url) => {
- const webauthnOptions = window.CBOR.encode('someArbitraryOptions')
-
- return Promise.resolve({
- ok: true, arrayBuffer: () => webauthnOptions
- })
- })
-
- jest.spyOn(window.navigator.credentials, 'get').mockImplementation(() => {
- return Promise.reject(new DOMException('error'))
- })
-
- jest.spyOn(window.GOVUK.ErrorBanner, 'showBanner').mockImplementation(() => {
- done()
- })
-
- button.click()
- })
-
- test.each([
- ['network'],
- ['server'],
- ])('errors if POSTing WebAuthn credentials fails (%s)', (errorType, done) => {
- jest.spyOn(window, 'fetch')
- .mockImplementationOnce((_url) => {
- const webauthnOptions = window.CBOR.encode('someArbitraryOptions')
-
- return Promise.resolve({
- ok: true, arrayBuffer: () => webauthnOptions
- })
- })
-
- jest.spyOn(window.navigator.credentials, 'get').mockImplementation((options) => {
- expect(options).toEqual('someArbitraryOptions')
- const credentialsGetResponse = {
- response: {
- authenticatorData: [2, 2, 2],
- signature: [3, 3, 3],
- clientDataJSON: [4, 4, 4]
- },
- rawId: [1, 1, 1],
- type: "public-key",
- }
- return Promise.resolve(credentialsGetResponse)
- })
-
- jest.spyOn(window, 'fetch').mockImplementationOnce((_url) => {
- // subsequent POST of credential data to server
- switch (errorType) {
- case 'network':
- return Promise.reject('error')
- case 'server':
- return Promise.resolve({ ok: false, statusText: 'FORBIDDEN' })
- }
- })
-
-
- jest.spyOn(window.GOVUK.ErrorBanner, 'showBanner').mockImplementation(() => {
- done()
- })
-
- button.click()
- })
-
-
- test('reloads page if POSTing WebAuthn credentials returns 403', (done) => {
- jest.spyOn(window, 'fetch').mockImplementationOnce((_url) => {
- const webauthnOptions = window.CBOR.encode('someArbitraryOptions')
-
- return Promise.resolve({
- ok: true, arrayBuffer: () => webauthnOptions
- })
- })
-
- jest.spyOn(window.navigator.credentials, 'get').mockImplementation((options) => {
- expect(options).toEqual('someArbitraryOptions')
- const credentialsGetResponse = {
- response: {
- authenticatorData: [2, 2, 2],
- signature: [3, 3, 3],
- clientDataJSON: [4, 4, 4]
- },
- rawId: [1, 1, 1],
- type: "public-key",
- }
- return Promise.resolve(credentialsGetResponse)
- })
-
- jest.spyOn(window, 'fetch').mockImplementationOnce((_url) => {
- return Promise.resolve(
- {
- ok: false,
- status: 403,
- })
- })
-
- // assert that reload is called and the page is refreshed
- jest.spyOn(window.location, 'reload').mockImplementation(() => {
- done()
- })
-
- // this will make the test fail if the error banner is displayed
- jest.spyOn(window.GOVUK.ErrorBanner, 'showBanner').mockImplementation((msg) => {
- done(msg)
- })
-
- button.click()
- })
-
-
-})
diff --git a/tests/javascripts/registerSecurityKey.test.js b/tests/javascripts/registerSecurityKey.test.js
deleted file mode 100644
index a69d7d3c1..000000000
--- a/tests/javascripts/registerSecurityKey.test.js
+++ /dev/null
@@ -1,165 +0,0 @@
-beforeAll(() => {
- window.CBOR = require('../../node_modules/cbor-js/cbor.js')
- require('../../app/assets/javascripts/registerSecurityKey.js')
-
- // populate missing values to allow consistent jest.spyOn()
- window.fetch = () => {}
- window.navigator.credentials = { create: () => { } }
- window.GOVUK.ErrorBanner = {
- showBanner: () => { },
- hideBanner: () => { }
- };
-})
-
-afterAll(() => {
- require('./support/teardown.js')
-
- // restore window attributes to their original undefined state
- delete window.fetch
- delete window.navigator.credentials
-})
-
-describe('Register security key', () => {
- let button
-
- beforeEach(() => {
- // disable console.error() so we don't see it in test output
- // you might need to comment this out to debug some failures
- jest.spyOn(console, 'error').mockImplementation(() => {})
-
- document.body.innerHTML = `
-
- Register a key
- `
-
- button = document.querySelector('[data-module="register-security-key"]')
- window.GOVUK.modules.start()
- })
-
- afterEach(() => {
- jest.restoreAllMocks()
- })
-
- test('creates a new credential and reloads', (done) => {
-
- jest.spyOn(window, 'fetch').mockImplementationOnce((_url) => {
- // initial fetch of options from the server
- // options from the server are CBOR-encoded
- const webauthnOptions = window.CBOR.encode('options')
-
- return Promise.resolve({
- ok: true, arrayBuffer: () => webauthnOptions
- })
- })
-
- jest.spyOn(window.navigator.credentials, 'create').mockImplementation((options) => {
- expect(options).toEqual('options')
-
- // fake PublicKeyCredential response from WebAuthn API
- // both of the nested properties are Array(Buffer) objects
- return Promise.resolve({
- response: {
- attestationObject: [1, 2, 3],
- clientDataJSON: [4, 5, 6],
- }
- })
- })
-
- jest.spyOn(window, 'fetch').mockImplementationOnce((_url, options) => {
- // subsequent POST of credential data to server
- const decodedData = window.CBOR.decode(options.body)
- expect(decodedData.clientDataJSON).toEqual(new Uint8Array([4,5,6]))
- expect(decodedData.attestationObject).toEqual(new Uint8Array([1,2,3]))
- expect(options.headers['X-CSRFToken']).toBe()
- return Promise.resolve({ ok: true })
- })
-
- jest.spyOn(window.location, 'reload').mockImplementation(() => {
- // signal that the async promise chain was called
- done()
- })
-
- // this will make the test fail if the error banner is displayed
- jest.spyOn(window.GOVUK.ErrorBanner, 'showBanner').mockImplementation(() => {
- done('didnt expect the banner to be shown')
- })
-
- button.click()
- })
-
- test.each([
- ['network'],
- ['server'],
- ])('errors if fetching WebAuthn options fails (%s error)', (errorType, done) => {
- jest.spyOn(window, 'fetch').mockImplementation((_url) => {
- if (errorType == 'network') {
- return Promise.reject('error')
- } else {
- return Promise.resolve({ ok: false, statusText: 'error' })
- }
- })
-
- jest.spyOn(window.GOVUK.ErrorBanner, 'showBanner').mockImplementation(() => {
- done()
- })
-
- button.click()
- })
-
- test.each([
- ['network'],
- ['server'],
- ])('errors if sending WebAuthn credentials fails (%s)', (errorType, done) => {
-
- jest.spyOn(window, 'fetch').mockImplementationOnce((_url) => {
- // initial fetch of options from the server
- const webauthnOptions = window.CBOR.encode('options')
-
- return Promise.resolve({
- ok: true, arrayBuffer: () => webauthnOptions
- })
- })
-
- jest.spyOn(window.navigator.credentials, 'create').mockImplementation(() => {
- // fake PublicKeyCredential response from WebAuthn API
- return Promise.resolve({ response: {} })
- })
-
- jest.spyOn(window, 'fetch').mockImplementationOnce((_url) => {
- // subsequent POST of credential data to server
- switch (errorType) {
- case 'network':
- return Promise.reject('error')
- case 'server':
- return Promise.resolve({ ok: false, statusText: 'FORBIDDEN' })
- }
- })
-
- jest.spyOn(window.GOVUK.ErrorBanner, 'showBanner').mockImplementation(() => {
- done()
- })
-
- button.click()
- })
-
- test('errors if comms with the authenticator fails', (done) => {
- jest.spyOn(window.navigator.credentials, 'create').mockImplementation(() => {
- return Promise.reject(new DOMException('error'))
- })
-
- jest.spyOn(window, 'fetch').mockImplementation((_url) => {
- // initial fetch of options from the server
- const webauthnOptions = window.CBOR.encode('options')
-
- return Promise.resolve({
- ok: true, arrayBuffer: () => webauthnOptions
- })
- })
-
- jest.spyOn(window.GOVUK.ErrorBanner, 'showBanner').mockImplementation(() => {
- done()
- })
-
- button.click()
- })
-})