* stages/authenticator_duo: initial duo stage

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* stages/authenticator_duo: improve setup

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* stages/authenticator_validate: add Duo support

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* flows: make use of oneOf OpenAPI to annotate all challenge types

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* outpost: update to new api schema

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* outposts/ldap: fix client usage

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* outposts/ldap: return user info when user can't search

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: fix linting error

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* stages: fix stage unittests

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* flows: add default challenge response

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web/flows: update types

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* flows: fix mismatched names

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* website/docs: add duo docs

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* stages/authenticator_duo: add missing duo device

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web/admin: fix enable buttons missing on stages

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* stages/prompt: annotate PromptChallengeResponse's additionalProperties

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web/flows: improve logging

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* tests/e2e: fix flow titles not being set

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* tests/e2e: fix invalid flows

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* api: fix error when authorization header has no spaces

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* stages/user_write: handle integrity error

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* outposts: handle disconnects without outpost better

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* flows: allow blank on WithUserInfo

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L 2021-05-25 13:35:59 +02:00 committed by GitHub
commit 250e23408e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
108 changed files with 3170 additions and 626 deletions

View File

@ -56,7 +56,7 @@ gen-outpost:
-i /local/schema.yml \
-g go \
-o /local/outpost/api \
--additional-properties=packageName=api,enumClassPrefix=true
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true
rm -f outpost/api/go.mod outpost/api/go.sum
gen: gen-build gen-clean gen-web gen-outpost

View File

@ -44,6 +44,7 @@ urllib3 = {extras = ["secure"],version = "*"}
uvicorn = {extras = ["standard"],version = "*"}
webauthn = "*"
xmlsec = "*"
duo-client = "*"
[requires]
python_version = "3.9"

100
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "61354b75aa954ea0a995ee1909b861092a4be5c1af66d3c00c7c7845e056d064"
"sha256": "eb043e24ba05d5d78459a973fe0cd7c37dad1cca90431f68b6df773247c58cbb"
},
"pipfile-spec": 6,
"requires": {
@ -56,6 +56,7 @@
"sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a",
"sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"
],
"markers": "python_version >= '3.6'",
"version": "==3.7.4.post0"
},
"aioredis": {
@ -70,6 +71,7 @@
"sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2",
"sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"
],
"markers": "python_version >= '3.6'",
"version": "==5.0.6"
},
"asgiref": {
@ -77,6 +79,7 @@
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
],
"markers": "python_version >= '3.6'",
"version": "==3.3.4"
},
"async-timeout": {
@ -84,6 +87,7 @@
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
],
"markers": "python_full_version >= '3.5.3'",
"version": "==3.0.1"
},
"attrs": {
@ -91,6 +95,7 @@
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.2.0"
},
"autobahn": {
@ -98,6 +103,7 @@
"sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac",
"sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03"
],
"markers": "python_version >= '3.7'",
"version": "==21.3.1"
},
"automat": {
@ -127,6 +133,7 @@
"sha256:7518c3f028123f2c770cf1e568f24877259e0ec03badef657174fa93392a9e6a",
"sha256:b69c96f210a79f544c6dea923f708873121c174b8b4d72babd725328bf53e3d2"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.20.79"
},
"cachetools": {
@ -134,6 +141,7 @@
"sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001",
"sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"
],
"markers": "python_version ~= '3.5'",
"version": "==4.2.2"
},
"cbor2": {
@ -152,6 +160,7 @@
"sha256:f0058d33b5eaffb176d6190d175a5391f13362f165881deea2b99e63b66ecf55",
"sha256:f5df0ad8c16f7992bf24e5c9a53f03a11a990fd18253c3c335315bd25a34f832"
],
"markers": "python_version >= '3.6'",
"version": "==5.3.0"
},
"celery": {
@ -244,6 +253,7 @@
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.0.0"
},
"click": {
@ -251,6 +261,7 @@
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"click-didyoumean": {
@ -310,6 +321,7 @@
"sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f",
"sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.2"
},
"defusedxml": {
@ -420,6 +432,14 @@
"index": "pypi",
"version": "==0.16.0"
},
"duo-client": {
"hashes": [
"sha256:790de9573e2a0a85cc10cdb671b37759ee5f6c8557fe9972a8837bb07d71f69b",
"sha256:a5c9282cba3a02ae2ffbb16552e66379c19272664561baec86ad3a661f26ebf2"
],
"index": "pypi",
"version": "==4.3.1"
},
"facebook-sdk": {
"hashes": [
"sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e",
@ -432,6 +452,7 @@
"hashes": [
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.18.2"
},
"geoip2": {
@ -447,6 +468,7 @@
"sha256:044d81b1e58012f8ebc71cc134e191c1fa312f543f1fbc99973afe28c25e3228",
"sha256:b3ca7a8ff9ab3bdefee3ad5aefb11fc6485423767eee016f5942d8e606ca23fb"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.30.1"
},
"gunicorn": {
@ -462,6 +484,7 @@
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
],
"markers": "python_version >= '3.6'",
"version": "==0.12.0"
},
"hiredis": {
@ -508,6 +531,7 @@
"sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0",
"sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"
],
"markers": "python_version >= '3.6'",
"version": "==2.0.0"
},
"httptools": {
@ -556,6 +580,7 @@
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
],
"markers": "python_version >= '3.5'",
"version": "==0.5.1"
},
"jmespath": {
@ -563,6 +588,7 @@
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.0"
},
"jsonschema": {
@ -577,6 +603,7 @@
"sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d",
"sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a"
],
"markers": "python_version >= '3.6'",
"version": "==5.1.0"
},
"kubernetes": {
@ -590,7 +617,10 @@
"ldap3": {
"hashes": [
"sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91",
"sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57"
"sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59",
"sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c",
"sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57",
"sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056"
],
"index": "pypi",
"version": "==2.9"
@ -651,6 +681,7 @@
"hashes": [
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
],
"markers": "python_version >= '3.6'",
"version": "==2.0.3"
},
"msgpack": {
@ -726,6 +757,7 @@
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
],
"markers": "python_version >= '3.6'",
"version": "==5.1.0"
},
"oauthlib": {
@ -733,6 +765,7 @@
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.1.0"
},
"packaging": {
@ -748,6 +781,7 @@
"sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa",
"sha256:b6c5a9643e3545bcbfd9451766cbaa5d9c67e7303c7bc32c750b6fa70ecb107d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.1"
},
"prompt-toolkit": {
@ -755,6 +789,7 @@
"sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04",
"sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc"
],
"markers": "python_full_version >= '3.6.1'",
"version": "==3.0.18"
},
"psycopg2-binary": {
@ -800,15 +835,37 @@
},
"pyasn1": {
"hashes": [
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"
],
"version": "==0.4.8"
},
"pyasn1-modules": {
"hashes": [
"sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0",
"sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d",
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74",
"sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed",
"sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199",
"sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb",
"sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd",
"sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811",
"sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8",
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"
"sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4",
"sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
"sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"
],
"version": "==0.2.8"
},
@ -817,6 +874,7 @@
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.20"
},
"pycryptodome": {
@ -860,6 +918,7 @@
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
],
"markers": "python_version >= '3.5'",
"version": "==2.0.2"
},
"pyjwt": {
@ -882,12 +941,14 @@
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7"
},
"pyrsistent": {
"hashes": [
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
],
"markers": "python_version >= '3.5'",
"version": "==0.17.3"
},
"python-dateutil": {
@ -895,6 +956,7 @@
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.1"
},
"python-dotenv": {
@ -951,6 +1013,7 @@
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==3.5.3"
},
"requests": {
@ -958,12 +1021,14 @@
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.25.1"
},
"requests-oauthlib": {
"hashes": [
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
],
"index": "pypi",
"version": "==1.3.0"
@ -1004,6 +1069,7 @@
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"sqlparse": {
@ -1011,6 +1077,7 @@
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
],
"markers": "python_version >= '3.5'",
"version": "==0.4.1"
},
"structlog": {
@ -1066,6 +1133,7 @@
"sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8",
"sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb"
],
"markers": "python_version >= '3.6'",
"version": "==21.2.1"
},
"typing-extensions": {
@ -1081,6 +1149,7 @@
"sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
"sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.0.1"
},
"urllib3": {
@ -1125,6 +1194,7 @@
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
],
"markers": "python_version >= '3.6'",
"version": "==5.0.0"
},
"watchgod": {
@ -1154,6 +1224,7 @@
"sha256:3e2bf58191d4619b161389a95bdce84ce9e0b24eb8107e7e590db682c2d0ca81",
"sha256:abf306dc6351dcef07f4d40453037e51cc5d9da2ef60d0fc5d0fe3bcda255372"
],
"markers": "python_version >= '3.6'",
"version": "==1.0.1"
},
"websockets": {
@ -1240,6 +1311,7 @@
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
],
"markers": "python_version >= '3.6'",
"version": "==1.6.3"
},
"zope.interface": {
@ -1296,6 +1368,7 @@
"sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4",
"sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==5.4.0"
}
},
@ -1312,6 +1385,7 @@
"sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e",
"sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"
],
"markers": "python_version ~= '3.6'",
"version": "==2.5.6"
},
"attrs": {
@ -1319,6 +1393,7 @@
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.2.0"
},
"bandit": {
@ -1357,6 +1432,7 @@
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.0.0"
},
"click": {
@ -1364,6 +1440,7 @@
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"colorama": {
@ -1437,6 +1514,7 @@
"sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0",
"sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"
],
"markers": "python_version >= '3.4'",
"version": "==4.0.7"
},
"gitpython": {
@ -1444,6 +1522,7 @@
"sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135",
"sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"
],
"markers": "python_version >= '3.5'",
"version": "==3.1.17"
},
"idna": {
@ -1465,6 +1544,7 @@
"sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6",
"sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"
],
"markers": "python_version >= '3.6' and python_version < '4.0'",
"version": "==5.8.0"
},
"lazy-object-proxy": {
@ -1492,6 +1572,7 @@
"sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93",
"sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.6.0"
},
"mccabe": {
@ -1528,6 +1609,7 @@
"sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd",
"sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"
],
"markers": "python_version >= '2.6'",
"version": "==5.6.0"
},
"pluggy": {
@ -1535,6 +1617,7 @@
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.13.1"
},
"py": {
@ -1542,6 +1625,7 @@
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.10.0"
},
"pylint": {
@ -1572,6 +1656,7 @@
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7"
},
"pytest": {
@ -1676,6 +1761,7 @@
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.25.1"
},
"requests-mock": {
@ -1699,6 +1785,7 @@
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"smmap": {
@ -1706,6 +1793,7 @@
"sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182",
"sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"
],
"markers": "python_version >= '3.5'",
"version": "==4.0.0"
},
"stevedore": {
@ -1713,6 +1801,7 @@
"sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee",
"sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"
],
"markers": "python_version >= '3.6'",
"version": "==3.3.0"
},
"toml": {
@ -1720,6 +1809,7 @@
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2"
},
"urllib3": {

View File

@ -18,7 +18,7 @@ LOGGER = get_logger()
def token_from_header(raw_header: bytes) -> Optional[Token]:
"""raw_header in the Format of `Bearer dGVzdDp0ZXN0`"""
auth_credentials = raw_header.decode()
if auth_credentials == "":
if auth_credentials == "" or " " not in auth_credentials:
return None
auth_type, auth_credentials = auth_credentials.split()
if auth_type.lower() not in ["basic", "bearer"]:

View File

@ -67,4 +67,11 @@ def postprocess_schema_responses(result, generator, **kwargs): # noqa: W0613
spectacular_settings.APPEND_COMPONENTS
)
# This is a workaround for authentik/stages/prompt/stage.py
# since the serializer PromptChallengeResponse
# accepts dynamic keys
for component in result["components"]["schemas"]:
if component == "PromptChallengeResponseRequest":
comp = result["components"]["schemas"][component]
comp["additionalProperties"] = {}
return result

View File

