Duo (#917)
* 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:
commit
250e23408e
2
Makefile
2
Makefile
|
@ -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
|
||||
|
|
1
Pipfile
1
Pipfile
|
@ -44,6 +44,7 @@ urllib3 = {extras = ["secure"],version = "*"}
|
|||
uvicorn = {extras = ["standard"],version = "*"}
|
||||
webauthn = "*"
|
||||
xmlsec = "*"
|
||||
duo-client = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.9"
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"]:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -88,7 +88,10 @@ class TaskInfo:
|
|||
start = default_timer()
|
||||
if hasattr(self, "start_timestamp"):
|
||||
start = self.start_timestamp
|
||||
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 "",
|
||||
|
|
|
@ -174,8 +174,8 @@ class FlowViewSet(ModelViewSet):
|
|||
return HttpResponseBadRequest()
|
||||
successful = importer.apply()
|
||||
if not successful:
|
||||
return Response(status=204)
|
||||
return HttpResponseBadRequest()
|
||||
return Response(status=204)
|
||||
|
||||
@permission_required(
|
||||
"authentik_flows.export_flow",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"]
|
|
@ -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"
|
|
@ -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",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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")
|
|
@ -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()
|
|
@ -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],
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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)):
|
||||
if isinstance(device, WebAuthnDevice):
|
||||
return get_webauthn_challenge(request, device)
|
||||
# Code-based challenges have no hints
|
||||
return {}
|
||||
return get_webauthn_challenge(request, device)
|
||||
|
||||
|
||||
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
|
||||
|
|
|
@ -17,6 +17,7 @@ class DeviceClasses(models.TextChoices):
|
|||
STATIC = "static"
|
||||
TOTP = "totp", _("TOTP")
|
||||
WEBAUTHN = "webauthn", _("WebAuthn")
|
||||
DUO = "duo", _("Duo")
|
||||
|
||||
|
||||
def default_device_classes() -> list:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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={
|
||||
data = {
|
||||
"type": ChallengeTypes.NATIVE.value,
|
||||
"component": "ak-stage-consent",
|
||||
"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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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.",
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
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
|
||||
)
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,6 +63,14 @@ 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 {
|
||||
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",
|
||||
|
@ -118,9 +115,27 @@ func (pi *ProviderInstance) Search(bindDN string, searchReq ldap.SearchRequest,
|
|||
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})
|
||||
}
|
||||
}
|
||||
pi.log.WithField("filter", searchReq.Filter).Debug("Search OK")
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
|
||||
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}
|
||||
}
|
||||
|
|
|
@ -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
1450
schema.yml
File diff suppressed because it is too large
Load Diff
|
@ -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"""
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>`;
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>`;
|
||||
}
|
||||
|
||||
}
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -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=${() => {
|
||||
|
|
|
@ -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}">
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}>`;
|
||||
|
@ -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">
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
}}>
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
Reference in New Issue