@ -64,6 +64,11 @@ from authentik.sources.oauth.api.source_connection import (
)
from authentik.sources.plex.api import PlexSourceViewSet
from authentik.sources.saml.api import SAMLSourceViewSet
from authentik.stages.authenticator_duo.api import (
AuthenticatorDuoStageViewSet,
DuoAdminDeviceViewSet,
DuoDeviceViewSet,
)
from authentik.stages.authenticator_static.api import (
AuthenticatorStaticStageViewSet,
StaticAdminDeviceViewSet,
@ -158,9 +163,15 @@ router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
router.register("propertymappings/scope", ScopeMappingViewSet)
router.register("authenticators/duo", DuoDeviceViewSet)
router.register("authenticators/static", StaticDeviceViewSet)
router.register("authenticators/totp", TOTPDeviceViewSet)
router.register("authenticators/webauthn", WebAuthnDeviceViewSet)
router.register(
"authenticators/admin/duo",
DuoAdminDeviceViewSet,
basename="admin-duodevice",
)
router.register(
"authenticators/admin/static",
StaticAdminDeviceViewSet,
@ -176,6 +187,7 @@ router.register(
)
router.register("stages/all", StageViewSet)
router.register("stages/authenticator/duo", AuthenticatorDuoStageViewSet)
router.register("stages/authenticator/static", AuthenticatorStaticStageViewSet)
router.register("stages/authenticator/totp", AuthenticatorTOTPStageViewSet)
router.register("stages/authenticator/validate", AuthenticatorValidateStageViewSet)

View File

@ -4,7 +4,7 @@ from importlib import import_module
from django.apps import AppConfig
from django.db import ProgrammingError
from django.utils.timezone import datetime
from django.utils.timezone import now
class AuthentikEventsConfig(AppConfig):
@ -19,7 +19,7 @@ class AuthentikEventsConfig(AppConfig):
try:
from authentik.events.models import Event
date_from = datetime.now() - timedelta(days=1)
date_from = now() - timedelta(days=1)
for event in Event.objects.filter(created__gte=date_from):
event._set_prom_metrics()

View File

@ -88,7 +88,10 @@ class TaskInfo:
start = default_timer()
if hasattr(self, "start_timestamp"):
start = self.start_timestamp
duration = max(self.finish_timestamp - start, 0)
try:
duration = max(self.finish_timestamp - start, 0)
except TypeError:
duration = 0
GAUGE_TASKS.labels(
task_name=self.task_name,
task_uid=self.result.uid or "",

View File

@ -174,8 +174,8 @@ class FlowViewSet(ModelViewSet):
return HttpResponseBadRequest()
successful = importer.apply()
if not successful:
return Response(status=204)
return HttpResponseBadRequest()
return HttpResponseBadRequest()
return Response(status=204)
@permission_required(
"authentik_flows.export_flow",

View File

@ -2,6 +2,9 @@
from importlib import import_module
from django.apps import AppConfig
from django.db.utils import ProgrammingError
from authentik.lib.utils.reflection import all_subclasses
class AuthentikFlowsConfig(AppConfig):
@ -14,3 +17,10 @@ class AuthentikFlowsConfig(AppConfig):
def ready(self):
import_module("authentik.flows.signals")
try:
from authentik.flows.models import Stage
for stage in all_subclasses(Stage):
_ = stage().type
except ProgrammingError:
pass

View File

@ -35,9 +35,9 @@ class Challenge(PassiveSerializer):
type = ChoiceField(
choices=[(x.value, x.name) for x in ChallengeTypes],
)
component = CharField(required=False)
title = CharField(required=False)
title = CharField(required=False, allow_blank=True)
background = CharField(required=False)
component = CharField(default="")
response_errors = DictField(
child=ErrorDetailSerializer(many=True), allow_empty=True, required=False
@ -48,18 +48,20 @@ class RedirectChallenge(Challenge):
"""Challenge type to redirect the client"""
to = CharField()
component = CharField(default="xak-flow-redirect")
class ShellChallenge(Challenge):
"""Legacy challenge type to render HTML as-is"""
"""challenge type to render HTML as-is"""
body = CharField()
component = CharField(default="xak-flow-shell")
class WithUserInfoChallenge(Challenge):
"""Challenge base which shows some user info"""
pending_user = CharField()
pending_user = CharField(allow_blank=True)
pending_user_avatar = CharField()
@ -67,6 +69,7 @@ class AccessDeniedChallenge(Challenge):
"""Challenge when a flow's active stage calls `stage_invalid()`."""
error_message = CharField(required=False)
component = CharField(default="ak-stage-access-denied")
class PermissionSerializer(PassiveSerializer):
@ -80,6 +83,7 @@ class ChallengeResponse(PassiveSerializer):
"""Base class for all challenge responses"""
stage: Optional["StageView"]
component = CharField(default="xak-flow-response-default")
def __init__(self, instance=None, data=None, **kwargs):
self.stage = kwargs.pop("stage", None)

View File

@ -26,7 +26,7 @@ PLAN_CONTEXT_SOURCE = "source"
GAUGE_FLOWS_CACHED = UpdatingGauge(
"authentik_flows_cached",
"Cached flows",
update_func=lambda: len(cache.keys("flow_*")),
update_func=lambda: len(cache.keys("flow_*") or []),
)
HIST_FLOWS_PLAN_TIME = Histogram(
"authentik_flows_plan_time",

View File

@ -289,7 +289,11 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
def test_reevaluate_keep(self):
@ -366,7 +370,11 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
def test_reevaluate_remove_consecutive(self):
@ -458,7 +466,11 @@ class TestFlowExecutor(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
def test_stageview_user_identifier(self):

View File

@ -11,7 +11,12 @@ from django.utils.decorators import method_decorator
from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.generic import View
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from drf_spectacular.utils import (
OpenApiParameter,
OpenApiResponse,
PolymorphicProxySerializer,
extend_schema,
)
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from sentry_sdk import capture_exception
@ -22,10 +27,12 @@ from authentik.events.models import cleanse_dict
from authentik.flows.challenge import (
AccessDeniedChallenge,
Challenge,
ChallengeResponse,
ChallengeTypes,
HttpChallengeResponse,
RedirectChallenge,
ShellChallenge,
WithUserInfoChallenge,
)
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
@ -35,7 +42,7 @@ from authentik.flows.planner import (
FlowPlan,
FlowPlanner,
)
from authentik.lib.utils.reflection import class_to_path
from authentik.lib.utils.reflection import all_subclasses, class_to_path
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
LOGGER = get_logger()
@ -46,6 +53,43 @@ SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre"
SESSION_KEY_GET = "authentik_flows_get"
def challenge_types():
"""This is a workaround for PolymorphicProxySerializer not accepting a callable for
`serializers`. This function returns a class which is an iterator, which returns the
subclasses of Challenge, and Challenge itself."""
class Inner(dict):
"""dummy class with custom callback on .items()"""
def items(self):
mapping = {}
classes = all_subclasses(Challenge)
classes.remove(WithUserInfoChallenge)
for cls in classes:
mapping[cls().fields["component"].default] = cls
return mapping.items()
return Inner()
def challenge_response_types():
"""This is a workaround for PolymorphicProxySerializer not accepting a callable for
`serializers`. This function returns a class which is an iterator, which returns the
subclasses of Challenge, and Challenge itself."""
class Inner(dict):
"""dummy class with custom callback on .items()"""
def items(self):
mapping = {}
classes = all_subclasses(ChallengeResponse)
for cls in classes:
mapping[cls(stage=None).fields["component"].default] = cls
return mapping.items()
return Inner()
@method_decorator(xframe_options_sameorigin, name="dispatch")
class FlowExecutorView(APIView):
"""Stage 1 Flow executor, passing requests to Stage Views"""
@ -126,7 +170,11 @@ class FlowExecutorView(APIView):
@extend_schema(
responses={
200: Challenge(),
200: PolymorphicProxySerializer(
component_name="FlowChallengeRequest",
serializers=challenge_types(),
resource_type_field_name="component",
),
404: OpenApiResponse(
description="No Token found"
), # This error can be raised by the email stage
@ -159,8 +207,18 @@ class FlowExecutorView(APIView):
return to_stage_response(request, FlowErrorResponse(request, exc))
@extend_schema(
responses={200: Challenge()},
request=OpenApiTypes.OBJECT,
responses={
200: PolymorphicProxySerializer(
component_name="FlowChallengeRequest",
serializers=challenge_types(),
resource_type_field_name="component",
),
},
request=PolymorphicProxySerializer(
component_name="FlowChallengeResponse",
serializers=challenge_response_types(),
resource_type_field_name="component",
),
parameters=[
OpenApiParameter(
name="query",
@ -219,7 +277,7 @@ class FlowExecutorView(APIView):
if self.plan.stages:
self._logger.debug(
"f(exec): Continuing with next stage",
reamining=len(self.plan.stages),
remaining=len(self.plan.stages),
)
kwargs = self.kwargs
kwargs.update({"flow_slug": self.flow.slug})

View File

@ -50,7 +50,7 @@ class WebsocketMessage:
class OutpostConsumer(AuthJsonConsumer):
"""Handler for Outposts that connect over websockets for health checks and live updates"""
outpost: Outpost
outpost: Optional[Outpost] = None
last_uid: Optional[str] = None
@ -95,6 +95,9 @@ class OutpostConsumer(AuthJsonConsumer):
uid = msg.args.get("uuid", self.channel_name)
self.last_uid = uid
if not self.outpost:
raise DenyConnection()
state = OutpostState.for_instance_uid(self.outpost, uid)
if self.channel_name not in state.channel_ids:
state.channel_ids.append(self.channel_name)

View File

@ -25,7 +25,7 @@ CURRENT_PROCESS = current_process()
GAUGE_POLICIES_CACHED = UpdatingGauge(
"authentik_policies_cached",
"Cached Policies",
update_func=lambda: len(cache.keys("policy_*")),
update_func=lambda: len(cache.keys("policy_*") or []),
)
HIST_POLICIES_BUILD_TIME = Histogram(
"authentik_policies_build_time",

View File

@ -194,6 +194,7 @@ class TestAuthorize(OAuthTestCase):
self.assertJSONEqual(
force_str(response.content),
{
"component": "xak-flow-redirect",
"type": ChallengeTypes.REDIRECT.value,
"to": f"foo://localhost?code={code.code}&state={state}",
},
@ -232,6 +233,7 @@ class TestAuthorize(OAuthTestCase):
self.assertJSONEqual(
force_str(response.content),
{
"component": "xak-flow-redirect",
"type": ChallengeTypes.REDIRECT.value,
"to": (
f"http://localhost#access_token={token.access_token}"

View File

@ -3,7 +3,6 @@ from typing import Any, Optional
from django.http import HttpRequest, HttpResponse
from django.http.response import HttpResponseBadRequest
from django.utils.translation import gettext_lazy as _
from django.views import View
from structlog.stdlib import get_logger
@ -38,14 +37,14 @@ class UserInfoView(View):
# GitHub Compatibility Scopes are handeled differently, since they required custom paths
# Hence they don't exist as Scope objects
github_scope_map = {
SCOPE_GITHUB_USER: _("GitHub Compatibility: Access your User Information"),
SCOPE_GITHUB_USER_READ: _(
SCOPE_GITHUB_USER: ("GitHub Compatibility: Access your User Information"),
SCOPE_GITHUB_USER_READ: (
"GitHub Compatibility: Access your User Information"
),
SCOPE_GITHUB_USER_EMAIL: _(
SCOPE_GITHUB_USER_EMAIL: (
"GitHub Compatibility: Access you Email addresses"
),
SCOPE_GITHUB_ORG_READ: _("GitHub Compatibility: Access your Groups"),
SCOPE_GITHUB_ORG_READ: ("GitHub Compatibility: Access your Groups"),
}
for scope in scopes:
if scope in github_scope_map:

View File

@ -34,6 +34,13 @@ class AutosubmitChallenge(Challenge):
url = CharField()
attrs = DictField(child=CharField())
component = CharField(default="ak-stage-autosubmit")
class AutoSubmitChallengeResponse(ChallengeResponse):
"""Pseudo class for autosubmit response"""
component = CharField(default="ak-stage-autosubmit")
# This View doesn't have a URL on purpose, as its called by the FlowExecutor
@ -42,6 +49,8 @@ class SAMLFlowFinalView(ChallengeStageView):
and redirects to the SP (if REDIRECT is configured) or shows an auto-submit element
(if POST is configured)."""
response_class = AutoSubmitChallengeResponse
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
provider: SAMLProvider = get_object_or_404(

View File

@ -109,6 +109,7 @@ INSTALLED_APPS = [
"authentik.sources.oauth",
"authentik.sources.plex",
"authentik.sources.saml",
"authentik.stages.authenticator_duo",
"authentik.stages.authenticator_static",
"authentik.stages.authenticator_totp",
"authentik.stages.authenticator_validate",

View File

@ -8,7 +8,7 @@ from rest_framework.serializers import BaseSerializer
from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UILoginButton
from authentik.flows.challenge import Challenge, ChallengeTypes
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.providers.oauth2.generators import generate_client_id
@ -17,6 +17,13 @@ class PlexAuthenticationChallenge(Challenge):
client_id = CharField()
slug = CharField()
component = CharField(default="ak-flow-sources-plex")
class PlexAuthenticationChallengeResponse(ChallengeResponse):
"""Pseudo class for plex response"""
component = CharField(default="ak-flow-sources-plex")
class PlexSource(Source):

View File

@ -8,7 +8,6 @@ from django.http.response import HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect
from django.utils.decorators import method_decorator
from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from structlog.stdlib import get_logger
@ -134,10 +133,8 @@ class InitiateView(View):
return bad_request_message(request, str(exc))
injected_stages = []
plan_kwargs = {
PLAN_CONTEXT_TITLE: _("Redirecting to %(app)s..." % {"app": source.name}),
PLAN_CONTEXT_CONSENT_TITLE: _(
"Redirecting to %(app)s..." % {"app": source.name}
),
PLAN_CONTEXT_TITLE: f"Redirecting to {source.name}...",
PLAN_CONTEXT_CONSENT_TITLE: f"Redirecting to {source.name}...",
PLAN_CONTEXT_ATTRS: {
"SAMLRequest": saml_request,
"RelayState": relay_state,

View File

@ -0,0 +1,103 @@
"""AuthenticatorDuoStage API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.flows.api.stages import StageSerializer
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_duo.stage import (
SESSION_KEY_DUO_ACTIVATION_CODE,
SESSION_KEY_DUO_USER_ID,
)
class AuthenticatorDuoStageSerializer(StageSerializer):
"""AuthenticatorDuoStage Serializer"""
class Meta:
model = AuthenticatorDuoStage
fields = StageSerializer.Meta.fields + [
"configure_flow",
"client_id",
"client_secret",
"api_hostname",
]
extra_kwargs = {
"client_secret": {"write_only": True},
}
class AuthenticatorDuoStageViewSet(ModelViewSet):
"""AuthenticatorDuoStage Viewset"""
queryset = AuthenticatorDuoStage.objects.all()
serializer_class = AuthenticatorDuoStageSerializer
@extend_schema(
request=OpenApiTypes.NONE,
responses={
204: OpenApiResponse(description="Enrollment successful"),
420: OpenApiResponse(description="Enrollment pending/failed"),
},
)
@action(methods=["POST"], detail=True, permission_classes=[])
# pylint: disable=invalid-name,unused-argument
def enrollment_status(self, request: Request, pk: str) -> Response:
"""Check enrollment status of user details in current session"""
stage: AuthenticatorDuoStage = self.get_object()
client = stage.client
user_id = self.request.session.get(SESSION_KEY_DUO_USER_ID)
activation_code = self.request.session.get(SESSION_KEY_DUO_ACTIVATION_CODE)
status = client.enroll_status(user_id, activation_code)
if status == "success":
return Response(status=204)
return Response(status=420)
class DuoDeviceSerializer(ModelSerializer):
"""Serializer for Duo authenticator devices"""
class Meta:
model = DuoDevice
fields = ["pk", "name"]
depth = 2
class DuoDeviceViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""Viewset for Duo authenticator devices"""
queryset = DuoDevice.objects.all()
serializer_class = DuoDeviceSerializer
search_fields = ["name"]
filterset_fields = ["name"]
ordering = ["name"]
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
class DuoAdminDeviceViewSet(ReadOnlyModelViewSet):
"""Viewset for Duo authenticator devices (for admins)"""
permission_classes = [IsAdminUser]
queryset = DuoDevice.objects.all()
serializer_class = DuoDeviceSerializer
search_fields = ["name"]
filterset_fields = ["name"]
ordering = ["name"]

View File

@ -0,0 +1,10 @@
"""authentik duo app config"""
from django.apps import AppConfig
class AuthentikStageAuthenticatorDuoConfig(AppConfig):
"""authentik duo config"""
name = "authentik.stages.authenticator_duo"
label = "authentik_stages_authenticator_duo"
verbose_name = "authentik Stages.Authenticator.Duo"

View File

@ -0,0 +1,98 @@
# Generated by Django 3.2.3 on 2021-05-23 20:28
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_flows", "0018_oob_flows"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="AuthenticatorDuoStage",
fields=[
(
"stage_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_flows.stage",
),
),
("client_id", models.TextField()),
("client_secret", models.TextField()),
("api_hostname", models.TextField()),
(
"configure_flow",
models.ForeignKey(
blank=True,
help_text="Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_flows.flow",
),
),
],
options={
"verbose_name": "Duo Authenticator Setup Stage",
"verbose_name_plural": "Duo Authenticator Setup Stages",
},
bases=("authentik_flows.stage", models.Model),
),
migrations.CreateModel(
name="DuoDevice",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
help_text="The human-readable name of this device.",
max_length=64,
),
),
(
"confirmed",
models.BooleanField(
default=True, help_text="Is this device ready for use?"
),
),
("duo_user_id", models.TextField()),
(
"stage",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_stages_authenticator_duo.authenticatorduostage",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Duo Device",
"verbose_name_plural": "Duo Devices",
},
),
]

View File

@ -0,0 +1,86 @@
"""Duo stage"""
from typing import Optional, Type
from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.views import View
from django_otp.models import Device
from duo_client.auth import Auth
from rest_framework.serializers import BaseSerializer
from authentik import __version__
from authentik.core.types import UserSettingSerializer
from authentik.flows.models import ConfigurableStage, Stage
class AuthenticatorDuoStage(ConfigurableStage, Stage):
"""Setup Duo authenticator devices"""
client_id = models.TextField()
client_secret = models.TextField()
api_hostname = models.TextField()
@property
def serializer(self) -> BaseSerializer:
from authentik.stages.authenticator_duo.api import (
AuthenticatorDuoStageSerializer,
)
return AuthenticatorDuoStageSerializer
@property
def type(self) -> Type[View]:
from authentik.stages.authenticator_duo.stage import AuthenticatorDuoStageView
return AuthenticatorDuoStageView
@property
def client(self) -> Auth:
"""Get an API Client to talk to duo"""
client = Auth(
self.client_id,
self.client_secret,
self.api_hostname,
user_agent=f"authentik {__version__}",
)
return client
@property
def component(self) -> str:
return "ak-stage-authenticator-duo-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={
"title": str(self._meta.verbose_name),
"component": "ak-user-settings-authenticator-duo",
}
)
def __str__(self) -> str:
return f"Duo Authenticator Setup Stage {self.name}"
class Meta:
verbose_name = _("Duo Authenticator Setup Stage")
verbose_name_plural = _("Duo Authenticator Setup Stages")
class DuoDevice(Device):
"""Duo Device for a single user"""
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
# Connect to the stage to when validating access we know the API Credentials
stage = models.ForeignKey(AuthenticatorDuoStage, on_delete=models.CASCADE)
duo_user_id = models.TextField()
def __str__(self):
return self.name or str(self.user)
class Meta:
verbose_name = _("Duo Device")
verbose_name_plural = _("Duo Devices")

View File

@ -0,0 +1,86 @@
"""Duo stage"""
from django.http import HttpRequest, HttpResponse
from rest_framework.fields import CharField
from structlog.stdlib import get_logger
from authentik.flows.challenge import (
Challenge,
ChallengeResponse,
ChallengeTypes,
WithUserInfoChallenge,
)
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
LOGGER = get_logger()
SESSION_KEY_DUO_USER_ID = "authentik_stages_authenticator_duo_user_id"
SESSION_KEY_DUO_ACTIVATION_CODE = "authentik_stages_authenticator_duo_activation_code"
class AuthenticatorDuoChallenge(WithUserInfoChallenge):
"""Duo Challenge"""
activation_barcode = CharField()
activation_code = CharField()
stage_uuid = CharField()
component = CharField(default="ak-stage-authenticator-duo")
class AuthenticatorDuoChallengeResponse(ChallengeResponse):
"""Pseudo class for duo response"""
component = CharField(default="ak-stage-authenticator-duo")
class AuthenticatorDuoStageView(ChallengeStageView):
"""Duo stage"""
response_class = AuthenticatorDuoChallengeResponse
def get_challenge(self, *args, **kwargs) -> Challenge:
user = self.get_pending_user()
stage: AuthenticatorDuoStage = self.executor.current_stage
enroll = stage.client.enroll(user.username)
user_id = enroll["user_id"]
self.request.session[SESSION_KEY_DUO_USER_ID] = user_id
self.request.session[SESSION_KEY_DUO_ACTIVATION_CODE] = enroll[
"activation_code"
]
return AuthenticatorDuoChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"activation_barcode": enroll["activation_barcode"],
"activation_code": enroll["activation_code"],
"stage_uuid": stage.stage_uuid,
}
)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
user = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER)
if not user:
LOGGER.debug("No pending user, continuing")
return self.executor.stage_ok()
return super().get(request, *args, **kwargs)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
# Duo Challenge has already been validated
stage: AuthenticatorDuoStage = self.executor.current_stage
user_id = self.request.session.get(SESSION_KEY_DUO_USER_ID)
activation_code = self.request.session.get(SESSION_KEY_DUO_ACTIVATION_CODE)
enroll_status = stage.client.enroll_status(user_id, activation_code)
if enroll_status != "success":
return HttpResponse(status=420)
existing_device = DuoDevice.objects.filter(duo_user_id=user_id).first()
self.request.session.pop(SESSION_KEY_DUO_USER_ID)
self.request.session.pop(SESSION_KEY_DUO_ACTIVATION_CODE)
if not existing_device:
DuoDevice.objects.create(
user=self.get_pending_user(), duo_user_id=user_id, stage=stage
)
else:
return self.executor.stage_invalid(
"Device with Credential ID already exists."
)
return self.executor.stage_ok()

View File

@ -22,17 +22,25 @@ class AuthenticatorStaticChallenge(WithUserInfoChallenge):
"""Static authenticator challenge"""
codes = ListField(child=CharField())
component = CharField(default="ak-stage-authenticator-static")
class AuthenticatorStaticChallengeResponse(ChallengeResponse):
"""Pseudo class for static response"""
component = CharField(default="ak-stage-authenticator-static")
class AuthenticatorStaticStageView(ChallengeStageView):
"""Static OTP Setup stage"""
response_class = AuthenticatorStaticChallengeResponse
def get_challenge(self, *args, **kwargs) -> AuthenticatorStaticChallenge:
tokens: list[StaticToken] = self.request.session[SESSION_STATIC_TOKENS]
return AuthenticatorStaticChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-authenticator-static",
"codes": [token.token for token in tokens],
}
)

View File

@ -25,6 +25,7 @@ class AuthenticatorTOTPChallenge(WithUserInfoChallenge):
"""TOTP Setup challenge"""
config_url = CharField()
component = CharField(default="ak-stage-authenticator-totp")
class AuthenticatorTOTPChallengeResponse(ChallengeResponse):
@ -33,6 +34,7 @@ class AuthenticatorTOTPChallengeResponse(ChallengeResponse):
device: TOTPDevice
code = IntegerField()
component = CharField(default="ak-stage-authenticator-totp")
def validate_code(self, code: int) -> int:
"""Validate totp code"""
@ -52,7 +54,6 @@ class AuthenticatorTOTPStageView(ChallengeStageView):
return AuthenticatorTOTPChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-authenticator-totp",
"config_url": device.config_url,
}
)

View File

@ -1,12 +1,13 @@
"""Validation stage challenge checking"""
from django.http import HttpRequest
from django.http.response import Http404
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from django_otp import match_token
from django_otp.models import Device
from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice
from rest_framework.fields import CharField, JSONField
from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger
from webauthn import WebAuthnAssertionOptions, WebAuthnAssertionResponse, WebAuthnUser
from webauthn.webauthn import (
AuthenticationRejectedException,
@ -16,9 +17,13 @@ from webauthn.webauthn import (
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User
from authentik.lib.utils.http import get_client_ip
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
from authentik.stages.authenticator_webauthn.utils import generate_challenge, get_origin
LOGGER = get_logger()
class DeviceChallenge(PassiveSerializer):
"""Single device challenge"""
@ -30,10 +35,10 @@ class DeviceChallenge(PassiveSerializer):
def get_challenge_for_device(request: HttpRequest, device: Device) -> dict:
"""Generate challenge for a single device"""
if isinstance(device, (TOTPDevice, StaticDevice)):
# Code-based challenges have no hints
return {}
return get_webauthn_challenge(request, device)
if isinstance(device, WebAuthnDevice):
return get_webauthn_challenge(request, device)
# Code-based challenges have no hints
return {}
def get_webauthn_challenge(request: HttpRequest, device: WebAuthnDevice) -> dict:
@ -111,3 +116,24 @@ def validate_challenge_webauthn(data: dict, request: HttpRequest, user: User) ->
device.set_sign_count(sign_count)
return data
def validate_challenge_duo(device_pk: int, request: HttpRequest, user: User) -> int:
"""Duo authentication"""
device = get_object_or_404(DuoDevice, pk=device_pk)
if device.user != user:
LOGGER.warning("device mismatch")
raise Http404
stage: AuthenticatorDuoStage = device.stage
response = stage.client.auth(
"auto",
user_id=device.duo_user_id,
ipaddr=get_client_ip(request),
type="authentik Login request",
display_username=user.username,
device="auto",
)
# {'result': 'allow', 'status': 'allow', 'status_msg': 'Success. Logging you in...'}
if response["result"] == "deny":
raise ValidationError("Duo denied access")
return device_pk

View File

@ -17,6 +17,7 @@ class DeviceClasses(models.TextChoices):
STATIC = "static"
TOTP = "totp", _("TOTP")
WEBAUTHN = "webauthn", _("WebAuthn")
DUO = "duo", _("Duo")
def default_device_classes() -> list:

View File

@ -1,7 +1,7 @@
"""Authenticator Validation"""
from django.http import HttpRequest, HttpResponse
from django_otp import devices_for_user
from rest_framework.fields import CharField, JSONField, ListField
from rest_framework.fields import CharField, IntegerField, JSONField, ListField
from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger
@ -17,6 +17,7 @@ from authentik.stages.authenticator_validate.challenge import (
DeviceChallenge,
get_challenge_for_device,
validate_challenge_code,
validate_challenge_duo,
validate_challenge_webauthn,
)
from authentik.stages.authenticator_validate.models import (
@ -29,28 +30,31 @@ LOGGER = get_logger()
PER_DEVICE_CLASSES = [DeviceClasses.WEBAUTHN]
class AuthenticatorChallenge(WithUserInfoChallenge):
class AuthenticatorValidationChallenge(WithUserInfoChallenge):
"""Authenticator challenge"""
device_challenges = ListField(child=DeviceChallenge())
component = CharField(default="ak-stage-authenticator-validate")
class AuthenticatorChallengeResponse(ChallengeResponse):
class AuthenticatorValidationChallengeResponse(ChallengeResponse):
"""Challenge used for Code-based and WebAuthn authenticators"""
code = CharField(required=False)
webauthn = JSONField(required=False)
duo = IntegerField(required=False)
component = CharField(default="ak-stage-authenticator-validate")
def validate_code(self, code: str) -> str:
"""Validate code-based response, raise error if code isn't allowed"""
def _challenge_allowed(self, classes: list):
device_challenges: list[dict] = self.stage.request.session.get(
"device_challenges"
)
if not any(
x["device_class"] in (DeviceClasses.TOTP, DeviceClasses.STATIC)
for x in device_challenges
):
raise ValidationError("Got code but no compatible device class allowed")
if not any(x["device_class"] in classes for x in device_challenges):
raise ValidationError("No compatible device class allowed")
def validate_code(self, code: str) -> str:
"""Validate code-based response, raise error if code isn't allowed"""
self._challenge_allowed([DeviceClasses.TOTP, DeviceClasses.STATIC])
return validate_challenge_code(
code, self.stage.request, self.stage.get_pending_user()
)
@ -58,21 +62,22 @@ class AuthenticatorChallengeResponse(ChallengeResponse):
def validate_webauthn(self, webauthn: dict) -> dict:
"""Validate webauthn response, raise error if webauthn wasn't allowed
or response is invalid"""
device_challenges: list[dict] = self.stage.request.session.get(
"device_challenges"
)
if not any(
x["device_class"] in (DeviceClasses.WEBAUTHN) for x in device_challenges
):
raise ValidationError("Got webauthn but no compatible device class allowed")
self._challenge_allowed([DeviceClasses.WEBAUTHN])
return validate_challenge_webauthn(
webauthn, self.stage.request, self.stage.get_pending_user()
)
def validate_duo(self, duo: int) -> int:
"""Initiate Duo authentication"""
self._challenge_allowed([DeviceClasses.DUO])
return validate_challenge_duo(
duo, self.stage.request, self.stage.get_pending_user()
)
def validate(self, data: dict):
# Checking if the given data is from a valid device class is done above
# Here we only check if the any data was sent at all
if "code" not in data and "webauthn" not in data:
if "code" not in data and "webauthn" not in data and "duo" not in data:
raise ValidationError("Empty response")
return data
@ -80,7 +85,7 @@ class AuthenticatorChallengeResponse(ChallengeResponse):
class AuthenticatorValidateStageView(ChallengeStageView):
"""Authenticator Validation"""
response_class = AuthenticatorChallengeResponse
response_class = AuthenticatorValidationChallengeResponse
def get_device_challenges(self) -> list[dict]:
"""Get a list of all device challenges applicable for the current stage"""
@ -141,19 +146,18 @@ class AuthenticatorValidateStageView(ChallengeStageView):
return self.executor.stage_ok()
return super().get(request, *args, **kwargs)
def get_challenge(self) -> AuthenticatorChallenge:
def get_challenge(self) -> AuthenticatorValidationChallenge:
challenges = self.request.session["device_challenges"]
return AuthenticatorChallenge(
return AuthenticatorValidationChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-authenticator-validate",
"device_challenges": challenges,
}
)
# pylint: disable=unused-argument
def challenge_valid(
self, challenge: AuthenticatorChallengeResponse
self, challenge: AuthenticatorValidationChallengeResponse
) -> HttpResponse:
# All validation is done by the serializer
return self.executor.stage_ok()

View File

@ -2,7 +2,7 @@
from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
from rest_framework.fields import JSONField
from rest_framework.fields import CharField, JSONField
from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger
from webauthn.webauthn import (
@ -13,7 +13,12 @@ from webauthn.webauthn import (
)
from authentik.core.models import User
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.challenge import (
Challenge,
ChallengeResponse,
ChallengeTypes,
WithUserInfoChallenge,
)
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
@ -32,16 +37,18 @@ SESSION_KEY_WEBAUTHN_AUTHENTICATED = (
)
class AuthenticatorWebAuthnChallenge(Challenge):
class AuthenticatorWebAuthnChallenge(WithUserInfoChallenge):
"""WebAuthn Challenge"""
registration = JSONField()
component = CharField(default="ak-stage-authenticator-webauthn")
class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
"""WebAuthn Challenge response"""
response = JSONField()
component = CharField(default="ak-stage-authenticator-webauthn")
request: HttpRequest
user: User
@ -129,7 +136,6 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
return AuthenticatorWebAuthnChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-authenticator-webauthn",
"registration": registration_dict,
}
)

View File

@ -21,12 +21,14 @@ class CaptchaChallenge(WithUserInfoChallenge):
"""Site public key"""
site_key = CharField()
component = CharField(default="ak-stage-captcha")
class CaptchaChallengeResponse(ChallengeResponse):
"""Validate captcha token"""
token = CharField()
component = CharField(default="ak-stage-captcha")
def validate_token(self, token: str) -> str:
"""Validate captcha token"""
@ -64,7 +66,6 @@ class CaptchaStageView(ChallengeStageView):
return CaptchaChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-captcha",
"site_key": self.executor.current_stage.public_key,
}
)

View File

@ -4,6 +4,7 @@ from django.urls import reverse
from django.utils.encoding import force_str
from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import FlowPlan
@ -54,5 +55,9 @@ class TestCaptchaStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)

View File

@ -25,11 +25,14 @@ class ConsentChallenge(WithUserInfoChallenge):
header_text = CharField()
permissions = PermissionSerializer(many=True)
component = CharField(default="ak-stage-consent")
class ConsentChallengeResponse(ChallengeResponse):
"""Consent challenge response, any valid response request is valid"""
component = CharField(default="ak-stage-consent")
class ConsentStageView(ChallengeStageView):
"""Simple consent checker."""
@ -37,24 +40,19 @@ class ConsentStageView(ChallengeStageView):
response_class = ConsentChallengeResponse
def get_challenge(self) -> Challenge:
challenge = ConsentChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-consent",
}
)
data = {
"type": ChallengeTypes.NATIVE.value,
"permissions": self.executor.plan.context.get(
PLAN_CONTEXT_CONSENT_PERMISSIONS, []
),
}
if PLAN_CONTEXT_CONSENT_TITLE in self.executor.plan.context:
challenge.initial_data["title"] = self.executor.plan.context[
PLAN_CONTEXT_CONSENT_TITLE
]
data["title"] = self.executor.plan.context[PLAN_CONTEXT_CONSENT_TITLE]
if PLAN_CONTEXT_CONSENT_HEADER in self.executor.plan.context:
challenge.initial_data["header_text"] = self.executor.plan.context[
data["header_text"] = self.executor.plan.context[
PLAN_CONTEXT_CONSENT_HEADER
]
if PLAN_CONTEXT_CONSENT_PERMISSIONS in self.executor.plan.context:
challenge.initial_data["permissions"] = self.executor.plan.context[
PLAN_CONTEXT_CONSENT_PERMISSIONS
]
challenge = ConsentChallenge(data=data)
return challenge
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:

View File

@ -7,6 +7,7 @@ from django.utils.encoding import force_str
from authentik.core.models import Application, User
from authentik.core.tasks import clean_expired_models
from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlan
@ -51,7 +52,11 @@ class TestConsentStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
self.assertFalse(UserConsent.objects.filter(user=self.user).exists())
@ -82,7 +87,11 @@ class TestConsentStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
self.assertTrue(
UserConsent.objects.filter(
@ -119,7 +128,11 @@ class TestConsentStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
self.assertTrue(
UserConsent.objects.filter(

View File

@ -1,5 +1,6 @@
"""authentik multi-stage authentication engine"""
from django.http.response import HttpResponse
from rest_framework.fields import CharField
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.stage import ChallengeStageView
@ -8,10 +9,14 @@ from authentik.flows.stage import ChallengeStageView
class DummyChallenge(Challenge):
"""Dummy challenge"""
component = CharField(default="ak-stage-dummy")
class DummyChallengeResponse(ChallengeResponse):
"""Dummy challenge response"""
component = CharField(default="ak-stage-dummy")
class DummyStageView(ChallengeStageView):
"""Dummy stage for testing with multiple stages"""
@ -25,7 +30,6 @@ class DummyStageView(ChallengeStageView):
return DummyChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-dummy",
"title": self.executor.current_stage.name,
}
)

View File

@ -4,6 +4,7 @@ from django.urls import reverse
from django.utils.encoding import force_str
from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.stages.dummy.models import DummyStage
@ -45,5 +46,9 @@ class TestDummyStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)

View File

@ -8,6 +8,7 @@ from django.urls import reverse
from django.utils.http import urlencode
from django.utils.timezone import now
from django.utils.translation import gettext as _
from rest_framework.fields import CharField
from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger
@ -28,11 +29,15 @@ PLAN_CONTEXT_EMAIL_SENT = "email_sent"
class EmailChallenge(Challenge):
"""Email challenge"""
component = CharField(default="ak-stage-email")
class EmailChallengeResponse(ChallengeResponse):
"""Email challenge resposen. No fields. This challenge is
always declared invalid to give the user a chance to retry"""
component = CharField(default="ak-stage-email")
def validate(self, data):
raise ValidationError("")
@ -97,7 +102,6 @@ class EmailStageView(ChallengeStageView):
challenge = EmailChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-email",
"title": "Email sent.",
}
)

View File

@ -8,6 +8,7 @@ from django.utils.encoding import force_str
from django.utils.http import urlencode
from authentik.core.models import Token, User
from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
@ -133,7 +134,11 @@ class TestEmailStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
session = self.client.session

View File

@ -36,11 +36,15 @@ class IdentificationChallenge(Challenge):
primary_action = CharField()
sources = UILoginButtonSerializer(many=True, required=False)
component = CharField(default="ak-stage-identification")
class IdentificationChallengeResponse(ChallengeResponse):
"""Identification challenge"""
uid_field = CharField()
component = CharField(default="ak-stage-identification")
pre_user: Optional[User] = None
def validate_uid_field(self, value: str) -> str:
@ -81,8 +85,8 @@ class IdentificationStageView(ChallengeStageView):
challenge = IdentificationChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-identification",
"primary_action": _("Log in"),
"component": "ak-stage-identification",
"user_fields": current_stage.user_fields,
}
)

View File

@ -53,7 +53,11 @@ class TestIdentificationStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
def test_invalid_with_username(self):
@ -118,8 +122,9 @@ class TestIdentificationStage(TestCase):
"icon_url": "/static/authentik/sources/.svg",
"name": "test",
"challenge": {
"component": "xak-flow-redirect",
"to": "/source/oauth/login/test/",
"type": "redirect",
"type": ChallengeTypes.REDIRECT.value,
},
}
],
@ -162,8 +167,9 @@ class TestIdentificationStage(TestCase):
"sources": [
{
"challenge": {
"component": "xak-flow-redirect",
"to": "/source/oauth/login/test/",
"type": "redirect",
"type": ChallengeTypes.REDIRECT.value,
},
"icon_url": "/static/authentik/sources/.svg",
"name": "test",

View File

@ -89,7 +89,11 @@ class TestUserLoginStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
self.stage.continue_flow_without_invitation = False
@ -123,7 +127,11 @@ class TestUserLoginStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
def test_with_invitation_prompt_data(self):
@ -154,7 +162,11 @@ class TestUserLoginStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
self.assertFalse(Invitation.objects.filter(pk=invite.pk))

View File

@ -63,12 +63,16 @@ class PasswordChallenge(WithUserInfoChallenge):
recovery_url = CharField(required=False)
component = CharField(default="ak-stage-password")
class PasswordChallengeResponse(ChallengeResponse):
"""Password challenge response"""
password = CharField()
component = CharField(default="ak-stage-password")
class PasswordStageView(ChallengeStageView):
"""Authentication stage which authenticates against django's AuthBackend"""
@ -79,7 +83,6 @@ class PasswordStageView(ChallengeStageView):
challenge = PasswordChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-password",
}
)
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)

View File

@ -118,7 +118,11 @@ class TestPasswordStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
def test_invalid_password(self):

View File

@ -26,31 +26,38 @@ LOGGER = get_logger()
PLAN_CONTEXT_PROMPT = "prompt_data"
class PromptSerializer(PassiveSerializer):
class StagePromptSerializer(PassiveSerializer):
"""Serializer for a single Prompt field"""
field_key = CharField()
label = CharField(allow_blank=True)
type = CharField()
required = BooleanField()
placeholder = CharField()
placeholder = CharField(allow_blank=True)
order = IntegerField()
class PromptChallenge(Challenge):
"""Initial challenge being sent, define fields"""
fields = PromptSerializer(many=True)
fields = StagePromptSerializer(many=True)
component = CharField(default="ak-stage-prompt")
class PromptResponseChallenge(ChallengeResponse):
class PromptChallengeResponse(ChallengeResponse):
"""Validate response, fields are dynamically created based
on the stage"""
def __init__(self, *args, stage: PromptStage, plan: FlowPlan, **kwargs):
component = CharField(default="ak-stage-prompt")
def __init__(self, *args, **kwargs):
stage: PromptStage = kwargs.pop("stage", None)
plan: FlowPlan = kwargs.pop("plan", None)
super().__init__(*args, **kwargs)
self.stage = stage
self.plan = plan
if not self.stage:
return
# list() is called so we only load the fields once
fields = list(self.stage.fields.all())
for field in fields:
@ -152,15 +159,14 @@ class ListPolicyEngine(PolicyEngine):
class PromptStageView(ChallengeStageView):
"""Prompt Stage, save form data in plan context."""
response_class = PromptResponseChallenge
response_class = PromptChallengeResponse
def get_challenge(self, *args, **kwargs) -> Challenge:
fields = list(self.executor.current_stage.fields.all().order_by("order"))
challenge = PromptChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-prompt",
"fields": [PromptSerializer(field).data for field in fields],
"fields": [StagePromptSerializer(field).data for field in fields],
},
)
return challenge
@ -168,7 +174,7 @@ class PromptStageView(ChallengeStageView):
def get_response_instance(self, data: QueryDict) -> ChallengeResponse:
if not self.executor.plan:
raise ValueError
return PromptResponseChallenge(
return PromptChallengeResponse(
instance=None,
data=data,
stage=self.executor.current_stage,

View File

@ -6,13 +6,14 @@ from django.urls import reverse
from django.utils.encoding import force_str
from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.policies.expression.models import ExpressionPolicy
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT, PromptResponseChallenge
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT, PromptChallengeResponse
class TestPromptStage(TestCase):
@ -111,7 +112,7 @@ class TestPromptStage(TestCase):
self.assertIn(prompt.label, force_str(response.content))
self.assertIn(prompt.placeholder, force_str(response.content))
def test_valid_challenge_with_policy(self) -> PromptResponseChallenge:
def test_valid_challenge_with_policy(self) -> PromptChallengeResponse:
"""Test challenge_response validation"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
@ -122,13 +123,13 @@ class TestPromptStage(TestCase):
)
self.stage.validation_policies.set([expr_policy])
self.stage.save()
challenge_response = PromptResponseChallenge(
challenge_response = PromptChallengeResponse(
None, stage=self.stage, plan=plan, data=self.prompt_data
)
self.assertEqual(challenge_response.is_valid(), True)
return challenge_response
def test_invalid_challenge(self) -> PromptResponseChallenge:
def test_invalid_challenge(self) -> PromptChallengeResponse:
"""Test challenge_response validation"""
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
@ -139,7 +140,7 @@ class TestPromptStage(TestCase):
)
self.stage.validation_policies.set([expr_policy])
self.stage.save()
challenge_response = PromptResponseChallenge(
challenge_response = PromptChallengeResponse(
None, stage=self.stage, plan=plan, data=self.prompt_data
)
self.assertEqual(challenge_response.is_valid(), False)
@ -167,7 +168,11 @@ class TestPromptStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
# Check that valid data has been saved

View File

@ -75,7 +75,11 @@ class TestUserDeleteStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
self.assertFalse(User.objects.filter(username=self.username).exists())

View File

@ -49,7 +49,11 @@ class TestUserLoginStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
def test_expiry(self):
@ -70,7 +74,11 @@ class TestUserLoginStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
self.assertNotEqual(list(self.client.session.keys()), [])
sleep(3)

View File

@ -4,6 +4,7 @@ from django.urls import reverse
from django.utils.encoding import force_str
from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes
from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
@ -48,5 +49,9 @@ class TestUserLogoutStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)

View File

@ -2,6 +2,7 @@
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.backends import ModelBackend
from django.db.utils import IntegrityError
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from structlog.stdlib import get_logger
@ -84,7 +85,11 @@ class UserWriteStageView(StageView):
PLAN_CONTEXT_SOURCES_CONNECTION
]
user.attributes[USER_ATTRIBUTE_SOURCES].append(connection.source.name)
user.save()
try:
user.save()
except IntegrityError as exc:
LOGGER.warning("Failed to save user", exc=exc)
self.executor.stage_invalid()
user_write.send(
sender=self, request=request, user=user, data=data, created=user_created
)

View File

@ -60,7 +60,11 @@ class TestUserWriteStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
user_qs = User.objects.filter(
username=plan.context[PLAN_CONTEXT_PROMPT]["username"]
@ -97,7 +101,11 @@ class TestUserWriteStage(TestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"to": reverse("authentik_core:root-redirect"), "type": "redirect"},
{
"component": "xak-flow-redirect",
"to": reverse("authentik_core:root-redirect"),
"type": ChallengeTypes.REDIRECT.value,
},
)
user_qs = User.objects.filter(
username=plan.context[PLAN_CONTEXT_PROMPT]["username"]

View File

@ -8,6 +8,7 @@ import (
"net/http"
"net/http/cookiejar"
"net/url"
"strconv"
"strings"
"time"
@ -52,8 +53,6 @@ func (pi *ProviderInstance) Bind(username string, bindDN, bindPW string, conn ne
// Create new http client that also sets the correct ip
config := api.NewConfiguration()
// Carry over the bearer authentication, so that failed login attempts are attributed to the outpost
config.DefaultHeader = pi.s.ac.Client.GetConfig().DefaultHeader
config.Host = pi.s.ac.Client.GetConfig().Host
config.Scheme = pi.s.ac.Client.GetConfig().Scheme
config.HTTPClient = &http.Client{
@ -75,7 +74,7 @@ func (pi *ProviderInstance) Bind(username string, bindDN, bindPW string, conn ne
if !passed {
return ldap.LDAPResultInvalidCredentials, nil
}
r, err := pi.s.ac.Client.CoreApi.CoreApplicationsCheckAccessRetrieve(context.Background(), pi.appSlug).Execute()
r, err := apiClient.CoreApi.CoreApplicationsCheckAccessRetrieve(context.Background(), pi.appSlug).Execute()
if r.StatusCode == 403 {
pi.log.WithField("bindDN", bindDN).Info("Access denied for user")
return ldap.LDAPResultInsufficientAccessRights, nil
@ -86,7 +85,7 @@ func (pi *ProviderInstance) Bind(username string, bindDN, bindPW string, conn ne
}
pi.log.WithField("bindDN", bindDN).Info("User has access")
// Get user info to store in context
userInfo, _, err := pi.s.ac.Client.CoreApi.CoreUsersMeRetrieve(context.Background()).Execute()
userInfo, _, err := apiClient.CoreApi.CoreUsersMeRetrieve(context.Background()).Execute()
if err != nil {
pi.log.WithField("bindDN", bindDN).WithError(err).Warning("failed to get user info")
return ldap.LDAPResultOperationsError, nil
@ -133,48 +132,69 @@ func (pi *ProviderInstance) delayDeleteUserInfo(dn string) {
}()
}
type ChallengeInt interface {
GetComponent() string
GetType() api.ChallengeChoices
GetResponseErrors() map[string][]api.ErrorDetail
}
func (pi *ProviderInstance) solveFlowChallenge(bindDN string, password string, client *api.APIClient, urlParams string, depth int) (bool, error) {
req := client.FlowsApi.FlowsExecutorGet(context.Background(), pi.flowSlug)
req.Query(urlParams)
req := client.FlowsApi.FlowsExecutorGet(context.Background(), pi.flowSlug).Query(urlParams)
challenge, _, err := req.Execute()
if err != nil {
pi.log.WithError(err).Warning("Failed to get challenge")
return false, err
}
pi.log.WithField("component", challenge.Component).WithField("type", challenge.Type).Debug("Got challenge")
responseReq := client.FlowsApi.FlowsExecutorSolve(context.Background(), pi.flowSlug)
responseReq.Query(urlParams)
switch *challenge.Component {
ch := challenge.GetActualInstance().(ChallengeInt)
pi.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got challenge")
responseReq := client.FlowsApi.FlowsExecutorSolve(context.Background(), pi.flowSlug).Query(urlParams)
switch ch.GetComponent() {
case "ak-stage-identification":
responseReq.RequestBody(map[string]interface{}{
"uid_field": bindDN,
})
responseReq = responseReq.FlowChallengeResponseRequest(api.IdentificationChallengeResponseRequestAsFlowChallengeResponseRequest(api.NewIdentificationChallengeResponseRequest(bindDN)))
case "ak-stage-password":
responseReq.RequestBody(map[string]interface{}{
"password": password,
})
responseReq = responseReq.FlowChallengeResponseRequest(api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(api.NewPasswordChallengeResponseRequest(password)))
case "ak-stage-authenticator-validate":
// We only support duo as authenticator, check if that's allowed
var deviceChallenge *api.DeviceChallenge
for _, devCh := range challenge.AuthenticatorValidationChallenge.DeviceChallenges {
if devCh.DeviceClass == string(api.DEVICECLASSESENUM_DUO) {
deviceChallenge = &devCh
}
}
if deviceChallenge == nil {
return false, errors.New("got ak-stage-authenticator-validate without duo")
}
devId, err := strconv.Atoi(deviceChallenge.DeviceUid)
if err != nil {
return false, errors.New("failed to convert duo device id to int")
}
devId32 := int32(devId)
inner := api.NewAuthenticatorValidationChallengeResponseRequest()
inner.Duo = &devId32
responseReq = responseReq.FlowChallengeResponseRequest(api.AuthenticatorValidationChallengeResponseRequestAsFlowChallengeResponseRequest(inner))
case "ak-stage-access-denied":
return false, errors.New("got ak-stage-access-denied")
default:
return false, fmt.Errorf("unsupported challenge type: %s", *challenge.Component)
return false, fmt.Errorf("unsupported challenge type: %s", ch.GetComponent())
}
response, _, err := responseReq.Execute()
pi.log.WithField("component", response.Component).WithField("type", response.Type).Debug("Got response")
switch *response.Component {
ch = response.GetActualInstance().(ChallengeInt)
pi.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got response")
switch ch.GetComponent() {
case "ak-stage-access-denied":
return false, errors.New("got ak-stage-access-denied")
}
if response.Type == "redirect" {
if ch.GetType() == "redirect" {
return true, nil
}
if err != nil {
pi.log.WithError(err).Warning("Failed to submit challenge")
return false, err
}
if len(*response.ResponseErrors) > 0 {
for key, errs := range *response.ResponseErrors {
if len(ch.GetResponseErrors()) > 0 {
for key, errs := range ch.GetResponseErrors() {
for _, err := range errs {
pi.log.WithField("key", key).WithField("code", err.Code).Debug(err.String)
pi.log.WithField("key", key).WithField("code", err.Code).WithField("msg", err.String).Warning("Flow error")
return false, nil
}
}

View File

@ -8,8 +8,15 @@ import (
"strings"
"github.com/nmcclain/ldap"
"goauthentik.io/outpost/api"
)
func (pi *ProviderInstance) SearchMe(user api.User, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {
entries := make([]*ldap.Entry, 1)
entries[0] = pi.UserEntry(user)
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
}
func (pi *ProviderInstance) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {
bindDN = strings.ToLower(bindDN)
baseDN := strings.ToLower("," + pi.BaseDN)
@ -29,14 +36,13 @@ func (pi *ProviderInstance) Search(bindDN string, searchReq ldap.SearchRequest,
pi.boundUsersMutex.RLock()
defer pi.boundUsersMutex.RUnlock()
flags, ok := pi.boundUsers[bindDN]
pi.log.WithField("bindDN", bindDN).WithField("ok", ok).Debugf("%+v\n", flags)
if !ok {
pi.log.Debug("User info not cached")
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
}
if !flags.CanSearch {
pi.log.Debug("User can't search")
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
pi.log.Debug("User can't search, showing info about user")
return pi.SearchMe(flags.UserInfo, searchReq, conn)
}
switch filterEntity {
@ -49,24 +55,7 @@ func (pi *ProviderInstance) Search(bindDN string, searchReq ldap.SearchRequest,
}
pi.log.WithField("count", len(groups.Results)).Trace("Got results from API")
for _, g := range groups.Results {
attrs := []*ldap.EntryAttribute{
{
Name: "cn",
Values: []string{g.Name},
},
{
Name: "uid",
Values: []string{string(g.Pk)},
},
{
Name: "objectClass",
Values: []string{GroupObjectClass, "goauthentik.io/ldap/group"},
},
}
attrs = append(attrs, AKAttrsToLDAP(g.Attributes)...)
dn := pi.GetGroupDN(g)
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
entries = append(entries, pi.GroupEntry(g))
}
case UserObjectClass, "":
users, _, err := pi.s.ac.Client.CoreApi.CoreUsersList(context.Background()).Execute()
@ -74,53 +63,79 @@ func (pi *ProviderInstance) Search(bindDN string, searchReq ldap.SearchRequest,
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err)
}
for _, u := range users.Results {
attrs := []*ldap.EntryAttribute{
{
Name: "cn",
Values: []string{u.Username},
},
{
Name: "uid",
Values: []string{u.Uid},
},
{
Name: "name",
Values: []string{u.Name},
},
{
Name: "displayName",
Values: []string{u.Name},
},
{
Name: "mail",
Values: []string{*u.Email},
},
{
Name: "objectClass",
Values: []string{UserObjectClass, "organizationalPerson", "goauthentik.io/ldap/user"},
},
}
if *u.IsActive {
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"inactive"}})
} else {
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"active"}})
}
if u.IsSuperuser {
attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"inactive"}})
} else {
attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"active"}})
}
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: pi.GroupsForUser(u)})
attrs = append(attrs, AKAttrsToLDAP(u.Attributes)...)
dn := fmt.Sprintf("cn=%s,%s", u.Username, pi.UserDN)
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
entries = append(entries, pi.UserEntry(u))
}
}
pi.log.WithField("filter", searchReq.Filter).Debug("Search OK")
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
}
func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
attrs := []*ldap.EntryAttribute{
{
Name: "cn",
Values: []string{u.Username},
},
{
Name: "uid",
Values: []string{u.Uid},
},
{
Name: "name",
Values: []string{u.Name},
},
{
Name: "displayName",
Values: []string{u.Name},
},
{
Name: "mail",
Values: []string{*u.Email},
},
{
Name: "objectClass",
Values: []string{UserObjectClass, "organizationalPerson", "goauthentik.io/ldap/user"},
},
}
if *u.IsActive {
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"inactive"}})
} else {
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"active"}})
}
if u.IsSuperuser {
attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"inactive"}})
} else {
attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"active"}})
}
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: pi.GroupsForUser(u)})
attrs = append(attrs, AKAttrsToLDAP(u.Attributes)...)
dn := fmt.Sprintf("cn=%s,%s", u.Username, pi.UserDN)
return &ldap.Entry{DN: dn, Attributes: attrs}
}
func (pi *ProviderInstance) GroupEntry(g api.Group) *ldap.Entry {
attrs := []*ldap.EntryAttribute{
{
Name: "cn",
Values: []string{g.Name},
},
{
Name: "uid",
Values: []string{string(g.Pk)},
},
{
Name: "objectClass",
Values: []string{GroupObjectClass, "goauthentik.io/ldap/group"},
},
}
attrs = append(attrs, AKAttrsToLDAP(g.Attributes)...)
dn := pi.GetGroupDN(g)
return &ldap.Entry{DN: dn, Attributes: attrs}
}

View File

@ -9,7 +9,8 @@ import (
func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute {
attrList := []*ldap.EntryAttribute{}
for attrKey, attrValue := range attrs.(map[string]interface{}) {
a := attrs.(*map[string]interface{})
for attrKey, attrValue := range *a {
entry := &ldap.EntryAttribute{Name: attrKey}
switch t := attrValue.(type) {
case []string:

1450
schema.yml

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
def test_totp_validate(self):
"""test flow with otp stages"""
sleep(1)
@ -65,6 +66,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_stages_authenticator_totp", "0006_default_setup_flow")
def test_totp_setup(self):
"""test TOTP Setup stage"""
@ -115,6 +117,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_stages_authenticator_static", "0005_default_setup_flow")
def test_static_setup(self):
"""test Static OTP Setup stage"""

View File

@ -40,6 +40,7 @@ class TestFlowsEnroll(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
def test_enroll_2_step(self):
"""Test 2-step enroll flow"""
# First stage fields
@ -77,6 +78,7 @@ class TestFlowsEnroll(SeleniumTestCase):
flow = Flow.objects.create(
name="default-enrollment-flow",
slug="default-enrollment-flow",
title="default-enrollment-flow",
designation=FlowDesignation.ENROLLMENT,
)
@ -108,6 +110,7 @@ class TestFlowsEnroll(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend")
def test_enroll_email(self):
"""Test enroll with Email verification"""
@ -152,6 +155,7 @@ class TestFlowsEnroll(SeleniumTestCase):
flow = Flow.objects.create(
name="default-enrollment-flow",
slug="default-enrollment-flow",
title="default-enrollment-flow",
designation=FlowDesignation.ENROLLMENT,
)

View File

@ -12,6 +12,7 @@ class TestFlowsLogin(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
def test_login(self):
"""test default login flow"""
self.driver.get(

View File

@ -19,6 +19,7 @@ class TestFlowsStageSetup(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_stages_password", "0002_passwordstage_change_flow")
def test_password_change(self):
"""test password change flow"""

View File

@ -63,6 +63,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
def test_authorization_consent_implied(self):
@ -117,6 +118,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
def test_authorization_consent_explicit(self):
@ -194,6 +196,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
def test_denied(self):

View File

@ -83,6 +83,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
def test_redirect_uri_error(self):
@ -124,6 +125,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@ -186,6 +188,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@ -256,6 +259,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@ -337,6 +341,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
def test_authorization_denied(self):

View File

@ -78,6 +78,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
def test_redirect_uri_error(self):
@ -119,6 +120,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@ -168,6 +170,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@ -234,6 +237,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
def test_authorization_denied(self):

View File

@ -78,6 +78,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
def test_redirect_uri_error(self):
@ -119,6 +120,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@ -165,6 +167,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@ -228,6 +231,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
def test_authorization_denied(self):

View File

@ -60,6 +60,7 @@ class TestProviderProxy(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@ -112,6 +113,7 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager

View File

@ -74,6 +74,7 @@ class TestProviderSAML(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@ -138,6 +139,7 @@ class TestProviderSAML(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@ -219,6 +221,7 @@ class TestProviderSAML(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@ -289,6 +292,7 @@ class TestProviderSAML(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0010_provider_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager

View File

@ -131,6 +131,7 @@ class TestSourceOAuth2(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@ -188,6 +189,7 @@ class TestSourceOAuth2(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@ -228,6 +230,7 @@ class TestSourceOAuth2(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@object_manager
@ -318,6 +321,7 @@ class TestSourceOAuth1(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@patch(

View File

@ -98,6 +98,7 @@ class TestSourceSAML(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_migration(
@ -166,6 +167,7 @@ class TestSourceSAML(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_migration(
@ -247,6 +249,7 @@ class TestSourceSAML(SeleniumTestCase):
@retry()
@apply_migration("authentik_core", "0003_default_user")
@apply_migration("authentik_flows", "0008_default_flows")
@apply_migration("authentik_flows", "0011_flow_title")
@apply_migration("authentik_flows", "0009_source_flows")
@apply_migration("authentik_crypto", "0002_create_self_signed_kp")
@apply_migration(

View File

@ -136,13 +136,13 @@ class SeleniumTestCase(StaticLiveServerTestCase):
)
identification_stage.find_element(
By.CSS_SELECTOR, "input[name=uid_field]"
By.CSS_SELECTOR, "input[name=uidField]"
).click()
identification_stage.find_element(
By.CSS_SELECTOR, "input[name=uid_field]"
By.CSS_SELECTOR, "input[name=uidField]"
).send_keys(USER().username)
identification_stage.find_element(
By.CSS_SELECTOR, "input[name=uid_field]"
By.CSS_SELECTOR, "input[name=uidField]"
).send_keys(Keys.ENTER)
flow_executor = self.get_shadow_root("ak-flow-executor")

View File

@ -8,23 +8,3 @@ export interface Error {
export interface ErrorDict {
[key: string]: Error[];
}
export interface Challenge {
type: ChallengeChoices;
component?: string;
title?: string;
response_errors?: ErrorDict;
}
export interface WithUserInfoChallenge extends Challenge {
pending_user: string;
pending_user_avatar: string;
}
export interface ShellChallenge extends Challenge {
body: string;
}
export interface RedirectChallenge extends Challenge {
to: string;
}

View File

@ -13,6 +13,7 @@ import { unsafeHTML } from "lit-html/directives/unsafe-html";
import "./access_denied/FlowAccessDenied";
import "./stages/authenticator_static/AuthenticatorStaticStage";
import "./stages/authenticator_totp/AuthenticatorTOTPStage";
import "./stages/authenticator_duo/AuthenticatorDuoStage";
import "./stages/authenticator_validate/AuthenticatorValidateStage";
import "./stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage";
import "./stages/autosubmit/AutosubmitStage";
@ -24,28 +25,14 @@ import "./stages/identification/IdentificationStage";
import "./stages/password/PasswordStage";
import "./stages/prompt/PromptStage";
import "./sources/plex/PlexLoginInit";
import { ShellChallenge, RedirectChallenge } from "../api/Flows";
import { IdentificationChallenge } from "./stages/identification/IdentificationStage";
import { PasswordChallenge } from "./stages/password/PasswordStage";
import { ConsentChallenge } from "./stages/consent/ConsentStage";
import { EmailChallenge } from "./stages/email/EmailStage";
import { AutosubmitChallenge } from "./stages/autosubmit/AutosubmitStage";
import { PromptChallenge } from "./stages/prompt/PromptStage";
import { AuthenticatorTOTPChallenge } from "./stages/authenticator_totp/AuthenticatorTOTPStage";
import { AuthenticatorStaticChallenge } from "./stages/authenticator_static/AuthenticatorStaticStage";
import { AuthenticatorValidateStageChallenge } from "./stages/authenticator_validate/AuthenticatorValidateStage";
import { WebAuthnAuthenticatorRegisterChallenge } from "./stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage";
import { CaptchaChallenge } from "./stages/captcha/CaptchaStage";
import { StageHost } from "./stages/base";
import { Challenge, ChallengeChoices, Config, FlowsApi } from "authentik-api";
import { ChallengeChoices, Config, FlowChallengeRequest, FlowChallengeResponseRequest, FlowsApi, RedirectChallenge, ShellChallenge } from "authentik-api";
import { config, DEFAULT_CONFIG } from "../api/Config";
import { ifDefined } from "lit-html/directives/if-defined";
import { until } from "lit-html/directives/until";
import { AccessDeniedChallenge } from "./access_denied/FlowAccessDenied";
import { PFSize } from "../elements/Spinner";
import { TITLE_DEFAULT } from "../constants";
import { configureSentry } from "../api/Sentry";
import { PlexAuthenticationChallenge } from "./sources/plex/PlexLoginInit";
@customElement("ak-flow-executor")
export class FlowExecutor extends LitElement implements StageHost {
@ -53,7 +40,7 @@ export class FlowExecutor extends LitElement implements StageHost {
flowSlug: string;
@property({attribute: false})
challenge?: Challenge;
challenge?: FlowChallengeRequest;
@property({type: Boolean})
loading = false;
@ -88,9 +75,6 @@ export class FlowExecutor extends LitElement implements StageHost {
constructor() {
super();
this.addEventListener("ak-flow-submit", () => {
this.submit();
});
this.flowSlug = window.location.pathname.split("/")[3];
}
@ -110,19 +94,21 @@ export class FlowExecutor extends LitElement implements StageHost {
});
}
submit<T>(formData?: T): Promise<void> {
submit(payload?: FlowChallengeResponseRequest): Promise<void> {
if (!payload) return Promise.reject();
if (!this.challenge) return Promise.reject();
// @ts-ignore
payload.component = this.challenge.component;
this.loading = true;
return new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolveRaw({
return new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
flowSlug: this.flowSlug,
requestBody: formData || {},
query: window.location.search.substring(1),
}).then((challengeRaw) => {
return challengeRaw.raw.json();
flowChallengeResponseRequest: payload,
}).then((data) => {
this.challenge = data;
this.postUpdate();
}).catch((e: Response) => {
this.errorMessage(e.statusText);
}).catch((e: Error) => {
this.errorMessage(e.toString());
}).finally(() => {
this.loading = false;
});
@ -133,28 +119,26 @@ export class FlowExecutor extends LitElement implements StageHost {
this.config = config;
});
this.loading = true;
new FlowsApi(DEFAULT_CONFIG).flowsExecutorGetRaw({
new FlowsApi(DEFAULT_CONFIG).flowsExecutorGet({
flowSlug: this.flowSlug,
query: window.location.search.substring(1),
}).then((challengeRaw) => {
return challengeRaw.raw.json();
}).then((challenge) => {
this.challenge = challenge as Challenge;
this.challenge = challenge;
// Only set background on first update, flow won't change throughout execution
if (this.challenge?.background) {
this.setBackground(this.challenge.background);
}
this.postUpdate();
}).catch((e: Response) => {
}).catch((e: Error) => {
// Catch JSON or Update errors
this.errorMessage(e.statusText);
this.errorMessage(e.toString());
}).finally(() => {
this.loading = false;
});
}
errorMessage(error: string): void {
this.challenge = <ShellChallenge>{
this.challenge = {
type: ChallengeChoices.Shell,
body: `<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
@ -174,7 +158,7 @@ export class FlowExecutor extends LitElement implements StageHost {
</li>
</ul>
</footer>`
};
} as FlowChallengeRequest;
}
renderLoading(): TemplateResult {
@ -200,33 +184,35 @@ export class FlowExecutor extends LitElement implements StageHost {
case ChallengeChoices.Native:
switch (this.challenge.component) {
case "ak-stage-access-denied":
return html`<ak-stage-access-denied .host=${this} .challenge=${this.challenge as AccessDeniedChallenge}></ak-stage-access-denied>`;
return html`<ak-stage-access-denied .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-access-denied>`;
case "ak-stage-identification":
return html`<ak-stage-identification .host=${this} .challenge=${this.challenge as IdentificationChallenge}></ak-stage-identification>`;
return html`<ak-stage-identification .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-identification>`;
case "ak-stage-password":
return html`<ak-stage-password .host=${this} .challenge=${this.challenge as PasswordChallenge}></ak-stage-password>`;
return html`<ak-stage-password .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-password>`;
case "ak-stage-captcha":
return html`<ak-stage-captcha .host=${this} .challenge=${this.challenge as CaptchaChallenge}></ak-stage-captcha>`;
return html`<ak-stage-captcha .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-captcha>`;
case "ak-stage-consent":
return html`<ak-stage-consent .host=${this} .challenge=${this.challenge as ConsentChallenge}></ak-stage-consent>`;
return html`<ak-stage-consent .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-consent>`;
case "ak-stage-dummy":
return html`<ak-stage-dummy .host=${this} .challenge=${this.challenge as Challenge}></ak-stage-dummy>`;
return html`<ak-stage-dummy .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-dummy>`;
case "ak-stage-email":
return html`<ak-stage-email .host=${this} .challenge=${this.challenge as EmailChallenge}></ak-stage-email>`;
return html`<ak-stage-email .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-email>`;
case "ak-stage-autosubmit":
return html`<ak-stage-autosubmit .host=${this} .challenge=${this.challenge as AutosubmitChallenge}></ak-stage-autosubmit>`;
return html`<ak-stage-autosubmit .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-autosubmit>`;
case "ak-stage-prompt":
return html`<ak-stage-prompt .host=${this} .challenge=${this.challenge as PromptChallenge}></ak-stage-prompt>`;
return html`<ak-stage-prompt .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-prompt>`;
case "ak-stage-authenticator-totp":
return html`<ak-stage-authenticator-totp .host=${this} .challenge=${this.challenge as AuthenticatorTOTPChallenge}></ak-stage-authenticator-totp>`;
return html`<ak-stage-authenticator-totp .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-totp>`;
case "ak-stage-authenticator-duo":
return html`<ak-stage-authenticator-duo .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-duo>`;
case "ak-stage-authenticator-static":
return html`<ak-stage-authenticator-static .host=${this} .challenge=${this.challenge as AuthenticatorStaticChallenge}></ak-stage-authenticator-static>`;
return html`<ak-stage-authenticator-static .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-static>`;
case "ak-stage-authenticator-webauthn":
return html`<ak-stage-authenticator-webauthn .host=${this} .challenge=${this.challenge as WebAuthnAuthenticatorRegisterChallenge}></ak-stage-authenticator-webauthn>`;
return html`<ak-stage-authenticator-webauthn .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-webauthn>`;
case "ak-stage-authenticator-validate":
return html`<ak-stage-authenticator-validate .host=${this} .challenge=${this.challenge as AuthenticatorValidateStageChallenge}></ak-stage-authenticator-validate>`;
return html`<ak-stage-authenticator-validate .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-validate>`;
case "ak-flow-sources-plex":
return html`<ak-flow-sources-plex .host=${this} .challenge=${this.challenge as PlexAuthenticationChallenge}></ak-flow-sources-plex>`;
return html`<ak-flow-sources-plex .host=${this as StageHost} .challenge=${this.challenge}></ak-flow-sources-plex>`;
default:
break;
}
@ -284,8 +270,7 @@ export class FlowExecutor extends LitElement implements StageHost {
</li>`;
}))}
${this.config?.brandingTitle != "authentik" ? html`
<li><a href="https://goauthentik.io">${t`Powered by authentik`}</a></li>
` : html``}
<li><a href="https://goauthentik.io">${t`Powered by authentik`}</a></li>` : html``}
</ul>
</footer>
</div>

View File

@ -1,5 +1,5 @@
import { Challenge } from "authentik-api";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { AccessDeniedChallenge, FlowChallengeResponseRequest } from "authentik-api";
import { CSSResult, customElement, html, TemplateResult } from "lit-element";
import { BaseStage } from "../stages/base";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
@ -12,15 +12,8 @@ import { t } from "@lingui/macro";
import "../../elements/EmptyState";
export interface AccessDeniedChallenge extends Challenge {
error_message?: string;
}
@customElement("ak-stage-access-denied")
export class FlowAccessDenied extends BaseStage {
@property({ attribute: false })
challenge?: AccessDeniedChallenge;
export class FlowAccessDenied extends BaseStage<AccessDeniedChallenge, FlowChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFList, PFFormControl, PFTitle, AKGlobal];
@ -45,9 +38,9 @@ export class FlowAccessDenied extends BaseStage {
<i class="pf-icon pf-icon-error-circle-o"></i>
${t`Request has been denied.`}
</p>
${this.challenge?.error_message &&
${this.challenge?.errorMessage &&
html`<hr>
<p>${this.challenge.error_message}</p>`}
<p>${this.challenge.errorMessage}</p>`}
</div>
</form>
</div>

View File

@ -1,5 +1,5 @@
import { t } from "@lingui/macro";
import { Challenge } from "authentik-api";
import { PlexAuthenticationChallenge } from "authentik-api";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -7,7 +7,7 @@ import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import AKGlobal from "../../../authentik.css";
import { CSSResult, customElement, property } from "lit-element";
import { CSSResult, customElement } from "lit-element";
import { html, TemplateResult } from "lit-html";
import { BaseStage } from "../../stages/base";
import { PlexAPIClient, popupCenterScreen } from "./API";
@ -15,28 +15,20 @@ import { DEFAULT_CONFIG } from "../../../api/Config";
import { SourcesApi } from "authentik-api";
import { showMessage } from "../../../elements/messages/MessageContainer";
import { MessageLevel } from "../../../elements/messages/Message";
import { PlexAuthenticationChallengeResponseRequest } from "authentik-api/dist/models/PlexAuthenticationChallengeResponseRequest";
export interface PlexAuthenticationChallenge extends Challenge {
client_id: string;
slug: string;
}
@customElement("ak-flow-sources-plex")
export class PlexLoginInit extends BaseStage {
@property({ attribute: false })
challenge?: PlexAuthenticationChallenge;
export class PlexLoginInit extends BaseStage<PlexAuthenticationChallenge, PlexAuthenticationChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
}
async firstUpdated(): Promise<void> {
const authInfo = await PlexAPIClient.getPin(this.challenge?.client_id || "");
const authInfo = await PlexAPIClient.getPin(this.challenge?.clientId || "");
const authWindow = popupCenterScreen(authInfo.authUrl, "plex auth", 550, 700);
PlexAPIClient.pinPoll(this.challenge?.client_id || "", authInfo.pin.id).then(token => {
PlexAPIClient.pinPoll(this.challenge?.clientId || "", authInfo.pin.id).then(token => {
authWindow?.close();
new SourcesApi(DEFAULT_CONFIG).sourcesPlexRedeemTokenCreate({
plexTokenRedeemRequest: {

View File

@ -0,0 +1,87 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, TemplateResult } from "lit-element";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import AKGlobal from "../../../authentik.css";
import { BaseStage } from "../base";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { FlowURLManager } from "../../../api/legacy";
import { AuthenticatorDuoChallenge, StagesApi } from "authentik-api";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { AuthenticatorDuoChallengeResponseRequest } from "authentik-api/dist/models/AuthenticatorDuoChallengeResponseRequest";
@customElement("ak-stage-authenticator-duo")
export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge, AuthenticatorDuoChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
firstUpdated(): void {
const i = setInterval(() => {
this.checkEnrollStatus().then(() => {
clearInterval(i);
});
}, 3000);
}
checkEnrollStatus(): Promise<void> {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoEnrollmentStatusCreate({
stageUuid: this.challenge?.stageUuid || "",
}).then(() => {
this.host?.submit({});
}).catch(() => {
console.debug("authentik/flows/duo: Waiting for auth status");
});
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.title}
</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
<div slot="link">
<a href="${FlowURLManager.cancel()}">${t`Not you?`}</a>
</div>
</ak-form-static>
<img src=${this.challenge.activationBarcode} />
<p>
${t`Alternatively, if your current device has Duo installed, click on this link:`}
</p>
<a href=${this.challenge.activationCode}>${t`Duo activation`}</a>
<div class="pf-c-form__group pf-m-action">
<button type="button" class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
this.checkEnrollStatus();
}}>
${t`Check status`}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
</footer>`;
}
}

View File

@ -1,6 +1,5 @@
import { t } from "@lingui/macro";
import { css, CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { WithUserInfoChallenge } from "../../../api/Flows";
import { css, CSSResult, customElement, html, TemplateResult } from "lit-element";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -13,6 +12,8 @@ import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { FlowURLManager } from "../../../api/legacy";
import { AuthenticatorStaticChallenge } from "authentik-api";
import { AuthenticatorStaticChallengeResponseRequest } from "authentik-api/dist/models/AuthenticatorStaticChallengeResponseRequest";
export const STATIC_TOKEN_STYLE = css`
/* Static OTP Tokens */
@ -29,15 +30,9 @@ export const STATIC_TOKEN_STYLE = css`
}
`;
export interface AuthenticatorStaticChallenge extends WithUserInfoChallenge {
codes: number[];
}
@customElement("ak-stage-authenticator-static")
export class AuthenticatorStaticStage extends BaseStage {
@property({ attribute: false })
challenge?: AuthenticatorStaticChallenge;
export class AuthenticatorStaticStage extends BaseStage<AuthenticatorStaticChallenge, AuthenticatorStaticChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal, STATIC_TOKEN_STYLE];
@ -59,8 +54,8 @@ export class AuthenticatorStaticStage extends BaseStage {
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pending_user_avatar}"
user=${this.challenge.pending_user}>
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
<div slot="link">
<a href="${FlowURLManager.cancel()}">${t`Not you?`}</a>
</div>

View File

@ -1,6 +1,5 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { WithUserInfoChallenge } from "../../../api/Flows";
import { CSSResult, customElement, html, TemplateResult } from "lit-element";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -16,16 +15,11 @@ import "../../../elements/EmptyState";
import "../../FormStatic";
import { MessageLevel } from "../../../elements/messages/Message";
import { FlowURLManager } from "../../../api/legacy";
import { AuthenticatorTOTPChallenge, AuthenticatorTOTPChallengeResponseRequest } from "authentik-api";
export interface AuthenticatorTOTPChallenge extends WithUserInfoChallenge {
config_url: string;
}
@customElement("ak-stage-authenticator-totp")
export class AuthenticatorTOTPStage extends BaseStage {
@property({ attribute: false })
challenge?: AuthenticatorTOTPChallenge;
export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge, AuthenticatorTOTPChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
@ -47,20 +41,20 @@ export class AuthenticatorTOTPStage extends BaseStage {
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pending_user_avatar}"
user=${this.challenge.pending_user}>
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
<div slot="link">
<a href="${FlowURLManager.cancel()}">${t`Not you?`}</a>
</div>
</ak-form-static>
<input type="hidden" name="otp_uri" value=${this.challenge.config_url} />
<input type="hidden" name="otp_uri" value=${this.challenge.configUrl} />
<ak-form-element>
<!-- @ts-ignore -->
<qr-code data="${this.challenge.config_url}"></qr-code>
<qr-code data="${this.challenge.configUrl}"></qr-code>
<button type="button" class="pf-c-button pf-m-secondary pf-m-progress pf-m-in-progress" @click=${(e: Event) => {
e.preventDefault();
if (!this.challenge?.config_url) return;
navigator.clipboard.writeText(this.challenge?.config_url).then(() => {
if (!this.challenge?.configUrl) return;
navigator.clipboard.writeText(this.challenge?.configUrl).then(() => {
showMessage({
level: MessageLevel.success,
message: t`Successfully copied TOTP Config.`
@ -75,7 +69,7 @@ export class AuthenticatorTOTPStage extends BaseStage {
label="${t`Code`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.response_errors || {})["code"]}>
.errors=${(this.challenge?.responseErrors || {})["code"]}>
<!-- @ts-ignore -->
<input type="text"
name="code"

View File

@ -1,6 +1,5 @@
import { t } from "@lingui/macro";
import { css, CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { WithUserInfoChallenge } from "../../../api/Flows";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -11,40 +10,26 @@ import AKGlobal from "../../../authentik.css";
import { BaseStage, StageHost } from "../base";
import "./AuthenticatorValidateStageWebAuthn";
import "./AuthenticatorValidateStageCode";
import "./AuthenticatorValidateStageDuo";
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
import { AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
import { AuthenticatorValidationChallenge } from "authentik-api/dist/models/AuthenticatorValidationChallenge";
export enum DeviceClasses {
STATIC = "static",
TOTP = "totp",
WEBAUTHN = "webauthn",
}
export interface DeviceChallenge {
device_class: DeviceClasses;
device_uid: string;
challenge: unknown;
}
export interface AuthenticatorValidateStageChallenge extends WithUserInfoChallenge {
device_challenges: DeviceChallenge[];
}
export interface AuthenticatorValidateStageChallengeResponse {
code: string;
webauthn: string;
DUO = "duo",
}
@customElement("ak-stage-authenticator-validate")
export class AuthenticatorValidateStage extends BaseStage implements StageHost {
@property({ attribute: false })
challenge?: AuthenticatorValidateStageChallenge;
export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> implements StageHost {
@property({attribute: false})
selectedDeviceChallenge?: DeviceChallenge;
submit<T>(formData?: T): Promise<void> {
return this.host?.submit<T>(formData) || Promise.resolve();
submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<void> {
return this.host?.submit(payload) || Promise.resolve();
}
static get styles(): CSSResult[] {
@ -76,7 +61,13 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
}
renderDevicePickerSingle(deviceChallenge: DeviceChallenge): TemplateResult {
switch (deviceChallenge.device_class) {
switch (deviceChallenge.deviceClass) {
case DeviceClasses.DUO:
return html`<i class="fas fa-mobile-alt"></i>
<div class="right">
<p>${t`Duo push-notifications`}</p>
<small>${t`Receive a push notification on your phone to prove your identity.`}</small>
</div>`;
case DeviceClasses.WEBAUTHN:
return html`<i class="fas fa-mobile-alt"></i>
<div class="right">
@ -115,7 +106,7 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
renderDevicePicker(): TemplateResult {
return html`
<ul>
${this.challenge?.device_challenges.map((challenges) => {
${this.challenge?.deviceChallenges.map((challenges) => {
return html`<li>
<button class="pf-c-button authenticator-button" type="button" @click=${() => {
this.selectedDeviceChallenge = challenges;
@ -131,23 +122,31 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
if (!this.selectedDeviceChallenge) {
return html``;
}
switch (this.selectedDeviceChallenge?.device_class) {
switch (this.selectedDeviceChallenge?.deviceClass) {
case DeviceClasses.STATIC:
case DeviceClasses.TOTP:
return html`<ak-stage-authenticator-validate-code
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.device_challenges.length || []) > 1}>
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
</ak-stage-authenticator-validate-code>`;
case DeviceClasses.WEBAUTHN:
return html`<ak-stage-authenticator-validate-webauthn
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.device_challenges.length || []) > 1}>
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
</ak-stage-authenticator-validate-webauthn>`;
case DeviceClasses.DUO:
return html`<ak-stage-authenticator-validate-duo
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
</ak-stage-authenticator-validate-duo>`;
}
return html``;
}
render(): TemplateResult {
@ -158,8 +157,8 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
</ak-empty-state>`;
}
// User only has a single device class, so we don't show a picker
if (this.challenge?.device_challenges.length === 1) {
this.selectedDeviceChallenge = this.challenge.device_challenges[0];
if (this.challenge?.deviceChallenges.length === 1) {
this.selectedDeviceChallenge = this.challenge.deviceChallenges[0];
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">

View File

@ -8,18 +8,17 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import AKGlobal from "../../../authentik.css";
import { BaseStage } from "../base";
import { AuthenticatorValidateStage, AuthenticatorValidateStageChallenge, DeviceChallenge } from "./AuthenticatorValidateStage";
import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
import "../../FormStatic";
import { FlowURLManager } from "../../../api/legacy";
import { AuthenticatorValidationChallenge } from "authentik-api/dist/models/AuthenticatorValidationChallenge";
import { AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
@customElement("ak-stage-authenticator-validate-code")
export class AuthenticatorValidateStageWebCode extends BaseStage {
@property({ attribute: false })
challenge?: AuthenticatorValidateStageChallenge;
export class AuthenticatorValidateStageWebCode extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
@property({ attribute: false })
deviceChallenge?: DeviceChallenge;
@ -42,8 +41,8 @@ export class AuthenticatorValidateStageWebCode extends BaseStage {
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pending_user_avatar}"
user=${this.challenge.pending_user}>
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
<div slot="link">
<a href="${FlowURLManager.cancel()}">${t`Not you?`}</a>
</div>
@ -52,7 +51,7 @@ export class AuthenticatorValidateStageWebCode extends BaseStage {
label="${t`Code`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.response_errors || {})["code"]}>
.errors=${(this.challenge?.responseErrors || {})["code"]}>
<!-- @ts-ignore -->
<input type="text"
name="code"

View File

@ -0,0 +1,79 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import AKGlobal from "../../../authentik.css";
import { BaseStage } from "../base";
import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { FlowURLManager } from "../../../api/legacy";
import { AuthenticatorValidationChallenge } from "authentik-api/dist/models/AuthenticatorValidationChallenge";
import { AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
@customElement("ak-stage-authenticator-validate-duo")
export class AuthenticatorValidateStageWebDuo extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
@property({ attribute: false })
deviceChallenge?: DeviceChallenge;
@property({ type: Boolean })
showBackButton = false;
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
firstUpdated(): void {
this.host?.submit({
"duo": this.deviceChallenge?.deviceUid
});
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
}
return html`<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
<div slot="link">
<a href="${FlowURLManager.cancel()}">${t`Not you?`}</a>
</div>
</ak-form-static>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
${this.showBackButton ?
html`<li class="pf-c-login__main-footer-links-item">
<button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
if (!this.host) return;
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
}}>
${t`Return to device picker`}
</button>
</li>`:
html``}
</ul>
</footer>`;
}
}

View File

@ -10,13 +10,12 @@ import AKGlobal from "../../../authentik.css";
import { PFSize } from "../../../elements/Spinner";
import { transformAssertionForServer, transformCredentialRequestOptions } from "../authenticator_webauthn/utils";
import { BaseStage } from "../base";
import { AuthenticatorValidateStage, AuthenticatorValidateStageChallenge, DeviceChallenge } from "./AuthenticatorValidateStage";
import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage";
import { AuthenticatorValidationChallenge } from "authentik-api/dist/models/AuthenticatorValidationChallenge";
import { AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
@customElement("ak-stage-authenticator-validate-webauthn")
export class AuthenticatorValidateStageWebAuthn extends BaseStage {
@property({attribute: false})
challenge?: AuthenticatorValidateStageChallenge;
export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
@property({attribute: false})
deviceChallenge?: DeviceChallenge;
@ -60,9 +59,9 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage {
// post the assertion to the server for verification.
try {
const formData = new FormData();
formData.set("webauthn", JSON.stringify(transformedAssertionForServer));
await this.host?.submit(formData);
await this.host?.submit({
webauthn: transformedAssertionForServer
});
} catch (err) {
throw new Error(t`Error when validating assertion on server: ${err}`);
}

View File

@ -1,6 +1,5 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { WithUserInfoChallenge } from "../../../api/Flows";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -11,20 +10,14 @@ import AKGlobal from "../../../authentik.css";
import { PFSize } from "../../../elements/Spinner";
import { BaseStage } from "../base";
import { Assertion, transformCredentialCreateOptions, transformNewAssertionForServer } from "./utils";
export interface WebAuthnAuthenticatorRegisterChallenge extends WithUserInfoChallenge {
registration: PublicKeyCredentialCreationOptions;
}
import { AuthenticatorWebAuthnChallenge, AuthenticatorWebAuthnChallengeResponseRequest } from "authentik-api";
export interface WebAuthnAuthenticatorRegisterChallengeResponse {
response: Assertion;
}
@customElement("ak-stage-authenticator-webauthn")
export class WebAuthnAuthenticatorRegisterStage extends BaseStage {
@property({ attribute: false })
challenge?: WebAuthnAuthenticatorRegisterChallenge;
export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorWebAuthnChallenge, AuthenticatorWebAuthnChallengeResponseRequest> {
@property({type: Boolean})
registerRunning = false;
@ -42,7 +35,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage {
}
// convert certain members of the PublicKeyCredentialCreateOptions into
// byte arrays as expected by the spec.
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(this.challenge?.registration);
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(this.challenge?.registration as PublicKeyCredentialCreationOptions);
// request the authenticator(s) to create a new credential keypair.
let credential;
@ -106,8 +99,8 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage {
</div>`:
html`
<div class="pf-c-form__group pf-m-action">
${this.challenge?.response_errors ?
html`<p class="pf-m-block">${this.challenge.response_errors["response"][0].string}</p>`:
${this.challenge?.responseErrors ?
html`<p class="pf-m-block">${this.challenge.responseErrors["response"][0].string}</p>`:
html``}
<p class="pf-m-block">${this.registerMessage}</p>
<button class="pf-c-button pf-m-primary pf-m-block" @click=${() => {

View File

@ -1,6 +1,5 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { WithUserInfoChallenge } from "../../../api/Flows";
import { CSSResult, customElement, html, TemplateResult } from "lit-element";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -10,17 +9,11 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import AKGlobal from "../../../authentik.css";
import { BaseStage } from "../base";
import "../../../elements/EmptyState";
export interface AutosubmitChallenge extends WithUserInfoChallenge {
url: string;
attrs: { [key: string]: string };
}
import { AutosubmitChallenge } from "authentik-api";
import { AutoSubmitChallengeResponseRequest } from "authentik-api/dist/models/AutoSubmitChallengeResponseRequest";
@customElement("ak-stage-autosubmit")
export class AutosubmitStage extends BaseStage {
@property({ attribute: false })
challenge?: AutosubmitChallenge;
export class AutosubmitStage extends BaseStage<AutosubmitChallenge, AutoSubmitChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
@ -45,7 +38,7 @@ export class AutosubmitStage extends BaseStage {
<div class="pf-c-login__main-body">
<form class="pf-c-form" action="${this.challenge.url}" method="POST">
${Object.entries(this.challenge.attrs).map(([ key, value ]) => {
return html`<input type="hidden" name="${key}" value="${value}">`;
return html`<input type="hidden" name="${key as string}" value="${value as string}">`;
})}
<ak-empty-state
?loading="${true}">

View File

@ -1,14 +1,16 @@
import { Challenge } from "authentik-api";
import { LitElement } from "lit-element";
import { LitElement, property } from "lit-element";
export interface StageHost {
challenge?: Challenge;
submit<T>(formData?: T): Promise<void>;
challenge?: unknown;
submit(payload: unknown): Promise<void>;
}
export class BaseStage extends LitElement {
export class BaseStage<Tin, Tout> extends LitElement {
host?: StageHost;
host!: StageHost;
@property({ attribute: false })
challenge!: Tin;
submitForm(e: Event): void {
e.preventDefault();
@ -17,7 +19,7 @@ export class BaseStage extends LitElement {
} = {};
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
form.forEach((value, key) => object[key] = value);
this.host?.submit(object);
this.host?.submit(object as unknown as Tout);
}
}

View File

@ -1,6 +1,5 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { WithUserInfoChallenge } from "../../../api/Flows";
import { CSSResult, customElement, html, TemplateResult } from "lit-element";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -14,27 +13,15 @@ import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { FlowURLManager } from "../../../api/legacy";
export interface CaptchaChallenge extends WithUserInfoChallenge {
site_key: string;
}
import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "authentik-api";
@customElement("ak-stage-captcha")
export class CaptchaStage extends BaseStage {
@property({ attribute: false })
challenge?: CaptchaChallenge;
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
submitFormAlt(token: string): void {
const form = new FormData();
form.set("token", token);
this.host?.submit(form);
}
firstUpdated(): void {
const script = document.createElement("script");
script.src = "https://www.google.com/recaptcha/api.js";
@ -45,12 +32,14 @@ export class CaptchaStage extends BaseStage {
script.onload = () => {
console.debug("authentik/stages/captcha: script loaded");
grecaptcha.ready(() => {
if (!this.challenge?.site_key) return;
if (!this.challenge?.siteKey) return;
console.debug("authentik/stages/captcha: ready");
const captchaId = grecaptcha.render(captchaContainer, {
sitekey: this.challenge.site_key,
sitekey: this.challenge.siteKey,
callback: (token) => {
this.submitFormAlt(token);
this.host?.submit({
"token": token,
});
},
size: "invisible",
});
@ -76,8 +65,8 @@ export class CaptchaStage extends BaseStage {
<form class="pf-c-form">
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pending_user_avatar}"
user=${this.challenge.pending_user}>
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
<div slot="link">
<a href="${FlowURLManager.cancel()}">${t`Not you?`}</a>
</div>

View File

@ -1,6 +1,5 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { WithUserInfoChallenge } from "../../../api/Flows";
import { CSSResult, customElement, html, TemplateResult } from "lit-element";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -12,24 +11,11 @@ import { BaseStage } from "../base";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { FlowURLManager } from "../../../api/legacy";
import { ConsentChallenge, ConsentChallengeResponseRequest } from "authentik-api";
export interface Permission {
name: string;
id: string;
}
export interface ConsentChallenge extends WithUserInfoChallenge {
header_text: string;
permissions?: Permission[];
}
@customElement("ak-stage-consent")
export class ConsentStage extends BaseStage {
@property({ attribute: false })
challenge?: ConsentChallenge;
export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
@ -51,15 +37,15 @@ export class ConsentStage extends BaseStage {
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pending_user_avatar}"
user=${this.challenge.pending_user}>
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
<div slot="link">
<a href="${FlowURLManager.cancel()}">${t`Not you?`}</a>
</div>
</ak-form-static>
<div class="pf-c-form__group">
<p id="header-text">
${this.challenge.header_text}
${this.challenge.headerText}
</p>
<p>${t`Application requires following permissions`}</p>
<ul class="pf-c-list" id="permmissions">

View File

@ -1,6 +1,5 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { Challenge } from "../../../api/Flows";
import { CSSResult, customElement, html, TemplateResult } from "lit-element";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -11,12 +10,10 @@ import AKGlobal from "../../../authentik.css";
import { BaseStage } from "../base";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { DummyChallenge, DummyChallengeResponseRequest } from "authentik-api";
@customElement("ak-stage-dummy")
export class DummyStage extends BaseStage {
@property({ attribute: false })
challenge?: Challenge;
export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];

View File

@ -1,6 +1,5 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { Challenge } from "authentik-api";
import { CSSResult, customElement, html, TemplateResult } from "lit-element";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -10,14 +9,10 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import AKGlobal from "../../../authentik.css";
import { BaseStage } from "../base";
import "../../../elements/EmptyState";
export type EmailChallenge = Challenge;
import { EmailChallenge, EmailChallengeResponseRequest } from "authentik-api";
@customElement("ak-stage-email")
export class EmailStage extends BaseStage {
@property({ attribute: false })
challenge?: EmailChallenge;
export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];

View File

@ -1,5 +1,5 @@
import { t } from "@lingui/macro";
import { css, CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { css, CSSResult, customElement, html, TemplateResult } from "lit-element";
import { BaseStage } from "../base";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
@ -10,7 +10,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import AKGlobal from "../../../authentik.css";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import { Challenge } from "../../../api/Flows";
import { FlowChallengeRequest, IdentificationChallenge, IdentificationChallengeResponseRequest, UILoginButton } from "authentik-api";
export const PasswordManagerPrefill: {
password: string | undefined;
@ -20,30 +20,9 @@ export const PasswordManagerPrefill: {
totp: undefined,
};
export interface IdentificationChallenge extends Challenge {
user_fields?: string[];
primary_action: string;
sources?: UILoginButton[];
application_pre?: string;
enroll_url?: string;
recovery_url?: string;
}
export interface UILoginButton {
name: string;
challenge: Challenge;
icon_url?: string;
}
@customElement("ak-stage-identification")
export class IdentificationStage extends BaseStage {
@property({attribute: false})
challenge?: IdentificationChallenge;
export class IdentificationStage extends BaseStage<IdentificationChallenge, IdentificationChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal].concat(
@ -75,7 +54,7 @@ export class IdentificationStage extends BaseStage {
username.setAttribute("autocomplete", "username");
username.onkeyup = (ev: Event) => {
const el = ev.target as HTMLInputElement;
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uid_field]").forEach(input => {
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
input.value = el.value;
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
@ -98,7 +77,7 @@ export class IdentificationStage extends BaseStage {
PasswordManagerPrefill.password = el.value;
// Because password managers fill username, then password,
// we need to re-focus the uid_field here too
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uid_field]").forEach(input => {
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
@ -120,7 +99,7 @@ export class IdentificationStage extends BaseStage {
PasswordManagerPrefill.totp = el.value;
// Because totp managers fill username, then password, then optionally,
// we need to re-focus the uid_field here too
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uid_field]").forEach(input => {
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
@ -131,13 +110,13 @@ export class IdentificationStage extends BaseStage {
renderSource(source: UILoginButton): TemplateResult {
let icon = html`<i class="fas fas fa-share-square" title="${source.name}"></i>`;
if (source.icon_url) {
icon = html`<img src="${source.icon_url}" alt="${source.name}">`;
if (source.iconUrl) {
icon = html`<img src="${source.iconUrl}" alt="${source.name}">`;
}
return html`<li class="pf-c-login__main-footer-links-item">
<button type="button" @click=${() => {
if (!this.host) return;
this.host.challenge = source.challenge;
this.host.challenge = source.challenge as FlowChallengeRequest;
}}>
${icon}
</button>
@ -145,18 +124,18 @@ export class IdentificationStage extends BaseStage {
}
renderFooter(): TemplateResult {
if (!this.challenge?.enroll_url && !this.challenge?.recovery_url) {
if (!this.challenge?.enrollUrl && !this.challenge?.recoveryUrl) {
return html``;
}
return html`<div class="pf-c-login__main-footer-band">
${this.challenge.enroll_url ? html`
${this.challenge.enrollUrl ? html`
<p class="pf-c-login__main-footer-band-item">
${t`Need an account?`}
<a id="enroll" href="${this.challenge.enroll_url}">${t`Sign up.`}</a>
<a id="enroll" href="${this.challenge.enrollUrl}">${t`Sign up.`}</a>
</p>` : html``}
${this.challenge.recovery_url ? html`
${this.challenge.recoveryUrl ? html`
<p class="pf-c-login__main-footer-band-item">
<a id="recovery" href="${this.challenge.recovery_url}">${t`Forgot username or password?`}</a>
<a id="recovery" href="${this.challenge.recoveryUrl}">${t`Forgot username or password?`}</a>
</p>` : html``}
</div>`;
}
@ -164,15 +143,15 @@ export class IdentificationStage extends BaseStage {
renderInput(): TemplateResult {
let label = "";
let type = "text";
if (!this.challenge?.user_fields) {
if (!this.challenge?.userFields) {
return html`<p>
${t`Select one of the sources below to login.`}
</p>`;
}
if (this.challenge?.user_fields === ["email"]) {
if (this.challenge?.userFields === ["email"]) {
label = t`Email`;
type = "email";
} else if (this.challenge?.user_fields === ["username"]) {
} else if (this.challenge?.userFields === ["username"]) {
label = t`Username`;
} else {
label = t`Email or username`;
@ -181,10 +160,10 @@ export class IdentificationStage extends BaseStage {
label=${label}
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.response_errors || {})["uid_field"]}>
.errors=${(this.challenge?.responseErrors || {})["uid_field"]}>
<!-- @ts-ignore -->
<input type=${type}
name="uid_field"
name="uidField"
placeholder="Email or Username"
autofocus=""
autocomplete="username"
@ -193,7 +172,7 @@ export class IdentificationStage extends BaseStage {
</ak-form-element>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${this.challenge.primary_action}
${this.challenge.primaryAction}
</button>
</div>`;
}
@ -212,9 +191,9 @@ export class IdentificationStage extends BaseStage {
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
${this.challenge.application_pre ?
${this.challenge.applicationPre ?
html`<p>
${t`Login to continue to ${this.challenge.application_pre}.`}
${t`Login to continue to ${this.challenge.applicationPre}.`}
</p>`:
html``}
${this.renderInput()}

View File

@ -1,6 +1,5 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { WithUserInfoChallenge } from "../../../api/Flows";
import { CSSResult, customElement, html, TemplateResult } from "lit-element";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -14,16 +13,10 @@ import "../../../elements/EmptyState";
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
import "../../FormStatic";
import { FlowURLManager } from "../../../api/legacy";
export interface PasswordChallenge extends WithUserInfoChallenge {
recovery_url?: string;
}
import { PasswordChallenge, PasswordChallengeResponseRequest } from "authentik-api";
@customElement("ak-stage-password")
export class PasswordStage extends BaseStage {
@property({attribute: false})
challenge?: PasswordChallenge;
export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
@ -45,18 +38,18 @@ export class PasswordStage extends BaseStage {
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pending_user_avatar}"
user=${this.challenge.pending_user}>
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
<div slot="link">
<a href="${FlowURLManager.cancel()}">${t`Not you?`}</a>
</div>
</ak-form-static>
<input name="username" autocomplete="username" type="hidden" value="${this.challenge.pending_user}">
<input name="username" autocomplete="username" type="hidden" value="${this.challenge.pendingUser}">
<ak-form-element
label="${t`Password`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.response_errors || {})["password"]}>
.errors=${(this.challenge?.responseErrors || {})["password"]}>
<input type="password"
name="password"
placeholder="${t`Please enter your password`}"
@ -67,8 +60,8 @@ export class PasswordStage extends BaseStage {
value=${PasswordManagerPrefill.password || ""}>
</ak-form-element>
${this.challenge.recovery_url ?
html`<a href="${this.challenge.recovery_url}">
${this.challenge.recoveryUrl ?
html`<a href="${this.challenge.recoveryUrl}">
${t`Forgot password?`}</a>` : ""}
<div class="pf-c-form__group pf-m-action">

View File

@ -1,5 +1,5 @@
import { t } from "@lingui/macro";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { CSSResult, customElement, html, TemplateResult } from "lit-element";
import { unsafeHTML } from "lit-html/directives/unsafe-html";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
@ -13,37 +13,23 @@ import { BaseStage } from "../base";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import "../../../elements/Divider";
import { Challenge, Error } from "../../../api/Flows";
import { Error } from "../../../api/Flows";
import { PromptChallenge, PromptChallengeResponseRequest, StagePrompt } from "authentik-api";
export interface Prompt {
field_key: string;
label: string;
type: string;
required: boolean;
placeholder: string;
order: number;
}
export interface PromptChallenge extends Challenge {
fields: Prompt[];
}
@customElement("ak-stage-prompt")
export class PromptStage extends BaseStage {
@property({attribute: false})
challenge?: PromptChallenge;
export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFAlert, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
renderPromptInner(prompt: Prompt): string {
renderPromptInner(prompt: StagePrompt): string {
switch (prompt.type) {
case "text":
return `<input
type="text"
name="${prompt.field_key}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
autocomplete="off"
class="pf-c-form-control"
@ -52,7 +38,7 @@ export class PromptStage extends BaseStage {
case "username":
return `<input
type="text"
name="${prompt.field_key}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
autocomplete="username"
class="pf-c-form-control"
@ -61,7 +47,7 @@ export class PromptStage extends BaseStage {
case "email":
return `<input
type="email"
name="${prompt.field_key}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
?required=${prompt.required}
@ -69,7 +55,7 @@ export class PromptStage extends BaseStage {
case "password":
return `<input
type="password"
name="${prompt.field_key}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
autocomplete="new-password"
class="pf-c-form-control"
@ -77,28 +63,28 @@ export class PromptStage extends BaseStage {
case "number":
return `<input
type="number"
name="${prompt.field_key}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
?required=${prompt.required}>`;
case "checkbox":
return `<input
type="checkbox"
name="${prompt.field_key}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
?required=${prompt.required}>`;
case "date":
return `<input
type="date"
name="${prompt.field_key}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
?required=${prompt.required}>`;
case "date-time":
return `<input
type="datetime"
name="${prompt.field_key}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
?required=${prompt.required}>`;
@ -107,7 +93,7 @@ export class PromptStage extends BaseStage {
case "hidden":
return `<input
type="hidden"
name="${prompt.field_key}"
name="${prompt.fieldKey}"
value="${prompt.placeholder}"
class="pf-c-form-control"
?required=${prompt.required}>`;
@ -122,16 +108,16 @@ export class PromptStage extends BaseStage {
return html``;
}
return html`<div class="pf-c-form__alert">
${errors.map(err => {
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
</div>
<h4 class="pf-c-alert__title">
${err.string}
</h4>
</div>`;
})}
${errors.map(err => {
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
</div>
<h4 class="pf-c-alert__title">
${err.string}
</h4>
</div>`;
})}
</div>`;
}
@ -158,12 +144,12 @@ export class PromptStage extends BaseStage {
label="${prompt.label}"
?required="${prompt.required}"
class="pf-c-form__group"
.errors=${(this.challenge?.response_errors || {})[prompt.field_key]}>
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}>
${unsafeHTML(this.renderPromptInner(prompt))}
</ak-form-element>`;
})}
${"non_field_errors" in (this.challenge?.response_errors || {}) ?
this.renderNonFieldErrors(this.challenge?.response_errors?.non_field_errors || []):
${"non_field_errors" in (this.challenge?.responseErrors || {}) ?
this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || []):
html``}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">

View File

@ -63,6 +63,10 @@ msgstr "ANY, any policy must match to grant access."
msgid "ANY, any policy must match to include this stage access."
msgstr "ANY, any policy must match to include this stage access."
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
msgid "API Hostname"
msgstr "API Hostname"
#: src/elements/notifications/APIDrawer.ts
msgid "API Requests"
msgstr "API Requests"
@ -180,6 +184,10 @@ msgstr "Allows/denys requests based on the users and/or the IPs reputation."
msgid "Also known as Entity ID. Defaults the Metadata URL."
msgstr "Also known as Entity ID. Defaults the Metadata URL."
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
msgid "Alternatively, if your current device has Duo installed, click on this link:"
msgstr "Alternatively, if your current device has Duo installed, click on this link:"
#: src/pages/stages/consent/ConsentStageForm.ts
msgid "Always require consent"
msgstr "Always require consent"
@ -522,6 +530,10 @@ msgstr "Check IP"
msgid "Check Username"
msgstr "Check Username"
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
msgid "Check status"
msgstr "Check status"
#: src/flows/stages/email/EmailStage.ts
msgid "Check your Emails for a password reset link."
msgstr "Check your Emails for a password reset link."
@ -616,6 +628,7 @@ msgstr "Confidential clients are capable of maintaining the confidentiality of t
msgid "Configuration"
msgstr "Configuration"
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
#: src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
@ -715,6 +728,7 @@ msgstr "Context"
#: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts
#: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStageDuo.ts
#: src/flows/stages/autosubmit/AutosubmitStage.ts
#: src/flows/stages/consent/ConsentStage.ts
#: src/flows/stages/dummy/DummyStage.ts
@ -1010,6 +1024,10 @@ msgstr "Determines how authentik sends the response back to the Service Provider
msgid "Determines how long a session lasts. Default of 0 seconds means that the sessions lasts until the browser is closed."
msgstr "Determines how long a session lasts. Default of 0 seconds means that the sessions lasts until the browser is closed."
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
msgid "Device classes"
msgstr "Device classes"
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
msgid "Device classes which can be used to authenticate."
msgstr "Device classes which can be used to authenticate."
@ -1032,6 +1050,7 @@ msgstr "Digits"
msgid "Disable"
msgstr "Disable"
#: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorStatic.ts
msgid "Disable Static Tokens"
msgstr "Disable Static Tokens"
@ -1070,6 +1089,22 @@ msgstr "Download Private key"
msgid "Dummy stage used for testing. Shows a simple continue button and always passes."
msgstr "Dummy stage used for testing. Shows a simple continue button and always passes."
#: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts
msgid "Duo"
msgstr "Duo"
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
msgid "Duo Authenticators"
msgstr "Duo Authenticators"
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
msgid "Duo activation"
msgstr "Duo activation"
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts
msgid "Duo push-notifications"
msgstr "Duo push-notifications"
#: src/pages/providers/oauth2/OAuth2ProviderForm.ts
msgid "Each provider has a different issuer, based on the application slug."
msgstr "Each provider has a different issuer, based on the application slug."
@ -1159,6 +1194,7 @@ msgstr "Enable"
msgid "Enable StartTLS"
msgstr "Enable StartTLS"
#: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorStatic.ts
msgid "Enable Static Tokens"
msgstr "Enable Static Tokens"
@ -1431,6 +1467,7 @@ msgstr "Flow used before authentication."
msgid "Flow used by an authenticated user to configure their password. If empty, user will not be able to configure change their password."
msgstr "Flow used by an authenticated user to configure their password. If empty, user will not be able to configure change their password."
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
#: src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
msgid "Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage."
@ -1664,6 +1701,10 @@ msgstr "Include User claims from scopes in the id_token, for applications that d
msgid "Include claims in id_token"
msgstr "Include claims in id_token"
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
msgid "Integration key"
msgstr "Integration key"
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
msgid "Internal Host"
msgstr "Internal Host"
@ -1803,10 +1844,12 @@ msgstr "Load servers"
#: src/flows/FlowExecutor.ts
#: src/flows/FlowExecutor.ts
#: src/flows/access_denied/FlowAccessDenied.ts
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts
#: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStageDuo.ts
#: src/flows/stages/autosubmit/AutosubmitStage.ts
#: src/flows/stages/captcha/CaptchaStage.ts
#: src/flows/stages/consent/ConsentStage.ts
@ -1866,6 +1909,7 @@ msgstr "Loading"
#: src/pages/sources/saml/SAMLSourceForm.ts
#: src/pages/sources/saml/SAMLSourceForm.ts
#: src/pages/sources/saml/SAMLSourceForm.ts
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
#: src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
@ -2041,6 +2085,7 @@ msgstr "My Applications"
#: src/pages/sources/saml/SAMLSourceForm.ts
#: src/pages/sources/saml/SAMLSourceViewPage.ts
#: src/pages/stages/StageListPage.ts
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
#: src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
@ -2173,9 +2218,11 @@ msgstr "Not found"
msgid "Not synced yet."
msgstr "Not synced yet."
#: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts
#: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts
#: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStageDuo.ts
#: src/flows/stages/captcha/CaptchaStage.ts
#: src/flows/stages/consent/ConsentStage.ts
#: src/flows/stages/password/PasswordStage.ts
@ -2617,6 +2664,10 @@ msgstr "RSA-SHA512"
msgid "Re-evaluate policies"
msgstr "Re-evaluate policies"
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts
msgid "Receive a push notification on your phone to prove your identity."
msgstr "Receive a push notification on your phone to prove your identity."
#: src/pages/flows/FlowForm.ts
msgid "Recovery"
msgstr "Recovery"
@ -2728,6 +2779,7 @@ msgid "Return home"
msgstr "Return home"
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStageDuo.ts
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStageWebAuthn.ts
msgid "Return to device picker"
msgstr "Return to device picker"
@ -2804,6 +2856,10 @@ msgstr "Scopes"
msgid "Search..."
msgstr "Search..."
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
msgid "Secret key"
msgstr "Secret key"
#: src/pages/events/EventInfo.ts
msgid "Secret:"
msgstr "Secret:"
@ -3033,6 +3089,10 @@ msgstr "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenti
msgid "Stage used to configure a WebAutnn authenticator (i.e. Yubikey, FaceID/Windows Hello)."
msgstr "Stage used to configure a WebAutnn authenticator (i.e. Yubikey, FaceID/Windows Hello)."
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
msgid "Stage used to configure a duo-based authenticator. This stage should be used for configuration flows."
msgstr "Stage used to configure a duo-based authenticator. This stage should be used for configuration flows."
#: src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts
msgid "Stage used to configure a static authenticator (i.e. static tokens). This stage should be used for configuration flows."
msgstr "Stage used to configure a static authenticator (i.e. static tokens). This stage should be used for configuration flows."
@ -3041,6 +3101,7 @@ msgstr "Stage used to configure a static authenticator (i.e. static tokens). Thi
msgid "Stage used to validate any authenticator. This stage should be used during authentication or authorization flows."
msgstr "Stage used to validate any authenticator. This stage should be used during authentication or authorization flows."
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
#: src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
@ -3074,7 +3135,7 @@ msgstr "State"
msgid "Static Tokens"
msgstr "Static Tokens"
#: src/pages/user-settings/settings/UserSettingsAuthenticatorTOTP.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorStatic.ts
msgid "Static tokens"
msgstr "Static tokens"
@ -3090,11 +3151,13 @@ msgstr "Statically deny the flow. To use this stage effectively, disable *Evalua
msgid "Status"
msgstr "Status"
#: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorStatic.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorTOTP.ts
msgid "Status: Disabled"
msgstr "Status: Disabled"
#: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorStatic.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorTOTP.ts
msgid "Status: Enabled"
@ -3204,6 +3267,7 @@ msgstr "Successfully created service-connection."
msgid "Successfully created source."
msgstr "Successfully created source."
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
#: src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
@ -3342,6 +3406,7 @@ msgstr "Successfully updated service-connection."
msgid "Successfully updated source."
msgstr "Successfully updated source."
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
#: src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
@ -3533,7 +3598,7 @@ msgstr "Time in minutes the token sent is valid."
msgid "Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format 'transient', and the user doesn't log out manually. (Format: hours=1;minutes=2;seconds=3)."
msgstr "Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format 'transient', and the user doesn't log out manually. (Format: hours=1;minutes=2;seconds=3)."
#: src/pages/user-settings/settings/UserSettingsAuthenticatorStatic.ts
#: src/pages/user-settings/settings/UserSettingsAuthenticatorTOTP.ts
msgid "Time-based One-Time Passwords"
msgstr "Time-based One-Time Passwords"
@ -3893,7 +3958,6 @@ msgstr "User details"
msgid "User events"
msgstr "User events"
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
msgid "User fields"
msgstr "User fields"

View File

@ -63,6 +63,10 @@ msgstr ""
msgid "ANY, any policy must match to include this stage access."
msgstr ""
#:
msgid "API Hostname"
msgstr ""
#:
msgid "API Requests"
msgstr ""
@ -180,6 +184,10 @@ msgstr ""
msgid "Also known as Entity ID. Defaults the Metadata URL."
msgstr ""
#:
msgid "Alternatively, if your current device has Duo installed, click on this link:"
msgstr ""
#:
msgid "Always require consent"
msgstr ""
@ -518,6 +526,10 @@ msgstr ""
msgid "Check Username"
msgstr ""
#:
msgid "Check status"
msgstr ""
#:
msgid "Check your Emails for a password reset link."
msgstr ""
@ -614,6 +626,7 @@ msgstr ""
#:
#:
#:
#:
msgid "Configuration flow"
msgstr ""
@ -715,6 +728,7 @@ msgstr ""
#:
#:
#:
#:
msgid "Continue"
msgstr ""
@ -1002,6 +1016,10 @@ msgstr ""
msgid "Determines how long a session lasts. Default of 0 seconds means that the sessions lasts until the browser is closed."
msgstr ""
#:
msgid "Device classes"
msgstr ""
#:
msgid "Device classes which can be used to authenticate."
msgstr ""
@ -1024,6 +1042,7 @@ msgstr ""
msgid "Disable"
msgstr ""
#:
#:
msgid "Disable Static Tokens"
msgstr ""
@ -1062,6 +1081,22 @@ msgstr ""
msgid "Dummy stage used for testing. Shows a simple continue button and always passes."
msgstr ""
#:
msgid "Duo"
msgstr ""
#:
msgid "Duo Authenticators"
msgstr ""
#:
msgid "Duo activation"
msgstr ""
#:
msgid "Duo push-notifications"
msgstr ""
#:
msgid "Each provider has a different issuer, based on the application slug."
msgstr ""
@ -1151,6 +1186,7 @@ msgstr ""
msgid "Enable StartTLS"
msgstr ""
#:
#:
msgid "Enable Static Tokens"
msgstr ""
@ -1423,6 +1459,7 @@ msgstr ""
msgid "Flow used by an authenticated user to configure their password. If empty, user will not be able to configure change their password."
msgstr ""
#:
#:
#:
msgid "Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage."
@ -1656,6 +1693,10 @@ msgstr ""
msgid "Include claims in id_token"
msgstr ""
#:
msgid "Integration key"
msgstr ""
#:
msgid "Internal Host"
msgstr ""
@ -1811,6 +1852,8 @@ msgstr ""
#:
#:
#:
#:
#:
msgid "Loading"
msgstr ""
@ -1867,6 +1910,7 @@ msgstr ""
#:
#:
#:
#:
msgid "Loading..."
msgstr ""
@ -2054,6 +2098,7 @@ msgstr ""
#:
#:
#:
#:
msgid "Name"
msgstr ""
@ -2171,6 +2216,8 @@ msgstr ""
#:
#:
#:
#:
#:
msgid "Not you?"
msgstr ""
@ -2609,6 +2656,10 @@ msgstr ""
msgid "Re-evaluate policies"
msgstr ""
#:
msgid "Receive a push notification on your phone to prove your identity."
msgstr ""
#:
msgid "Recovery"
msgstr ""
@ -2719,6 +2770,7 @@ msgstr ""
msgid "Return home"
msgstr ""
#:
#:
#:
msgid "Return to device picker"
@ -2796,6 +2848,10 @@ msgstr ""
msgid "Search..."
msgstr ""
#:
msgid "Secret key"
msgstr ""
#:
msgid "Secret:"
msgstr ""
@ -3025,6 +3081,10 @@ msgstr ""
msgid "Stage used to configure a WebAutnn authenticator (i.e. Yubikey, FaceID/Windows Hello)."
msgstr ""
#:
msgid "Stage used to configure a duo-based authenticator. This stage should be used for configuration flows."
msgstr ""
#:
msgid "Stage used to configure a static authenticator (i.e. static tokens). This stage should be used for configuration flows."
msgstr ""
@ -3044,6 +3104,7 @@ msgstr ""
#:
#:
#:
#:
msgid "Stage-specific settings"
msgstr ""
@ -3082,11 +3143,13 @@ msgstr ""
msgid "Status"
msgstr ""
#:
#:
#:
msgid "Status: Disabled"
msgstr ""
#:
#:
#:
msgid "Status: Enabled"
@ -3213,6 +3276,7 @@ msgstr ""
#:
#:
#:
#:
msgid "Successfully created stage."
msgstr ""
@ -3351,6 +3415,7 @@ msgstr ""
#:
#:
#:
#:
msgid "Successfully updated stage."
msgstr ""
@ -3881,7 +3946,6 @@ msgstr ""
msgid "User events"
msgstr ""
#:
#:
msgid "User fields"
msgstr ""

View File

@ -1,5 +1,4 @@
import { CertificateGenerationRequest, CryptoApi } from "authentik-api";
import { CertificateKeyPair } from "authentik-api/src";
import { CertificateGenerationRequest, CertificateKeyPair, CryptoApi } from "authentik-api";
import { t } from "@lingui/macro";
import { customElement } from "lit-element";
import { html, TemplateResult } from "lit-html";

View File

@ -74,7 +74,7 @@ export class FlowViewPage extends LitElement {
new FlowsApi(DEFAULT_CONFIG).flowsInstancesExecuteRetrieve({
slug: this.flow.slug
}).then(link => {
const finalURL = `${link.link}?next=/%23${window.location.href}`;
const finalURL = `${link.link}?next=/%23${window.location.hash}`;
window.open(finalURL, "_blank");
});
}}>

View File

@ -13,7 +13,6 @@ import "../../../elements/forms/FormGroup";
export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
loadInstance(pk: number): Promise<SAMLProvider> {
console.log("reading saml provider");
return new ProvidersApi(DEFAULT_CONFIG).providersSamlRetrieve({
id: pk,
});

View File

@ -15,6 +15,7 @@ import { Stage, StagesApi } from "authentik-api";
import { DEFAULT_CONFIG } from "../../api/Config";
import { ifDefined } from "lit-html/directives/if-defined";
import "./authenticator_duo/AuthenticatorDuoStageForm.ts";
import "./authenticator_static/AuthenticatorStaticStageForm.ts";
import "./authenticator_totp/AuthenticatorTOTPStageForm.ts";
import "./authenticator_validate/AuthenticatorValidateStageForm.ts";

View File

@ -0,0 +1,105 @@
import { FlowsApi, AuthenticatorDuoStage, StagesApi, FlowsInstancesListDesignationEnum, AuthenticatorDuoStageRequest } from "authentik-api";
import { t } from "@lingui/macro";
import { customElement } from "lit-element";
import { html, TemplateResult } from "lit-html";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { ifDefined } from "lit-html/directives/if-defined";
import "../../../elements/forms/HorizontalFormElement";
import "../../../elements/forms/FormGroup";
import { until } from "lit-html/directives/until";
import { first } from "../../../utils";
import { ModelForm } from "../../../elements/forms/ModelForm";
@customElement("ak-stage-authenticator-duo-form")
export class AuthenticatorDuoStageForm extends ModelForm<AuthenticatorDuoStage, string> {
loadInstance(pk: string): Promise<AuthenticatorDuoStage> {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoRetrieve({
stageUuid: pk,
});
}
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated stage.`;
} else {
return t`Successfully created stage.`;
}
}
send = (data: AuthenticatorDuoStage): Promise<AuthenticatorDuoStage> => {
if (this.instance) {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoPartialUpdate({
stageUuid: this.instance.pk || "",
patchedAuthenticatorDuoStageRequest: data
});
} else {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoCreate({
authenticatorDuoStageRequest: data as unknown as AuthenticatorDuoStageRequest
});
}
};
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<div class="form-help-text">
${t`Stage used to configure a duo-based authenticator. This stage should be used for configuration flows.`}
</div>
<ak-form-element-horizontal
label=${t`Name`}
?required=${true}
name="name">
<input type="text" value="${ifDefined(this.instance?.name || "")}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header">
${t`Stage-specific settings`}
</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${t`Integration key`}
?required=${true}
name="clientId">
<input type="text" value="${first(this.instance?.clientId, "")}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Secret key`}
?required=${true}
?writeOnly=${this.instance !== undefined}
name="clientSecret">
<input type="text" value="" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`API Hostname`}
?required=${true}
name="apiHostname">
<input type="text" value="${first(this.instance?.apiHostname, "")}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Configuration flow`}
name="configureFlow">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.configureFlow === undefined}>---------</option>
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
ordering: "pk",
designation: FlowsInstancesListDesignationEnum.StageConfiguration,
}).then(flows => {
return flows.results.map(flow => {
let selected = this.instance?.configureFlow === flow.pk;
if (!this.instance?.pk && !this.instance?.configureFlow && flow.slug === "default-otp-time-configure") {
selected = true;
}
return html`<option value=${ifDefined(flow.pk)} ?selected=${selected}>${flow.name} (${flow.slug})</option>`;
});
}), html`<option>${t`Loading...`}</option>`)}
</select>
<p class="pf-c-form__helper-text">
${t`Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.`}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>`;
}
}

Some files were not shown because too many files have changed in this diff Show More