Merge pull request #814 from goauthentik/plex-auth

sources/plex: rewrite plex source
This commit is contained in:
Jens L 2021-05-03 22:46:37 +02:00 committed by GitHub
commit 07b001bc2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 2062 additions and 526 deletions

View File

@ -59,3 +59,4 @@ pylint-django = "*"
pytest = "*" pytest = "*"
pytest-django = "*" pytest-django = "*"
selenium = "*" selenium = "*"
requests-mock = "*"

138
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "24f00363590649f2442c6ac28dfe8692f0f317e0a5b91c0696b84610cef299d2" "sha256": "17be2923cf8d281e430ec1467aea723806ac6f7c58fc6553ede92317e43f4d14"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -56,6 +56,7 @@
"sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a",
"sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"
], ],
"markers": "python_version >= '3.6'",
"version": "==3.7.4.post0" "version": "==3.7.4.post0"
}, },
"aioredis": { "aioredis": {
@ -70,6 +71,7 @@
"sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2", "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2",
"sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb" "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"
], ],
"markers": "python_version >= '3.6'",
"version": "==5.0.6" "version": "==5.0.6"
}, },
"asgiref": { "asgiref": {
@ -77,6 +79,7 @@
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee", "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78" "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
], ],
"markers": "python_version >= '3.6'",
"version": "==3.3.4" "version": "==3.3.4"
}, },
"async-timeout": { "async-timeout": {
@ -84,6 +87,7 @@
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
], ],
"markers": "python_full_version >= '3.5.3'",
"version": "==3.0.1" "version": "==3.0.1"
}, },
"attrs": { "attrs": {
@ -91,6 +95,7 @@
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.3.0" "version": "==20.3.0"
}, },
"autobahn": { "autobahn": {
@ -98,6 +103,7 @@
"sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac", "sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac",
"sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03" "sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03"
], ],
"markers": "python_version >= '3.7'",
"version": "==21.3.1" "version": "==21.3.1"
}, },
"automat": { "automat": {
@ -127,6 +133,7 @@
"sha256:e4f8cb923edf035c2ae5f6169c70e77e31df70b88919b92b826a6b9bd14511b1", "sha256:e4f8cb923edf035c2ae5f6169c70e77e31df70b88919b92b826a6b9bd14511b1",
"sha256:f7c2c5c5ed5212b2628d8fb1c587b31c6e8d413ecbbd1a1cdf6f96ed6f5c8d5e" "sha256:f7c2c5c5ed5212b2628d8fb1c587b31c6e8d413ecbbd1a1cdf6f96ed6f5c8d5e"
], ],
"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.62" "version": "==1.20.62"
}, },
"cachetools": { "cachetools": {
@ -134,6 +141,7 @@
"sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001", "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001",
"sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff" "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"
], ],
"markers": "python_version ~= '3.5'",
"version": "==4.2.2" "version": "==4.2.2"
}, },
"cbor2": { "cbor2": {
@ -220,6 +228,7 @@
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" "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" "version": "==4.0.0"
}, },
"click": { "click": {
@ -227,6 +236,7 @@
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2" "version": "==7.1.2"
}, },
"click-didyoumean": { "click-didyoumean": {
@ -300,6 +310,7 @@
"sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f", "sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f",
"sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393" "sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"
], ],
"markers": "python_version >= '3.6'",
"version": "==3.0.2" "version": "==3.0.2"
}, },
"defusedxml": { "defusedxml": {
@ -425,6 +436,7 @@
"hashes": [ "hashes": [
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.18.2" "version": "==0.18.2"
}, },
"geoip2": { "geoip2": {
@ -440,6 +452,7 @@
"sha256:588bdb03a41ecb4978472b847881e5518b5d9ec6153d3d679aa127a55e13b39f", "sha256:588bdb03a41ecb4978472b847881e5518b5d9ec6153d3d679aa127a55e13b39f",
"sha256:9ad25fba07f46a628ad4d0ca09f38dcb262830df2ac95b217f9b0129c9e42206" "sha256:9ad25fba07f46a628ad4d0ca09f38dcb262830df2ac95b217f9b0129c9e42206"
], ],
"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.0" "version": "==1.30.0"
}, },
"gunicorn": { "gunicorn": {
@ -455,6 +468,7 @@
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
], ],
"markers": "python_version >= '3.6'",
"version": "==0.12.0" "version": "==0.12.0"
}, },
"hiredis": { "hiredis": {
@ -501,6 +515,7 @@
"sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0", "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0",
"sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a" "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"
], ],
"markers": "python_version >= '3.6'",
"version": "==2.0.0" "version": "==2.0.0"
}, },
"httptools": { "httptools": {
@ -549,6 +564,7 @@
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2" "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
], ],
"markers": "python_version >= '3.5'",
"version": "==0.5.1" "version": "==0.5.1"
}, },
"itypes": { "itypes": {
@ -563,6 +579,7 @@
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419",
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.11.3" "version": "==2.11.3"
}, },
"jmespath": { "jmespath": {
@ -570,6 +587,7 @@
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.0" "version": "==0.10.0"
}, },
"jsonschema": { "jsonschema": {
@ -584,6 +602,7 @@
"sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006", "sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006",
"sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c" "sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c"
], ],
"markers": "python_version >= '3.6'",
"version": "==5.0.2" "version": "==5.0.2"
}, },
"kubernetes": { "kubernetes": {
@ -596,8 +615,11 @@
}, },
"ldap3": { "ldap3": {
"hashes": [ "hashes": [
"sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056",
"sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91", "sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91",
"sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57" "sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57",
"sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c",
"sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.9" "version": "==2.9"
@ -709,12 +731,14 @@
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.1" "version": "==1.1.1"
}, },
"maxminddb": { "maxminddb": {
"hashes": [ "hashes": [
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc" "sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
], ],
"markers": "python_version >= '3.6'",
"version": "==2.0.3" "version": "==2.0.3"
}, },
"msgpack": { "msgpack": {
@ -790,6 +814,7 @@
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
], ],
"markers": "python_version >= '3.6'",
"version": "==5.1.0" "version": "==5.1.0"
}, },
"oauthlib": { "oauthlib": {
@ -797,6 +822,7 @@
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.1.0" "version": "==3.1.0"
}, },
"packaging": { "packaging": {
@ -812,6 +838,7 @@
"sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa", "sha256:030e4f9df5f53db2292eec37c6255957eb76168c6f974e4176c711cf91ed34aa",
"sha256:b6c5a9643e3545bcbfd9451766cbaa5d9c67e7303c7bc32c750b6fa70ecb107d" "sha256:b6c5a9643e3545bcbfd9451766cbaa5d9c67e7303c7bc32c750b6fa70ecb107d"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.1" "version": "==0.10.1"
}, },
"prompt-toolkit": { "prompt-toolkit": {
@ -819,6 +846,7 @@
"sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04", "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04",
"sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc" "sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc"
], ],
"markers": "python_full_version >= '3.6.1'",
"version": "==3.0.18" "version": "==3.0.18"
}, },
"psycopg2-binary": { "psycopg2-binary": {
@ -864,15 +892,37 @@
}, },
"pyasn1": { "pyasn1": {
"hashes": [ "hashes": [
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3",
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"
], ],
"version": "==0.4.8" "version": "==0.4.8"
}, },
"pyasn1-modules": { "pyasn1-modules": {
"hashes": [ "hashes": [
"sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d",
"sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed",
"sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811",
"sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405",
"sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
"sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4",
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74" "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74",
"sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd",
"sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb",
"sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199",
"sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0",
"sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"
], ],
"version": "==0.2.8" "version": "==0.2.8"
}, },
@ -881,6 +931,7 @@
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.20" "version": "==2.20"
}, },
"pycryptodome": { "pycryptodome": {
@ -924,6 +975,7 @@
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29" "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
], ],
"markers": "python_version >= '3.5'",
"version": "==2.0.2" "version": "==2.0.2"
}, },
"pyjwt": { "pyjwt": {
@ -946,12 +998,14 @@
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7" "version": "==2.4.7"
}, },
"pyrsistent": { "pyrsistent": {
"hashes": [ "hashes": [
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e" "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
], ],
"markers": "python_version >= '3.5'",
"version": "==0.17.3" "version": "==0.17.3"
}, },
"python-dateutil": { "python-dateutil": {
@ -959,6 +1013,7 @@
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.1" "version": "==2.8.1"
}, },
"python-dotenv": { "python-dotenv": {
@ -1015,6 +1070,7 @@
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==3.5.3" "version": "==3.5.3"
}, },
"requests": { "requests": {
@ -1022,12 +1078,14 @@
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.25.1" "version": "==2.25.1"
}, },
"requests-oauthlib": { "requests-oauthlib": {
"hashes": [ "hashes": [
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.3.0" "version": "==1.3.0"
@ -1045,6 +1103,7 @@
"sha256:44bc6b54fddd45e4bc0619059196679f9e8b79c027f4131bb072e6a22f4d5e28", "sha256:44bc6b54fddd45e4bc0619059196679f9e8b79c027f4131bb072e6a22f4d5e28",
"sha256:ac79fb25f5476e8e9ed1c53b8a2286d2c3f5dde49eb37dbcee5c7eb6a8415a22" "sha256:ac79fb25f5476e8e9ed1c53b8a2286d2c3f5dde49eb37dbcee5c7eb6a8415a22"
], ],
"markers": "python_version >= '3'",
"version": "==0.17.4" "version": "==0.17.4"
}, },
"ruamel.yaml.clib": { "ruamel.yaml.clib": {
@ -1081,7 +1140,7 @@
"sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2", "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2",
"sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f" "sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f"
], ],
"markers": "platform_python_implementation == 'CPython' and python_version < '3.10'", "markers": "python_version < '3.10' and platform_python_implementation == 'CPython'",
"version": "==0.2.2" "version": "==0.2.2"
}, },
"s3transfer": { "s3transfer": {
@ -1112,6 +1171,7 @@
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0" "version": "==1.15.0"
}, },
"sqlparse": { "sqlparse": {
@ -1119,6 +1179,7 @@
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
], ],
"markers": "python_version >= '3.5'",
"version": "==0.4.1" "version": "==0.4.1"
}, },
"structlog": { "structlog": {
@ -1174,6 +1235,7 @@
"sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8", "sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8",
"sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb" "sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb"
], ],
"markers": "python_version >= '3.6'",
"version": "==21.2.1" "version": "==21.2.1"
}, },
"typing-extensions": { "typing-extensions": {
@ -1189,6 +1251,7 @@
"sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f", "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
"sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae" "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.0.1" "version": "==3.0.1"
}, },
"urllib3": { "urllib3": {
@ -1233,6 +1296,7 @@
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30", "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e" "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
], ],
"markers": "python_version >= '3.6'",
"version": "==5.0.0" "version": "==5.0.0"
}, },
"watchgod": { "watchgod": {
@ -1262,6 +1326,7 @@
"sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663", "sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663",
"sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f" "sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.58.0" "version": "==0.58.0"
}, },
"websockets": { "websockets": {
@ -1348,6 +1413,7 @@
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
], ],
"markers": "python_version >= '3.6'",
"version": "==1.6.3" "version": "==1.6.3"
}, },
"zope.interface": { "zope.interface": {
@ -1404,6 +1470,7 @@
"sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4", "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4",
"sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263" "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" "version": "==5.4.0"
} }
}, },
@ -1420,6 +1487,7 @@
"sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e", "sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e",
"sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975" "sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"
], ],
"markers": "python_version ~= '3.6'",
"version": "==2.5.6" "version": "==2.5.6"
}, },
"attrs": { "attrs": {
@ -1427,6 +1495,7 @@
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.3.0" "version": "==20.3.0"
}, },
"bandit": { "bandit": {
@ -1452,11 +1521,27 @@
"index": "pypi", "index": "pypi",
"version": "==1.0.1" "version": "==1.0.1"
}, },
"certifi": {
"hashes": [
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
],
"version": "==2020.12.5"
},
"chardet": {
"hashes": [
"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": { "click": {
"hashes": [ "hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2" "version": "==7.1.2"
}, },
"colorama": { "colorama": {
@ -1530,14 +1615,23 @@
"sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0",
"sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005" "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"
], ],
"markers": "python_version >= '3.4'",
"version": "==4.0.7" "version": "==4.0.7"
}, },
"gitpython": { "gitpython": {
"hashes": [ "hashes": [
"sha256:05af150f47a5cca3f4b0af289b73aef8cf3c4fe2385015b06220cbcdee48bb6e", "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b",
"sha256:a77824e516d3298b04fb36ec7845e92747df8fcfee9cacc32dd6239f9652f867" "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61"
], ],
"version": "==3.1.15" "markers": "python_version >= '3.4'",
"version": "==3.1.14"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.10"
}, },
"iniconfig": { "iniconfig": {
"hashes": [ "hashes": [
@ -1551,6 +1645,7 @@
"sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6", "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6",
"sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d" "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"
], ],
"markers": "python_version >= '3.6' and python_version < '4.0'",
"version": "==5.8.0" "version": "==5.8.0"
}, },
"lazy-object-proxy": { "lazy-object-proxy": {
@ -1578,6 +1673,7 @@
"sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93",
"sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" "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" "version": "==1.6.0"
}, },
"mccabe": { "mccabe": {
@ -1614,6 +1710,7 @@
"sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd", "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd",
"sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4" "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"
], ],
"markers": "python_version >= '2.6'",
"version": "==5.6.0" "version": "==5.6.0"
}, },
"pluggy": { "pluggy": {
@ -1621,6 +1718,7 @@
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.13.1" "version": "==0.13.1"
}, },
"py": { "py": {
@ -1628,6 +1726,7 @@
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.10.0" "version": "==1.10.0"
}, },
"pylint": { "pylint": {
@ -1658,6 +1757,7 @@
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7" "version": "==2.4.7"
}, },
"pytest": { "pytest": {
@ -1757,6 +1857,22 @@
], ],
"version": "==2021.4.4" "version": "==2021.4.4"
}, },
"requests": {
"hashes": [
"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": {
"hashes": [
"sha256:33296f228d8c5df11a7988b741325422480baddfdf5dd9318fd0eb40c3ed8595",
"sha256:5c8ef0254c14a84744be146e9799dc13ebc4f6186058112d9aeed96b131b58e2"
],
"index": "pypi",
"version": "==1.9.2"
},
"selenium": { "selenium": {
"hashes": [ "hashes": [
"sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c", "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c",
@ -1770,6 +1886,7 @@
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0" "version": "==1.15.0"
}, },
"smmap": { "smmap": {
@ -1777,6 +1894,7 @@
"sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182", "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182",
"sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2" "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"
], ],
"markers": "python_version >= '3.5'",
"version": "==4.0.0" "version": "==4.0.0"
}, },
"stevedore": { "stevedore": {
@ -1784,6 +1902,7 @@
"sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee", "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee",
"sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a" "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"
], ],
"markers": "python_version >= '3.6'",
"version": "==3.3.0" "version": "==3.3.0"
}, },
"toml": { "toml": {
@ -1791,6 +1910,7 @@
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2" "version": "==0.10.2"
}, },
"typed-ast": { "typed-ast": {

View File

@ -63,6 +63,7 @@ from authentik.sources.oauth.api.source import OAuthSourceViewSet
from authentik.sources.oauth.api.source_connection import ( from authentik.sources.oauth.api.source_connection import (
UserOAuthSourceConnectionViewSet, UserOAuthSourceConnectionViewSet,
) )
from authentik.sources.plex.api import PlexSourceViewSet
from authentik.sources.saml.api import SAMLSourceViewSet from authentik.sources.saml.api import SAMLSourceViewSet
from authentik.stages.authenticator_static.api import ( from authentik.stages.authenticator_static.api import (
AuthenticatorStaticStageViewSet, AuthenticatorStaticStageViewSet,
@ -136,6 +137,7 @@ router.register("sources/oauth_user_connections", UserOAuthSourceConnectionViewS
router.register("sources/ldap", LDAPSourceViewSet) router.register("sources/ldap", LDAPSourceViewSet)
router.register("sources/saml", SAMLSourceViewSet) router.register("sources/saml", SAMLSourceViewSet)
router.register("sources/oauth", OAuthSourceViewSet) router.register("sources/oauth", OAuthSourceViewSet)
router.register("sources/plex", PlexSourceViewSet)
router.register("policies/all", PolicyViewSet) router.register("policies/all", PolicyViewSet)
router.register("policies/bindings", PolicyBindingViewSet) router.register("policies/bindings", PolicyBindingViewSet)

View File

@ -45,6 +45,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
"verbose_name", "verbose_name",
"verbose_name_plural", "verbose_name_plural",
"policy_engine_mode", "policy_engine_mode",
"user_matching_mode",
] ]

View File

@ -0,0 +1,40 @@
# Generated by Django 3.2 on 2021-05-03 17:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0019_source_managed"),
]
operations = [
migrations.AddField(
model_name="source",
name="user_matching_mode",
field=models.TextField(
choices=[
("identifier", "Use the source-specific identifier"),
(
"email_link",
"Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses.",
),
(
"email_deny",
"Use the user's email address, but deny enrollment when the email address already exists.",
),
(
"username_link",
"Link to a user with identical username address. Can have security implications when a username is used with another source.",
),
(
"username_deny",
"Use the user's username, but deny enrollment when the username already exists.",
),
],
default="identifier",
help_text="How the source determines if an existing user should be authenticated or a new user enrolled.",
),
),
]

View File

@ -240,6 +240,30 @@ class Application(PolicyBindingModel):
verbose_name_plural = _("Applications") verbose_name_plural = _("Applications")
class SourceUserMatchingModes(models.TextChoices):
"""Different modes a source can handle new/returning users"""
IDENTIFIER = "identifier", _("Use the source-specific identifier")
EMAIL_LINK = "email_link", _(
(
"Link to a user with identical email address. Can have security implications "
"when a source doesn't validate email addresses."
)
)
EMAIL_DENY = "email_deny", _(
"Use the user's email address, but deny enrollment when the email address already exists."
)
USERNAME_LINK = "username_link", _(
(
"Link to a user with identical username address. Can have security implications "
"when a username is used with another source."
)
)
USERNAME_DENY = "username_deny", _(
"Use the user's username, but deny enrollment when the username already exists."
)
class Source(ManagedModel, SerializerModel, PolicyBindingModel): class Source(ManagedModel, SerializerModel, PolicyBindingModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server""" """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
@ -272,6 +296,17 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
related_name="source_enrollment", related_name="source_enrollment",
) )
user_matching_mode = models.TextField(
choices=SourceUserMatchingModes.choices,
default=SourceUserMatchingModes.IDENTIFIER,
help_text=_(
(
"How the source determines if an existing user should be authenticated or "
"a new user enrolled."
)
),
)
objects = InheritanceManager() objects = InheritanceManager()
@property @property
@ -301,6 +336,8 @@ class UserSourceConnection(CreatedUpdatedModel):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
source = models.ForeignKey(Source, on_delete=models.CASCADE) source = models.ForeignKey(Source, on_delete=models.CASCADE)
objects = InheritanceManager()
class Meta: class Meta:
unique_together = (("user", "source"),) unique_together = (("user", "source"),)

View File

View File

@ -0,0 +1,261 @@
"""Source decision helper"""
from enum import Enum
from typing import Any, Optional, Type
from django.contrib import messages
from django.db.models.query_utils import Q
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext as _
from structlog.stdlib import get_logger
from authentik.core.models import (
Source,
SourceUserMatchingModes,
User,
UserSourceConnection,
)
from authentik.core.sources.stage import (
PLAN_CONTEXT_SOURCES_CONNECTION,
PostUserEnrollmentStage,
)
from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow, Stage, in_memory_stage
from authentik.flows.planner import (
PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_REDIRECT,
PLAN_CONTEXT_SOURCE,
PLAN_CONTEXT_SSO,
FlowPlanner,
)
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.utils import delete_none_keys
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
class Action(Enum):
"""Actions that can be decided based on the request
and source settings"""
LINK = "link"
AUTH = "auth"
ENROLL = "enroll"
DENY = "deny"
class SourceFlowManager:
"""Help sources decide what they should do after authorization. Based on source settings and
previous connections, authenticate the user, enroll a new user, link to an existing user
or deny the request."""
source: Source
request: HttpRequest
identifier: str
connection_type: Type[UserSourceConnection] = UserSourceConnection
def __init__(
self,
source: Source,
request: HttpRequest,
identifier: str,
enroll_info: dict[str, Any],
) -> None:
self.source = source
self.request = request
self.identifier = identifier
self.enroll_info = enroll_info
self._logger = get_logger().bind(source=source, identifier=identifier)
# pylint: disable=too-many-return-statements
def get_action(self, **kwargs) -> tuple[Action, Optional[UserSourceConnection]]:
"""decide which action should be taken"""
new_connection = self.connection_type(
source=self.source, identifier=self.identifier
)
# When request is authenticated, always link
if self.request.user.is_authenticated:
new_connection.user = self.request.user
new_connection = self.update_connection(new_connection, **kwargs)
new_connection.save()
return Action.LINK, new_connection
existing_connections = self.connection_type.objects.filter(
source=self.source, identifier=self.identifier
)
if existing_connections.exists():
connection = existing_connections.first()
return Action.AUTH, self.update_connection(connection, **kwargs)
# No connection exists, but we match on identifier, so enroll
if self.source.user_matching_mode == SourceUserMatchingModes.IDENTIFIER:
# We don't save the connection here cause it doesn't have a user assigned yet
return Action.ENROLL, self.update_connection(new_connection, **kwargs)
# Check for existing users with matching attributes
query = Q()
# Either query existing user based on email or username
if self.source.user_matching_mode in [
SourceUserMatchingModes.EMAIL_LINK,
SourceUserMatchingModes.EMAIL_DENY,
]:
if not self.enroll_info.get("email", None):
self._logger.warning("Refusing to use none email", source=self.source)
return Action.DENY, None
query = Q(email__exact=self.enroll_info.get("email", None))
if self.source.user_matching_mode in [
SourceUserMatchingModes.USERNAME_LINK,
SourceUserMatchingModes.USERNAME_DENY,
]:
if not self.enroll_info.get("username", None):
self._logger.warning(
"Refusing to use none username", source=self.source
)
return Action.DENY, None
query = Q(username__exact=self.enroll_info.get("username", None))
matching_users = User.objects.filter(query)
# No matching users, always enroll
if not matching_users.exists():
return Action.ENROLL, self.update_connection(new_connection, **kwargs)
user = matching_users.first()
if self.source.user_matching_mode in [
SourceUserMatchingModes.EMAIL_LINK,
SourceUserMatchingModes.USERNAME_LINK,
]:
new_connection.user = user
new_connection = self.update_connection(new_connection, **kwargs)
new_connection.save()
return Action.LINK, new_connection
if self.source.user_matching_mode in [
SourceUserMatchingModes.EMAIL_DENY,
SourceUserMatchingModes.USERNAME_DENY,
]:
return Action.DENY, None
return Action.DENY, None
def update_connection(
self, connection: UserSourceConnection, **kwargs
) -> UserSourceConnection:
"""Optionally make changes to the connection after it is looked up/created."""
return connection
def get_flow(self, **kwargs) -> HttpResponse:
"""Get the flow response based on user_matching_mode"""
action, connection = self.get_action()
if action == Action.LINK:
self._logger.debug("Linking existing user")
return self.handle_existing_user_link()
if not connection:
return redirect("/")
if action == Action.AUTH:
self._logger.debug("Handling auth user")
return self.handle_auth_user(connection)
if action == Action.ENROLL:
self._logger.debug("Handling enrollment of new user")
return self.handle_enroll(connection)
return redirect("/")
# pylint: disable=unused-argument
def get_stages_to_append(self, flow: Flow) -> list[Stage]:
"""Hook to override stages which are appended to the flow"""
if flow.slug == self.source.enrollment_flow.slug:
return [
in_memory_stage(PostUserEnrollmentStage),
]
return []
def _handle_login_flow(self, flow: Flow, **kwargs) -> HttpResponse:
"""Prepare Authentication Plan, redirect user FlowExecutor"""
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-admin"
)
kwargs.update(
{
# Since we authenticate the user by their token, they have no backend set
PLAN_CONTEXT_AUTHENTICATION_BACKEND: "django.contrib.auth.backends.ModelBackend",
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_SOURCE: self.source,
PLAN_CONTEXT_REDIRECT: final_redirect,
}
)
if not flow:
return HttpResponseBadRequest()
# We run the Flow planner here so we can pass the Pending user in the context
planner = FlowPlanner(flow)
plan = planner.plan(self.request, kwargs)
for stage in self.get_stages_to_append(flow):
plan.append(stage)
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=flow.slug,
)
# pylint: disable=unused-argument
def handle_auth_user(
self,
connection: UserSourceConnection,
) -> HttpResponse:
"""Login user and redirect."""
messages.success(
self.request,
_(
"Successfully authenticated with %(source)s!"
% {"source": self.source.name}
),
)
flow_kwargs = {PLAN_CONTEXT_PENDING_USER: connection.user}
return self._handle_login_flow(self.source.authentication_flow, **flow_kwargs)
def handle_existing_user_link(
self,
) -> HttpResponse:
"""Handler when the user was already authenticated and linked an external source
to their account."""
Event.new(
EventAction.SOURCE_LINKED,
message="Linked Source",
source=self.source,
).from_http(self.request)
messages.success(
self.request,
_("Successfully linked %(source)s!" % {"source": self.source.name}),
)
return redirect(
reverse(
"authentik_core:if-admin",
)
+ f"#/user;page-{self.source.slug}"
)
def handle_enroll(
self,
connection: UserSourceConnection,
) -> HttpResponse:
"""User was not authenticated and previous request was not authenticated."""
messages.success(
self.request,
_(
"Successfully authenticated with %(source)s!"
% {"source": self.source.name}
),
)
# We run the Flow planner here so we can pass the Pending user in the context
if not self.source.enrollment_flow:
self._logger.warning("source has no enrollment flow")
return HttpResponseBadRequest()
return self._handle_login_flow(
self.source.enrollment_flow,
**{
PLAN_CONTEXT_PROMPT: delete_none_keys(self.enroll_info),
PLAN_CONTEXT_SOURCES_CONNECTION: connection,
},
)

View File

@ -1,32 +1,30 @@
"""OAuth Stages""" """Source flow manager stages"""
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from authentik.core.models import User from authentik.core.models import User, UserSourceConnection
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import StageView from authentik.flows.stage import StageView
from authentik.sources.oauth.models import UserOAuthSourceConnection
PLAN_CONTEXT_SOURCES_OAUTH_ACCESS = "sources_oauth_access" PLAN_CONTEXT_SOURCES_CONNECTION = "goauthentik.io/sources/connection"
class PostUserEnrollmentStage(StageView): class PostUserEnrollmentStage(StageView):
"""Dynamically injected stage which saves the OAuth Connection after """Dynamically injected stage which saves the Connection after
the user has been enrolled.""" the user has been enrolled."""
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Stage used after the user has been enrolled""" """Stage used after the user has been enrolled"""
access: UserOAuthSourceConnection = self.executor.plan.context[ connection: UserSourceConnection = self.executor.plan.context[
PLAN_CONTEXT_SOURCES_OAUTH_ACCESS PLAN_CONTEXT_SOURCES_CONNECTION
] ]
user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
access.user = user connection.user = user
access.save() connection.save()
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
Event.new( Event.new(
EventAction.SOURCE_LINKED, EventAction.SOURCE_LINKED,
message="Linked OAuth Source", message="Linked Source",
source=access.source, source=connection.source,
).from_http(self.request) ).from_http(self.request)
return self.executor.stage_ok() return self.executor.stage_ok()

View File

@ -1,11 +1,14 @@
"""authentik core models tests""" """authentik core models tests"""
from time import sleep from time import sleep
from typing import Callable, Type
from django.test import TestCase from django.test import TestCase
from django.utils.timezone import now from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
from authentik.core.models import Token from authentik.core.models import Provider, Source, Token
from authentik.flows.models import Stage
from authentik.lib.utils.reflection import all_subclasses
class TestModels(TestCase): class TestModels(TestCase):
@ -24,3 +27,40 @@ class TestModels(TestCase):
) )
sleep(0.5) sleep(0.5)
self.assertFalse(token.is_expired) self.assertFalse(token.is_expired)
def source_tester_factory(test_model: Type[Stage]) -> Callable:
"""Test source"""
def tester(self: TestModels):
model_class = None
if test_model._meta.abstract:
model_class = test_model.__bases__[0]()
else:
model_class = test_model()
model_class.slug = "test"
self.assertIsNotNone(model_class.component)
_ = model_class.ui_login_button
_ = model_class.ui_user_settings
return tester
def provider_tester_factory(test_model: Type[Stage]) -> Callable:
"""Test provider"""
def tester(self: TestModels):
model_class = None
if test_model._meta.abstract:
model_class = test_model.__bases__[0]()
else:
model_class = test_model()
self.assertIsNotNone(model_class.component)
return tester
for model in all_subclasses(Source):
setattr(TestModels, f"test_model_{model.__name__}", source_tester_factory(model))
for model in all_subclasses(Provider):
setattr(TestModels, f"test_model_{model.__name__}", provider_tester_factory(model))

View File

@ -2,9 +2,10 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from rest_framework.fields import CharField from rest_framework.fields import CharField, DictField
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import Challenge
@dataclass @dataclass
@ -14,8 +15,8 @@ class UILoginButton:
# Name, ran through i18n # Name, ran through i18n
name: str name: str
# URL Which Button points to # Challenge which is presented to the user when they click the button
url: str challenge: Challenge
# Icon URL, used as-is # Icon URL, used as-is
icon_url: Optional[str] = None icon_url: Optional[str] = None
@ -25,7 +26,7 @@ class UILoginButtonSerializer(PassiveSerializer):
"""Serializer for Login buttons of sources""" """Serializer for Login buttons of sources"""
name = CharField() name = CharField()
url = CharField() challenge = DictField()
icon_url = CharField(required=False, allow_null=True) icon_url = CharField(required=False, allow_null=True)

View File

@ -16,7 +16,6 @@ def model_tester_factory(test_model: Type[Stage]) -> Callable:
"""Test a form""" """Test a form"""
def tester(self: TestModels): def tester(self: TestModels):
try:
model_class = None model_class = None
if test_model._meta.abstract: if test_model._meta.abstract:
model_class = test_model.__bases__[0]() model_class = test_model.__bases__[0]()
@ -25,8 +24,6 @@ def model_tester_factory(test_model: Type[Stage]) -> Callable:
self.assertTrue(issubclass(model_class.type, StageView)) self.assertTrue(issubclass(model_class.type, StageView))
self.assertIsNotNone(test_model.component) self.assertIsNotNone(test_model.component)
_ = test_model.ui_user_settings _ = test_model.ui_user_settings
except NotImplementedError:
pass
return tester return tester

View File

@ -0,0 +1,84 @@
# Generated by Django 3.2 on 2021-05-02 17:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_policies_event_matcher", "0012_auto_20210323_1339"),
]
operations = [
migrations.AlterField(
model_name="eventmatcherpolicy",
name="app",
field=models.TextField(
blank=True,
choices=[
("authentik.admin", "authentik Admin"),
("authentik.api", "authentik API"),
("authentik.events", "authentik Events"),
("authentik.crypto", "authentik Crypto"),
("authentik.flows", "authentik Flows"),
("authentik.outposts", "authentik Outpost"),
("authentik.lib", "authentik lib"),
("authentik.policies", "authentik Policies"),
("authentik.policies.dummy", "authentik Policies.Dummy"),
(
"authentik.policies.event_matcher",
"authentik Policies.Event Matcher",
),
("authentik.policies.expiry", "authentik Policies.Expiry"),
("authentik.policies.expression", "authentik Policies.Expression"),
("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"),
("authentik.policies.password", "authentik Policies.Password"),
("authentik.policies.reputation", "authentik Policies.Reputation"),
("authentik.providers.proxy", "authentik Providers.Proxy"),
("authentik.providers.oauth2", "authentik Providers.OAuth2"),
("authentik.providers.saml", "authentik Providers.SAML"),
("authentik.recovery", "authentik Recovery"),
("authentik.sources.ldap", "authentik Sources.LDAP"),
("authentik.sources.oauth", "authentik Sources.OAuth"),
("authentik.sources.plex", "authentik Sources.Plex"),
("authentik.sources.saml", "authentik Sources.SAML"),
(
"authentik.stages.authenticator_static",
"authentik Stages.Authenticator.Static",
),
(
"authentik.stages.authenticator_totp",
"authentik Stages.Authenticator.TOTP",
),
(
"authentik.stages.authenticator_validate",
"authentik Stages.Authenticator.Validate",
),
(
"authentik.stages.authenticator_webauthn",
"authentik Stages.Authenticator.WebAuthn",
),
("authentik.stages.captcha", "authentik Stages.Captcha"),
("authentik.stages.consent", "authentik Stages.Consent"),
("authentik.stages.deny", "authentik Stages.Deny"),
("authentik.stages.dummy", "authentik Stages.Dummy"),
("authentik.stages.email", "authentik Stages.Email"),
(
"authentik.stages.identification",
"authentik Stages.Identification",
),
("authentik.stages.invitation", "authentik Stages.User Invitation"),
("authentik.stages.password", "authentik Stages.Password"),
("authentik.stages.prompt", "authentik Stages.Prompt"),
("authentik.stages.user_delete", "authentik Stages.User Delete"),
("authentik.stages.user_login", "authentik Stages.User Login"),
("authentik.stages.user_logout", "authentik Stages.User Logout"),
("authentik.stages.user_write", "authentik Stages.User Write"),
("authentik.core", "authentik Core"),
("authentik.managed", "authentik Managed"),
],
default="",
help_text="Match events created by selected application. When left empty, all applications are matched.",
),
),
]

View File

@ -107,6 +107,7 @@ INSTALLED_APPS = [
"authentik.recovery", "authentik.recovery",
"authentik.sources.ldap", "authentik.sources.ldap",
"authentik.sources.oauth", "authentik.sources.oauth",
"authentik.sources.plex",
"authentik.sources.saml", "authentik.sources.saml",
"authentik.stages.authenticator_static", "authentik.stages.authenticator_static",
"authentik.stages.authenticator_totp", "authentik.stages.authenticator_totp",

View File

@ -2,11 +2,21 @@
from importlib import import_module from importlib import import_module
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
LOGGER = get_logger() LOGGER = get_logger()
AUTHENTIK_SOURCES_OAUTH_TYPES = [
"authentik.sources.oauth.types.discord",
"authentik.sources.oauth.types.facebook",
"authentik.sources.oauth.types.github",
"authentik.sources.oauth.types.google",
"authentik.sources.oauth.types.reddit",
"authentik.sources.oauth.types.twitter",
"authentik.sources.oauth.types.azure_ad",
"authentik.sources.oauth.types.oidc",
]
class AuthentikSourceOAuthConfig(AppConfig): class AuthentikSourceOAuthConfig(AppConfig):
"""authentik source.oauth config""" """authentik source.oauth config"""
@ -18,7 +28,7 @@ class AuthentikSourceOAuthConfig(AppConfig):
def ready(self): def ready(self):
"""Load source_types from config file""" """Load source_types from config file"""
for source_type in settings.AUTHENTIK_SOURCES_OAUTH_TYPES: for source_type in AUTHENTIK_SOURCES_OAUTH_TYPES:
try: try:
import_module(source_type) import_module(source_type)
LOGGER.debug("Loaded OAuth Source Type", type=source_type) LOGGER.debug("Loaded OAuth Source Type", type=source_type)

View File

@ -1,23 +0,0 @@
"""authentik oauth_client Authorization backend"""
from typing import Optional
from django.contrib.auth.backends import ModelBackend
from django.http import HttpRequest
from authentik.core.models import User
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
class AuthorizedServiceBackend(ModelBackend):
"Authentication backend for users registered with remote OAuth provider."
def authenticate(
self, request: HttpRequest, source: OAuthSource, identifier: str
) -> Optional[User]:
"Fetch user for a given source by id."
access = UserOAuthSourceConnection.objects.filter(
source=source, identifier=identifier
).select_related("user")
if not access.exists():
return None
return access.first().user

View File

@ -9,6 +9,7 @@ from rest_framework.serializers import Serializer
from authentik.core.models import Source, UserSourceConnection from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UILoginButton, UserSettingSerializer from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.flows.challenge import ChallengeTypes, RedirectChallenge
if TYPE_CHECKING: if TYPE_CHECKING:
from authentik.sources.oauth.types.manager import SourceType from authentik.sources.oauth.types.manager import SourceType
@ -67,10 +68,15 @@ class OAuthSource(Source):
@property @property
def ui_login_button(self) -> UILoginButton: def ui_login_button(self) -> UILoginButton:
return UILoginButton( return UILoginButton(
url=reverse( challenge=RedirectChallenge(
instance={
"type": ChallengeTypes.REDIRECT.value,
"to": reverse(
"authentik_sources_oauth:oauth-client-login", "authentik_sources_oauth:oauth-client-login",
kwargs={"source_slug": self.slug}, kwargs={"source_slug": self.slug},
), ),
}
),
icon_url=static(f"authentik/sources/{self.provider_type}.svg"), icon_url=static(f"authentik/sources/{self.provider_type}.svg"),
name=self.name, name=self.name,
) )
@ -163,16 +169,6 @@ class OpenIDOAuthSource(OAuthSource):
verbose_name_plural = _("OpenID OAuth Sources") verbose_name_plural = _("OpenID OAuth Sources")
class PlexOAuthSource(OAuthSource):
"""Login using plex.tv."""
class Meta:
abstract = True
verbose_name = _("Plex OAuth Source")
verbose_name_plural = _("Plex OAuth Sources")
class UserOAuthSourceConnection(UserSourceConnection): class UserOAuthSourceConnection(UserSourceConnection):
"""Authorized remote OAuth provider.""" """Authorized remote OAuth provider."""

View File

@ -1,13 +0,0 @@
"""Oauth2 Client Settings"""
AUTHENTIK_SOURCES_OAUTH_TYPES = [
"authentik.sources.oauth.types.discord",
"authentik.sources.oauth.types.facebook",
"authentik.sources.oauth.types.github",
"authentik.sources.oauth.types.google",
"authentik.sources.oauth.types.reddit",
"authentik.sources.oauth.types.twitter",
"authentik.sources.oauth.types.azure_ad",
"authentik.sources.oauth.types.oidc",
"authentik.sources.oauth.types.plex",
]

View File

@ -1,7 +1,7 @@
"""Discord Type tests""" """Discord Type tests"""
from django.test import TestCase from django.test import TestCase
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.discord import DiscordOAuth2Callback from authentik.sources.oauth.types.discord import DiscordOAuth2Callback
# https://discord.com/developers/docs/resources/user#user-object # https://discord.com/developers/docs/resources/user#user-object
@ -33,9 +33,7 @@ class TestTypeDiscord(TestCase):
def test_enroll_context(self): def test_enroll_context(self):
"""Test discord Enrollment context""" """Test discord Enrollment context"""
ak_context = DiscordOAuth2Callback().get_user_enroll_context( ak_context = DiscordOAuth2Callback().get_user_enroll_context(DISCORD_USER)
self.source, UserOAuthSourceConnection(), DISCORD_USER
)
self.assertEqual(ak_context["username"], DISCORD_USER["username"]) self.assertEqual(ak_context["username"], DISCORD_USER["username"])
self.assertEqual(ak_context["email"], DISCORD_USER["email"]) self.assertEqual(ak_context["email"], DISCORD_USER["email"])
self.assertEqual(ak_context["name"], DISCORD_USER["username"]) self.assertEqual(ak_context["name"], DISCORD_USER["username"])

View File

@ -1,7 +1,7 @@
"""GitHub Type tests""" """GitHub Type tests"""
from django.test import TestCase from django.test import TestCase
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.github import GitHubOAuth2Callback from authentik.sources.oauth.types.github import GitHubOAuth2Callback
# https://developer.github.com/v3/users/#get-the-authenticated-user # https://developer.github.com/v3/users/#get-the-authenticated-user
@ -63,9 +63,7 @@ class TestTypeGitHub(TestCase):
def test_enroll_context(self): def test_enroll_context(self):
"""Test GitHub Enrollment context""" """Test GitHub Enrollment context"""
ak_context = GitHubOAuth2Callback().get_user_enroll_context( ak_context = GitHubOAuth2Callback().get_user_enroll_context(GITHUB_USER)
self.source, UserOAuthSourceConnection(), GITHUB_USER
)
self.assertEqual(ak_context["username"], GITHUB_USER["login"]) self.assertEqual(ak_context["username"], GITHUB_USER["login"])
self.assertEqual(ak_context["email"], GITHUB_USER["email"]) self.assertEqual(ak_context["email"], GITHUB_USER["email"])
self.assertEqual(ak_context["name"], GITHUB_USER["name"]) self.assertEqual(ak_context["name"], GITHUB_USER["name"])

View File

@ -1,7 +1,7 @@
"""google Type tests""" """google Type tests"""
from django.test import TestCase from django.test import TestCase
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.google import GoogleOAuth2Callback from authentik.sources.oauth.types.google import GoogleOAuth2Callback
# https://developers.google.com/identity/protocols/oauth2/openid-connect?hl=en # https://developers.google.com/identity/protocols/oauth2/openid-connect?hl=en
@ -32,9 +32,7 @@ class TestTypeGoogle(TestCase):
def test_enroll_context(self): def test_enroll_context(self):
"""Test Google Enrollment context""" """Test Google Enrollment context"""
ak_context = GoogleOAuth2Callback().get_user_enroll_context( ak_context = GoogleOAuth2Callback().get_user_enroll_context(GOOGLE_USER)
self.source, UserOAuthSourceConnection(), GOOGLE_USER
)
self.assertEqual(ak_context["username"], GOOGLE_USER["email"]) self.assertEqual(ak_context["username"], GOOGLE_USER["email"])
self.assertEqual(ak_context["email"], GOOGLE_USER["email"]) self.assertEqual(ak_context["email"], GOOGLE_USER["email"])
self.assertEqual(ak_context["name"], GOOGLE_USER["name"]) self.assertEqual(ak_context["name"], GOOGLE_USER["name"])

View File

@ -1,7 +1,7 @@
"""Twitter Type tests""" """Twitter Type tests"""
from django.test import Client, TestCase from django.test import Client, TestCase
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.twitter import TwitterOAuthCallback from authentik.sources.oauth.types.twitter import TwitterOAuthCallback
# https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/ \ # https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/ \
@ -104,9 +104,7 @@ class TestTypeGitHub(TestCase):
def test_enroll_context(self): def test_enroll_context(self):
"""Test Twitter Enrollment context""" """Test Twitter Enrollment context"""
ak_context = TwitterOAuthCallback().get_user_enroll_context( ak_context = TwitterOAuthCallback().get_user_enroll_context(TWITTER_USER)
self.source, UserOAuthSourceConnection(), TWITTER_USER
)
self.assertEqual(ak_context["username"], TWITTER_USER["screen_name"]) self.assertEqual(ak_context["username"], TWITTER_USER["screen_name"])
self.assertEqual(ak_context["email"], TWITTER_USER.get("email", None)) self.assertEqual(ak_context["email"], TWITTER_USER.get("email", None))
self.assertEqual(ak_context["name"], TWITTER_USER["name"]) self.assertEqual(ak_context["name"], TWITTER_USER["name"])

View File

@ -2,7 +2,6 @@
from typing import Any, Optional from typing import Any, Optional
from uuid import UUID from uuid import UUID
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
@ -10,7 +9,7 @@ from authentik.sources.oauth.views.callback import OAuthCallback
class AzureADOAuthCallback(OAuthCallback): class AzureADOAuthCallback(OAuthCallback):
"""AzureAD OAuth2 Callback""" """AzureAD OAuth2 Callback"""
def get_user_id(self, source: OAuthSource, info: dict[str, Any]) -> Optional[str]: def get_user_id(self, info: dict[str, Any]) -> Optional[str]:
try: try:
return str(UUID(info.get("objectId")).int) return str(UUID(info.get("objectId")).int)
except TypeError: except TypeError:
@ -18,8 +17,6 @@ class AzureADOAuthCallback(OAuthCallback):
def get_user_enroll_context( def get_user_enroll_context(
self, self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: dict[str, Any], info: dict[str, Any],
) -> dict[str, Any]: ) -> dict[str, Any]:
mail = info.get("mail", None) or info.get("otherMails", [None])[0] mail = info.get("mail", None) or info.get("otherMails", [None])[0]

View File

@ -1,7 +1,6 @@
"""Discord OAuth Views""" """Discord OAuth Views"""
from typing import Any from typing import Any
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -21,8 +20,6 @@ class DiscordOAuth2Callback(OAuthCallback):
def get_user_enroll_context( def get_user_enroll_context(
self, self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: dict[str, Any], info: dict[str, Any],
) -> dict[str, Any]: ) -> dict[str, Any]:
return { return {

View File

@ -4,7 +4,6 @@ from typing import Any, Optional
from facebook import GraphAPI from facebook import GraphAPI
from authentik.sources.oauth.clients.oauth2 import OAuth2Client from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -34,8 +33,6 @@ class FacebookOAuth2Callback(OAuthCallback):
def get_user_enroll_context( def get_user_enroll_context(
self, self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: dict[str, Any], info: dict[str, Any],
) -> dict[str, Any]: ) -> dict[str, Any]:
return { return {

View File

@ -1,7 +1,6 @@
"""GitHub OAuth Views""" """GitHub OAuth Views"""
from typing import Any from typing import Any
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
@ -11,8 +10,6 @@ class GitHubOAuth2Callback(OAuthCallback):
def get_user_enroll_context( def get_user_enroll_context(
self, self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: dict[str, Any], info: dict[str, Any],
) -> dict[str, Any]: ) -> dict[str, Any]:
return { return {

View File

@ -1,7 +1,6 @@
"""Google OAuth Views""" """Google OAuth Views"""
from typing import Any from typing import Any
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -21,8 +20,6 @@ class GoogleOAuth2Callback(OAuthCallback):
def get_user_enroll_context( def get_user_enroll_context(
self, self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: dict[str, Any], info: dict[str, Any],
) -> dict[str, Any]: ) -> dict[str, Any]:
return { return {

View File

@ -1,7 +1,7 @@
"""OpenID Connect OAuth Views""" """OpenID Connect OAuth Views"""
from typing import Any from typing import Any
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -19,13 +19,11 @@ class OpenIDConnectOAuthRedirect(OAuthRedirect):
class OpenIDConnectOAuth2Callback(OAuthCallback): class OpenIDConnectOAuth2Callback(OAuthCallback):
"""OpenIDConnect OAuth2 Callback""" """OpenIDConnect OAuth2 Callback"""
def get_user_id(self, source: OAuthSource, info: dict[str, str]) -> str: def get_user_id(self, info: dict[str, str]) -> str:
return info.get("sub", "") return info.get("sub", "")
def get_user_enroll_context( def get_user_enroll_context(
self, self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: dict[str, Any], info: dict[str, Any],
) -> dict[str, Any]: ) -> dict[str, Any]:
return { return {

View File

@ -1,134 +0,0 @@
"""Plex OAuth Views"""
from typing import Any, Optional
from urllib.parse import urlencode
from django.http.response import Http404
from requests import post
from requests.api import get
from requests.exceptions import RequestException
from structlog.stdlib import get_logger
from authentik import __version__
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
LOGGER = get_logger()
SESSION_ID_KEY = "PLEX_ID"
SESSION_CODE_KEY = "PLEX_CODE"
DEFAULT_PAYLOAD = {
"X-Plex-Product": "authentik",
"X-Plex-Version": __version__,
"X-Plex-Device-Vendor": "BeryJu.org",
}
class PlexRedirect(OAuthRedirect):
"""Plex Auth redirect, get a pin then redirect to a URL to claim it"""
headers = {}
def get_pin(self, **data) -> dict:
"""Get plex pin that the user will claim
https://forums.plex.tv/t/authenticating-with-plex/609370"""
return post(
"https://plex.tv/api/v2/pins.json?strong=true",
data=data,
headers=self.headers,
).json()
def get_redirect_url(self, **kwargs) -> str:
slug = kwargs.get("source_slug", "")
self.headers = {"Origin": self.request.build_absolute_uri("/")}
try:
source: OAuthSource = OAuthSource.objects.get(slug=slug)
except OAuthSource.DoesNotExist:
raise Http404(f"Unknown OAuth source '{slug}'.")
else:
payload = DEFAULT_PAYLOAD.copy()
payload["X-Plex-Client-Identifier"] = source.consumer_key
# Get a pin first
pin = self.get_pin(**payload)
LOGGER.debug("Got pin", **pin)
self.request.session[SESSION_ID_KEY] = pin["id"]
self.request.session[SESSION_CODE_KEY] = pin["code"]
qs = {
"clientID": source.consumer_key,
"code": pin["code"],
"forwardUrl": self.request.build_absolute_uri(
self.get_callback_url(source)
),
}
return f"https://app.plex.tv/auth#!?{urlencode(qs)}"
class PlexOAuthClient(OAuth2Client):
"""Retrive the plex token after authentication, then ask the plex API about user info"""
def check_application_state(self) -> bool:
return SESSION_ID_KEY in self.request.session
def get_access_token(self, **request_kwargs) -> Optional[dict[str, Any]]:
payload = dict(DEFAULT_PAYLOAD)
payload["X-Plex-Client-Identifier"] = self.source.consumer_key
payload["Accept"] = "application/json"
response = get(
f"https://plex.tv/api/v2/pins/{self.request.session[SESSION_ID_KEY]}",
headers=payload,
)
response.raise_for_status()
token = response.json()["authToken"]
return {"plex_token": token}
def get_profile_info(self, token: dict[str, str]) -> Optional[dict[str, Any]]:
"Fetch user profile information."
qs = {"X-Plex-Token": token["plex_token"]}
try:
response = self.do_request(
"get", f"https://plex.tv/users/account.json?{urlencode(qs)}"
)
response.raise_for_status()
except RequestException as exc:
LOGGER.warning("Unable to fetch user profile", exc=exc)
return None
else:
return response.json().get("user", {})
class PlexOAuth2Callback(OAuthCallback):
"""Plex OAuth2 Callback"""
client_class = PlexOAuthClient
def get_user_id(
self, source: UserOAuthSourceConnection, info: dict[str, Any]
) -> Optional[str]:
return info.get("uuid")
def get_user_enroll_context(
self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: dict[str, Any],
) -> dict[str, Any]:
return {
"username": info.get("username"),
"email": info.get("email"),
"name": info.get("title"),
}
@MANAGER.type()
class PlexType(SourceType):
"""Plex Type definition"""
redirect_view = PlexRedirect
callback_view = PlexOAuth2Callback
name = "Plex"
slug = "plex"
authorization_url = ""
access_token_url = "" # nosec
profile_url = ""

View File

@ -4,7 +4,6 @@ from typing import Any
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from authentik.sources.oauth.clients.oauth2 import OAuth2Client from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -36,8 +35,6 @@ class RedditOAuth2Callback(OAuthCallback):
def get_user_enroll_context( def get_user_enroll_context(
self, self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: dict[str, Any], info: dict[str, Any],
) -> dict[str, Any]: ) -> dict[str, Any]:
return { return {

View File

@ -1,7 +1,6 @@
"""Twitter OAuth Views""" """Twitter OAuth Views"""
from typing import Any from typing import Any
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
@ -11,8 +10,6 @@ class TwitterOAuthCallback(OAuthCallback):
def get_user_enroll_context( def get_user_enroll_context(
self, self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: dict[str, Any], info: dict[str, Any],
) -> dict[str, Any]: ) -> dict[str, Any]:
return { return {

View File

@ -4,35 +4,14 @@ from typing import Any, Optional
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.http import Http404, HttpRequest, HttpResponse from django.http import Http404, HttpRequest, HttpResponse
from django.http.response import HttpResponseBadRequest
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import View from django.views.generic import View
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import User from authentik.core.sources.flow_manager import SourceFlowManager
from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow, in_memory_stage
from authentik.flows.planner import (
PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_REDIRECT,
PLAN_CONTEXT_SOURCE,
PLAN_CONTEXT_SSO,
FlowPlanner,
)
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.utils import delete_none_keys
from authentik.sources.oauth.auth import AuthorizedServiceBackend
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.views.base import OAuthClientMixin from authentik.sources.oauth.views.base import OAuthClientMixin
from authentik.sources.oauth.views.flows import (
PLAN_CONTEXT_SOURCES_OAUTH_ACCESS,
PostUserEnrollmentStage,
)
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
LOGGER = get_logger() LOGGER = get_logger()
@ -40,8 +19,7 @@ LOGGER = get_logger()
class OAuthCallback(OAuthClientMixin, View): class OAuthCallback(OAuthClientMixin, View):
"Base OAuth callback view." "Base OAuth callback view."
source_id = None source: OAuthSource
source = None
# pylint: disable=too-many-return-statements # pylint: disable=too-many-return-statements
def get(self, request: HttpRequest, *_, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *_, **kwargs) -> HttpResponse:
@ -60,47 +38,27 @@ class OAuthCallback(OAuthClientMixin, View):
# Fetch access token # Fetch access token
token = client.get_access_token() token = client.get_access_token()
if token is None: if token is None:
return self.handle_login_failure(self.source, "Could not retrieve token.") return self.handle_login_failure("Could not retrieve token.")
if "error" in token: if "error" in token:
return self.handle_login_failure(self.source, token["error"]) return self.handle_login_failure(token["error"])
# Fetch profile info # Fetch profile info
info = client.get_profile_info(token) raw_info = client.get_profile_info(token)
if info is None: if raw_info is None:
return self.handle_login_failure(self.source, "Could not retrieve profile.") return self.handle_login_failure("Could not retrieve profile.")
identifier = self.get_user_id(self.source, info) identifier = self.get_user_id(raw_info)
if identifier is None: if identifier is None:
return self.handle_login_failure(self.source, "Could not determine id.") return self.handle_login_failure("Could not determine id.")
# Get or create access record # Get or create access record
defaults = { enroll_info = self.get_user_enroll_context(raw_info)
"access_token": token.get("access_token"), sfm = OAuthSourceFlowManager(
}
existing = UserOAuthSourceConnection.objects.filter(
source=self.source, identifier=identifier
)
if existing.exists():
connection = existing.first()
connection.access_token = token.get("access_token")
UserOAuthSourceConnection.objects.filter(pk=connection.pk).update(
**defaults
)
else:
connection = UserOAuthSourceConnection(
source=self.source, source=self.source,
request=self.request,
identifier=identifier, identifier=identifier,
enroll_info=enroll_info,
)
return sfm.get_flow(
access_token=token.get("access_token"), access_token=token.get("access_token"),
) )
user = AuthorizedServiceBackend().authenticate(
source=self.source, identifier=identifier, request=request
)
if user is None:
if self.request.user.is_authenticated:
LOGGER.debug("Linking existing user", source=self.source)
return self.handle_existing_user_link(self.source, connection, info)
LOGGER.debug("Handling enrollment of new user", source=self.source)
return self.handle_enroll(self.source, connection, info)
LOGGER.debug("Handling existing user", source=self.source)
return self.handle_existing_user(self.source, user, connection, info)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get_callback_url(self, source: OAuthSource) -> str: def get_callback_url(self, source: OAuthSource) -> str:
@ -114,132 +72,35 @@ class OAuthCallback(OAuthClientMixin, View):
def get_user_enroll_context( def get_user_enroll_context(
self, self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: dict[str, Any], info: dict[str, Any],
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Create a dict of User data""" """Create a dict of User data"""
raise NotImplementedError() raise NotImplementedError()
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get_user_id( def get_user_id(self, info: dict[str, Any]) -> Optional[str]:
self, source: UserOAuthSourceConnection, info: dict[str, Any]
) -> Optional[str]:
"""Return unique identifier from the profile info.""" """Return unique identifier from the profile info."""
if "id" in info: if "id" in info:
return info["id"] return info["id"]
return None return None
def handle_login_failure(self, source: OAuthSource, reason: str) -> HttpResponse: def handle_login_failure(self, reason: str) -> HttpResponse:
"Message user and redirect on error." "Message user and redirect on error."
LOGGER.warning("Authentication Failure", reason=reason) LOGGER.warning("Authentication Failure", reason=reason)
messages.error(self.request, _("Authentication Failed.")) messages.error(self.request, _("Authentication Failed."))
return redirect(self.get_error_redirect(source, reason)) return redirect(self.get_error_redirect(self.source, reason))
def handle_login_flow(
self, flow: Flow, *stages_to_append, **kwargs
) -> HttpResponse:
"""Prepare Authentication Plan, redirect user FlowExecutor"""
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-admin"
)
kwargs.update(
{
# Since we authenticate the user by their token, they have no backend set
PLAN_CONTEXT_AUTHENTICATION_BACKEND: "django.contrib.auth.backends.ModelBackend",
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_SOURCE: self.source,
PLAN_CONTEXT_REDIRECT: final_redirect,
}
)
if not flow:
return HttpResponseBadRequest()
# We run the Flow planner here so we can pass the Pending user in the context
planner = FlowPlanner(flow)
plan = planner.plan(self.request, kwargs)
for stage in stages_to_append:
plan.append(stage)
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=flow.slug,
)
# pylint: disable=unused-argument class OAuthSourceFlowManager(SourceFlowManager):
def handle_existing_user( """Flow manager for oauth sources"""
connection_type = UserOAuthSourceConnection
def update_connection(
self, self,
source: OAuthSource, connection: UserOAuthSourceConnection,
user: User, access_token: Optional[str] = None,
access: UserOAuthSourceConnection, ) -> UserOAuthSourceConnection:
info: dict[str, Any], """Set the access_token on the connection"""
) -> HttpResponse: connection.access_token = access_token
"Login user and redirect." return connection
messages.success(
self.request,
_(
"Successfully authenticated with %(source)s!"
% {"source": self.source.name}
),
)
flow_kwargs = {PLAN_CONTEXT_PENDING_USER: user}
return self.handle_login_flow(source.authentication_flow, **flow_kwargs)
def handle_existing_user_link(
self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: dict[str, Any],
) -> HttpResponse:
"""Handler when the user was already authenticated and linked an external source
to their account."""
# there's already a user logged in, just link them up
user = self.request.user
access.user = user
access.save()
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
Event.new(
EventAction.SOURCE_LINKED, message="Linked OAuth Source", source=source
).from_http(self.request)
messages.success(
self.request,
_("Successfully linked %(source)s!" % {"source": self.source.name}),
)
return redirect(
reverse(
"authentik_core:if-admin",
)
+ f"#/user;page-{self.source.slug}"
)
def handle_enroll(
self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: dict[str, Any],
) -> HttpResponse:
"""User was not authenticated and previous request was not authenticated."""
messages.success(
self.request,
_(
"Successfully authenticated with %(source)s!"
% {"source": self.source.name}
),
)
# We run the Flow planner here so we can pass the Pending user in the context
if not source.enrollment_flow:
LOGGER.warning("source has no enrollment flow", source=source)
return HttpResponseBadRequest()
return self.handle_login_flow(
source.enrollment_flow,
in_memory_stage(PostUserEnrollmentStage),
**{
PLAN_CONTEXT_PROMPT: delete_none_keys(
self.get_user_enroll_context(source, access, info)
),
PLAN_CONTEXT_SOURCES_OAUTH_ACCESS: access,
},
)

View File

View File

@ -0,0 +1,75 @@
"""Plex Source Serializer"""
from django.http import Http404
from django.shortcuts import get_object_or_404
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.fields import CharField
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import RedirectChallenge
from authentik.flows.views import to_stage_response
from authentik.sources.plex.models import PlexSource
from authentik.sources.plex.plex import PlexAuth
class PlexSourceSerializer(SourceSerializer):
"""Plex Source Serializer"""
class Meta:
model = PlexSource
fields = SourceSerializer.Meta.fields + ["client_id", "allowed_servers"]
class PlexTokenRedeemSerializer(PassiveSerializer):
"""Serializer to redeem a plex token"""
plex_token = CharField()
class PlexSourceViewSet(ModelViewSet):
"""Plex source Viewset"""
queryset = PlexSource.objects.all()
serializer_class = PlexSourceSerializer
lookup_field = "slug"
@permission_required(None)
@swagger_auto_schema(
request_body=PlexTokenRedeemSerializer(),
responses={200: RedirectChallenge(), 404: "Token not found"},
manual_parameters=[
openapi.Parameter(
name="slug",
in_=openapi.IN_QUERY,
type=openapi.TYPE_STRING,
)
],
)
@action(
methods=["POST"],
detail=False,
pagination_class=None,
filter_backends=[],
permission_classes=[AllowAny],
)
def redeem_token(self, request: Request) -> Response:
"""Redeem a plex token, check it's access to resources against what's allowed
for the source, and redirect to an authentication/enrollment flow."""
source: PlexSource = get_object_or_404(
PlexSource, slug=request.query_params.get("slug", "")
)
plex_token = request.data.get("plex_token", None)
if not plex_token:
raise Http404
auth_api = PlexAuth(source, plex_token)
if not auth_api.check_server_overlap():
raise Http404
response = auth_api.get_user_url(request)
return to_stage_response(request, response)

View File

@ -0,0 +1,10 @@
"""authentik plex config"""
from django.apps import AppConfig
class AuthentikSourcePlexConfig(AppConfig):
"""authentik source plex config"""
name = "authentik.sources.plex"
label = "authentik_sources_plex"
verbose_name = "authentik Sources.Plex"

View File

@ -0,0 +1,77 @@
# Generated by Django 3.2 on 2021-05-03 18:59
import django.contrib.postgres.fields
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_core", "0020_source_user_matching_mode"),
]
operations = [
migrations.CreateModel(
name="PlexSource",
fields=[
(
"source_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.source",
),
),
(
"client_id",
models.TextField(
default="yOuPQQvgNfBGreZZ38WoOY1d3qk3Xso2AuQHi6RG",
help_text="Client identifier used to talk to Plex.",
),
),
(
"allowed_servers",
django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(),
default=list,
help_text="Which servers a user has to be a member of to be granted access. Empty list allows every server.",
size=None,
),
),
],
options={
"verbose_name": "Plex Source",
"verbose_name_plural": "Plex Sources",
},
bases=("authentik_core.source",),
),
migrations.CreateModel(
name="PlexSourceConnection",
fields=[
(
"usersourceconnection_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.usersourceconnection",
),
),
("plex_token", models.TextField()),
("identifier", models.TextField()),
],
options={
"verbose_name": "User Plex Source Connection",
"verbose_name_plural": "User Plex Source Connections",
},
bases=("authentik_core.usersourceconnection",),
),
]

View File

@ -0,0 +1,80 @@
"""Plex source"""
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from rest_framework.fields import CharField
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.providers.oauth2.generators import generate_client_id
class PlexAuthenticationChallenge(Challenge):
"""Challenge shown to the user in identification stage"""
client_id = CharField()
slug = CharField()
class PlexSource(Source):
"""Authenticate against plex.tv"""
client_id = models.TextField(
default=generate_client_id(),
help_text=_("Client identifier used to talk to Plex."),
)
allowed_servers = ArrayField(
models.TextField(),
default=list,
help_text=_(
(
"Which servers a user has to be a member of to be granted access. "
"Empty list allows every server."
)
),
)
@property
def component(self) -> str:
return "ak-source-plex-form"
@property
def serializer(self) -> BaseSerializer:
from authentik.sources.plex.api import PlexSourceSerializer
return PlexSourceSerializer
@property
def ui_login_button(self) -> UILoginButton:
return UILoginButton(
challenge=PlexAuthenticationChallenge(
{
"type": ChallengeTypes.NATIVE.value,
"component": "ak-flow-sources-plex",
"client_id": self.client_id,
"slug": self.slug,
}
),
icon_url=static("authentik/sources/plex.svg"),
name=self.name,
)
class Meta:
verbose_name = _("Plex Source")
verbose_name_plural = _("Plex Sources")
class PlexSourceConnection(UserSourceConnection):
"""Connect user and plex source"""
plex_token = models.TextField()
identifier = models.TextField()
class Meta:
verbose_name = _("User Plex Source Connection")
verbose_name_plural = _("User Plex Source Connections")

View File

@ -0,0 +1,112 @@
"""Plex Views"""
from urllib.parse import urlencode
from django.http.request import HttpRequest
from django.http.response import Http404, HttpResponse
from requests import Session
from requests.exceptions import RequestException
from structlog.stdlib import get_logger
from authentik import __version__
from authentik.core.sources.flow_manager import SourceFlowManager
from authentik.sources.plex.models import PlexSource, PlexSourceConnection
LOGGER = get_logger()
SESSION_ID_KEY = "PLEX_ID"
SESSION_CODE_KEY = "PLEX_CODE"
class PlexAuth:
"""Plex authentication utilities"""
_source: PlexSource
_token: str
def __init__(self, source: PlexSource, token: str):
self._source = source
self._token = token
self._session = Session()
self._session.headers.update(
{"Accept": "application/json", "Content-Type": "application/json"}
)
self._session.headers.update(self.headers)
@property
def headers(self) -> dict[str, str]:
"""Get common headers"""
return {
"X-Plex-Product": "authentik",
"X-Plex-Version": __version__,
"X-Plex-Device-Vendor": "BeryJu.org",
}
def get_resources(self) -> list[dict]:
"""Get all resources the plex-token has access to"""
qs = {
"X-Plex-Token": self._token,
"X-Plex-Client-Identifier": self._source.client_id,
}
response = self._session.get(
f"https://plex.tv/api/v2/resources?{urlencode(qs)}",
)
response.raise_for_status()
return response.json()
def get_user_info(self) -> tuple[dict, int]:
"""Get user info of the plex token"""
qs = {
"X-Plex-Token": self._token,
"X-Plex-Client-Identifier": self._source.client_id,
}
response = self._session.get(
f"https://plex.tv/api/v2/user?{urlencode(qs)}",
)
response.raise_for_status()
raw_user_info = response.json()
return {
"username": raw_user_info.get("username"),
"email": raw_user_info.get("email"),
"name": raw_user_info.get("title"),
}, raw_user_info.get("id")
def check_server_overlap(self) -> bool:
"""Check if the plex-token has any server overlap with our configured servers"""
try:
resources = self.get_resources()
except RequestException as exc:
LOGGER.warning("Unable to fetch user resources", exc=exc)
raise Http404
else:
for resource in resources:
if resource["provides"] != "server":
continue
if resource["clientIdentifier"] in self._source.allowed_servers:
LOGGER.info(
"Plex allowed access from server", name=resource["name"]
)
return True
return False
def get_user_url(self, request: HttpRequest) -> HttpResponse:
"""Get a URL to a flow executor for either enrollment or authentication"""
user_info, identifier = self.get_user_info()
sfm = PlexSourceFlowManager(
source=self._source,
request=request,
identifier=str(identifier),
enroll_info=user_info,
)
return sfm.get_flow(plex_token=self._token)
class PlexSourceFlowManager(SourceFlowManager):
"""Flow manager for plex sources"""
connection_type = PlexSourceConnection
def update_connection(
self, connection: PlexSourceConnection, plex_token: str
) -> PlexSourceConnection:
"""Set the access_token on the connection"""
connection.plex_token = plex_token
return connection

View File

@ -0,0 +1,64 @@
"""plex Source tests"""
from django.test import TestCase
from requests_mock import Mocker
from authentik.providers.oauth2.generators import generate_client_secret
from authentik.sources.plex.models import PlexSource
from authentik.sources.plex.plex import PlexAuth
USER_INFO_RESPONSE = {
"id": 1234123419,
"uuid": "qwerqewrqewrqwr",
"username": "username",
"title": "title",
"email": "foo@bar.baz",
}
RESOURCES_RESPONSE = [
{
"name": "foo",
"clientIdentifier": "allowed",
"provides": "server",
},
{
"name": "foo",
"clientIdentifier": "denied",
"provides": "server",
},
]
class TestPlexSource(TestCase):
"""plex Source tests"""
def setUp(self):
self.source: PlexSource = PlexSource.objects.create(
name="test",
slug="test",
)
def test_get_user_info(self):
"""Test get_user_info"""
token = generate_client_secret()
api = PlexAuth(self.source, token)
with Mocker() as mocker:
mocker.get("https://plex.tv/api/v2/user", json=USER_INFO_RESPONSE)
self.assertEqual(
api.get_user_info(),
(
{"username": "username", "email": "foo@bar.baz", "name": "title"},
1234123419,
),
)
def test_check_server_overlap(self):
"""Test check_server_overlap"""
token = generate_client_secret()
api = PlexAuth(self.source, token)
with Mocker() as mocker:
mocker.get("https://plex.tv/api/v2/resources", json=RESOURCES_RESPONSE)
self.assertFalse(api.check_server_overlap())
self.source.allowed_servers = ["allowed"]
self.source.save()
with Mocker() as mocker:
mocker.get("https://plex.tv/api/v2/resources", json=RESOURCES_RESPONSE)
self.assertTrue(api.check_server_overlap())

View File

@ -10,6 +10,7 @@ from rest_framework.serializers import Serializer
from authentik.core.models import Source from authentik.core.models import Source
from authentik.core.types import UILoginButton from authentik.core.types import UILoginButton
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.flows.challenge import ChallengeTypes, RedirectChallenge
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.utils.time import timedelta_string_validator from authentik.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.processors.constants import ( from authentik.sources.saml.processors.constants import (
@ -169,10 +170,16 @@ class SAMLSource(Source):
@property @property
def ui_login_button(self) -> UILoginButton: def ui_login_button(self) -> UILoginButton:
return UILoginButton( return UILoginButton(
name=self.name, challenge=RedirectChallenge(
url=reverse( instance={
"authentik_sources_saml:login", kwargs={"source_slug": self.slug} "type": ChallengeTypes.REDIRECT.value,
"to": reverse(
"authentik_sources_saml:login",
kwargs={"source_slug": self.slug},
), ),
}
),
name=self.name,
) )
def __str__(self): def __str__(self):

View File

@ -112,7 +112,9 @@ class IdentificationStageView(ChallengeStageView):
for source in sources: for source in sources:
ui_login_button = source.ui_login_button ui_login_button = source.ui_login_button
if ui_login_button: if ui_login_button:
ui_sources.append(asdict(ui_login_button)) button = asdict(ui_login_button)
button["challenge"] = ui_login_button.challenge.data
ui_sources.append(button)
challenge.initial_data["sources"] = ui_sources challenge.initial_data["sources"] = ui_sources
return challenge return challenge

View File

@ -117,7 +117,10 @@ class TestIdentificationStage(TestCase):
{ {
"icon_url": "/static/authentik/sources/.svg", "icon_url": "/static/authentik/sources/.svg",
"name": "test", "name": "test",
"url": "/source/oauth/login/test/", "challenge": {
"to": "/source/oauth/login/test/",
"type": "redirect",
},
} }
], ],
}, },
@ -158,9 +161,12 @@ class TestIdentificationStage(TestCase):
"title": self.flow.title, "title": self.flow.title,
"sources": [ "sources": [
{ {
"challenge": {
"to": "/source/oauth/login/test/",
"type": "redirect",
},
"icon_url": "/static/authentik/sources/.svg", "icon_url": "/static/authentik/sources/.svg",
"name": "test", "name": "test",
"url": "/source/oauth/login/test/",
} }
], ],
}, },

View File

@ -10213,6 +10213,238 @@ paths:
description: A unique integer value identifying this User OAuth Source Connection. description: A unique integer value identifying this User OAuth Source Connection.
required: true required: true
type: integer type: integer
/sources/plex/:
get:
operationId: sources_plex_list
description: Plex source Viewset
parameters:
- name: ordering
in: query
description: Which field to use when ordering the results.
required: false
type: string
- name: search
in: query
description: A search term.
required: false
type: string
- name: page
in: query
description: Page Index
required: false
type: integer
- name: page_size
in: query
description: Page Size
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- results
- pagination
type: object
properties:
pagination:
required:
- next
- previous
- count
- current
- total_pages
- start_index
- end_index
type: object
properties:
next:
type: number
previous:
type: number
count:
type: number
current:
type: number
total_pages:
type: number
start_index:
type: number
end_index:
type: number
results:
type: array
items:
$ref: '#/definitions/PlexSource'
'403':
description: Authentication credentials were invalid, absent or insufficient.
schema:
$ref: '#/definitions/GenericError'
tags:
- sources
post:
operationId: sources_plex_create
description: Plex source Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/PlexSource'
responses:
'201':
description: ''
schema:
$ref: '#/definitions/PlexSource'
'400':
description: Invalid input.
schema:
$ref: '#/definitions/ValidationError'
'403':
description: Authentication credentials were invalid, absent or insufficient.
schema:
$ref: '#/definitions/GenericError'
tags:
- sources
parameters: []
/sources/plex/redeem_token/:
post:
operationId: sources_plex_redeem_token
description: |-
Redeem a plex token, check it's access to resources against what's allowed
for the source, and redirect to an authentication/enrollment flow.
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/PlexTokenRedeem'
- name: slug
in: query
type: string
responses:
'200':
description: ''
schema:
$ref: '#/definitions/RedirectChallenge'
'404':
description: Token not found
'400':
description: Invalid input.
schema:
$ref: '#/definitions/ValidationError'
'403':
description: Authentication credentials were invalid, absent or insufficient.
schema:
$ref: '#/definitions/GenericError'
tags:
- sources
parameters: []
/sources/plex/{slug}/:
get:
operationId: sources_plex_read
description: Plex source Viewset
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/PlexSource'
'403':
description: Authentication credentials were invalid, absent or insufficient.
schema:
$ref: '#/definitions/GenericError'
'404':
description: Object does not exist or caller has insufficient permissions
to access it.
schema:
$ref: '#/definitions/APIException'
tags:
- sources
put:
operationId: sources_plex_update
description: Plex source Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/PlexSource'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/PlexSource'
'400':
description: Invalid input.
schema:
$ref: '#/definitions/ValidationError'
'403':
description: Authentication credentials were invalid, absent or insufficient.
schema:
$ref: '#/definitions/GenericError'
'404':
description: Object does not exist or caller has insufficient permissions
to access it.
schema:
$ref: '#/definitions/APIException'
tags:
- sources
patch:
operationId: sources_plex_partial_update
description: Plex source Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/PlexSource'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/PlexSource'
'400':
description: Invalid input.
schema:
$ref: '#/definitions/ValidationError'
'403':
description: Authentication credentials were invalid, absent or insufficient.
schema:
$ref: '#/definitions/GenericError'
'404':
description: Object does not exist or caller has insufficient permissions
to access it.
schema:
$ref: '#/definitions/APIException'
tags:
- sources
delete:
operationId: sources_plex_delete
description: Plex source Viewset
parameters: []
responses:
'204':
description: ''
'403':
description: Authentication credentials were invalid, absent or insufficient.
schema:
$ref: '#/definitions/GenericError'
'404':
description: Object does not exist or caller has insufficient permissions
to access it.
schema:
$ref: '#/definitions/APIException'
tags:
- sources
parameters:
- name: slug
in: path
description: Internal source name, used in URLs.
required: true
type: string
format: slug
pattern: ^[-a-zA-Z0-9_]+$
/sources/saml/: /sources/saml/:
get: get:
operationId: sources_saml_list operationId: sources_saml_list
@ -16210,6 +16442,7 @@ definitions:
- authentik.recovery - authentik.recovery
- authentik.sources.ldap - authentik.sources.ldap
- authentik.sources.oauth - authentik.sources.oauth
- authentik.sources.plex
- authentik.sources.saml - authentik.sources.saml
- authentik.stages.authenticator_static - authentik.stages.authenticator_static
- authentik.stages.authenticator_totp - authentik.stages.authenticator_totp
@ -17056,6 +17289,17 @@ definitions:
enum: enum:
- all - all
- any - any
user_matching_mode:
title: User matching mode
description: How the source determines if an existing user should be authenticated
or a new user enrolled.
type: string
enum:
- identifier
- email_link
- email_deny
- username_link
- username_deny
UserSetting: UserSetting:
required: required:
- object_uid - object_uid
@ -17136,6 +17380,17 @@ definitions:
enum: enum:
- all - all
- any - any
user_matching_mode:
title: User matching mode
description: How the source determines if an existing user should be authenticated
or a new user enrolled.
type: string
enum:
- identifier
- email_link
- email_deny
- username_link
- username_deny
server_uri: server_uri:
title: Server URI title: Server URI
type: string type: string
@ -17316,6 +17571,17 @@ definitions:
enum: enum:
- all - all
- any - any
user_matching_mode:
title: User matching mode
description: How the source determines if an existing user should be authenticated
or a new user enrolled.
type: string
enum:
- identifier
- email_link
- email_deny
- username_link
- username_deny
provider_type: provider_type:
title: Provider type title: Provider type
type: string type: string
@ -17386,6 +17652,132 @@ definitions:
type: string type: string
maxLength: 255 maxLength: 255
minLength: 1 minLength: 1
PlexSource:
required:
- name
- slug
type: object
properties:
pk:
title: Pbm uuid
type: string
format: uuid
readOnly: true
name:
title: Name
description: Source's display Name.
type: string
minLength: 1
slug:
title: Slug
description: Internal source name, used in URLs.
type: string
format: slug
pattern: ^[-a-zA-Z0-9_]+$
maxLength: 50
minLength: 1
enabled:
title: Enabled
type: boolean
authentication_flow:
title: Authentication flow
description: Flow to use when authenticating existing users.
type: string
format: uuid
x-nullable: true
enrollment_flow:
title: Enrollment flow
description: Flow to use when enrolling new users.
type: string
format: uuid
x-nullable: true
component:
title: Component
type: string
readOnly: true
verbose_name:
title: Verbose name
type: string
readOnly: true
verbose_name_plural:
title: Verbose name plural
type: string
readOnly: true
policy_engine_mode:
title: Policy engine mode
type: string
enum:
- all
- any
user_matching_mode:
title: User matching mode
description: How the source determines if an existing user should be authenticated
or a new user enrolled.
type: string
enum:
- identifier
- email_link
- email_deny
- username_link
- username_deny
client_id:
title: Client id
description: Client identifier used to talk to Plex.
type: string
minLength: 1
allowed_servers:
description: Which servers a user has to be a member of to be granted access.
Empty list allows every server.
type: array
items:
title: Allowed servers
type: string
minLength: 1
PlexTokenRedeem:
required:
- plex_token
type: object
properties:
plex_token:
title: Plex token
type: string
minLength: 1
RedirectChallenge:
required:
- type
- to
type: object
properties:
type:
title: Type
type: string
enum:
- native
- shell
- redirect
component:
title: Component
type: string
minLength: 1
title:
title: Title
type: string
minLength: 1
background:
title: Background
type: string
minLength: 1
response_errors:
title: Response errors
type: object
additionalProperties:
type: array
items:
$ref: '#/definitions/ErrorDetail'
to:
title: To
type: string
minLength: 1
SAMLSource: SAMLSource:
required: required:
- name - name
@ -17445,6 +17837,17 @@ definitions:
enum: enum:
- all - all
- any - any
user_matching_mode:
title: User matching mode
description: How the source determines if an existing user should be authenticated
or a new user enrolled.
type: string
enum:
- identifier
- email_link
- email_deny
- username_link
- username_deny
pre_authentication_flow: pre_authentication_flow:
title: Pre authentication flow title: Pre authentication flow
description: Flow used before authentication. description: Flow used before authentication.
@ -18190,6 +18593,17 @@ definitions:
enabled: enabled:
title: Enabled title: Enabled
type: boolean type: boolean
user_matching_mode:
title: User matching mode
description: How the source determines if an existing user should
be authenticated or a new user enrolled.
type: string
enum:
- identifier
- email_link
- email_deny
- username_link
- username_deny
authentication_flow: authentication_flow:
title: Authentication flow title: Authentication flow
description: Flow to use when authenticating existing users. description: Flow to use when authenticating existing users.

View File

@ -147,11 +147,11 @@ class TestSourceOAuth2(SeleniumTestCase):
wait.until( wait.until(
ec.presence_of_element_located( ec.presence_of_element_located(
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") (By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
) )
) )
identification_stage.find_element( identification_stage.find_element(
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
).click() ).click()
# Now we should be at the IDP, wait for the login field # Now we should be at the IDP, wait for the login field
@ -206,11 +206,11 @@ class TestSourceOAuth2(SeleniumTestCase):
wait.until( wait.until(
ec.presence_of_element_located( ec.presence_of_element_located(
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") (By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
) )
) )
identification_stage.find_element( identification_stage.find_element(
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
).click() ).click()
# Now we should be at the IDP, wait for the login field # Now we should be at the IDP, wait for the login field
@ -245,11 +245,11 @@ class TestSourceOAuth2(SeleniumTestCase):
wait.until( wait.until(
ec.presence_of_element_located( ec.presence_of_element_located(
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") (By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
) )
) )
identification_stage.find_element( identification_stage.find_element(
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
).click() ).click()
# Now we should be at the IDP, wait for the login field # Now we should be at the IDP, wait for the login field
@ -338,17 +338,18 @@ class TestSourceOAuth1(SeleniumTestCase):
wait.until( wait.until(
ec.presence_of_element_located( ec.presence_of_element_located(
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") (By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
) )
) )
identification_stage.find_element( identification_stage.find_element(
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
).click() ).click()
# Now we should be at the IDP, wait for the login field # Now we should be at the IDP, wait for the login field
self.wait.until(ec.presence_of_element_located((By.NAME, "username"))) self.wait.until(ec.presence_of_element_located((By.NAME, "username")))
self.driver.find_element(By.NAME, "username").send_keys("example-user") self.driver.find_element(By.NAME, "username").send_keys("example-user")
self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER) self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER)
sleep(2)
# Wait until we're logged in # Wait until we're logged in
self.wait.until( self.wait.until(

View File

@ -140,11 +140,11 @@ class TestSourceSAML(SeleniumTestCase):
wait.until( wait.until(
ec.presence_of_element_located( ec.presence_of_element_located(
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") (By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
) )
) )
identification_stage.find_element( identification_stage.find_element(
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
).click() ).click()
# Now we should be at the IDP, wait for the username field # Now we should be at the IDP, wait for the username field
@ -208,11 +208,11 @@ class TestSourceSAML(SeleniumTestCase):
wait.until( wait.until(
ec.presence_of_element_located( ec.presence_of_element_located(
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") (By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
) )
) )
identification_stage.find_element( identification_stage.find_element(
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
).click() ).click()
sleep(1) sleep(1)
@ -289,11 +289,11 @@ class TestSourceSAML(SeleniumTestCase):
wait.until( wait.until(
ec.presence_of_element_located( ec.presence_of_element_located(
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link") (By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
) )
) )
identification_stage.find_element( identification_stage.find_element(
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link" By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
).click() ).click()
# Now we should be at the IDP, wait for the username field # Now we should be at the IDP, wait for the username field

View File

@ -6,7 +6,8 @@
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:lit/recommended" "plugin:lit/recommended",
"plugin:custom-elements/recommended"
], ],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
@ -15,7 +16,8 @@
}, },
"plugins": [ "plugins": [
"@typescript-eslint", "@typescript-eslint",
"lit" "lit",
"custom-elements"
], ],
"rules": { "rules": {
"indent": "off", "indent": "off",

View File

@ -18,7 +18,7 @@ stages:
steps: steps:
- task: NodeTool@0 - task: NodeTool@0
inputs: inputs:
versionSpec: '12.x' versionSpec: '14.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: CmdLine@2 - task: CmdLine@2
inputs: inputs:
@ -37,7 +37,7 @@ stages:
steps: steps:
- task: NodeTool@0 - task: NodeTool@0
inputs: inputs:
versionSpec: '12.x' versionSpec: '14.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
inputs: inputs:
@ -59,7 +59,7 @@ stages:
steps: steps:
- task: NodeTool@0 - task: NodeTool@0
inputs: inputs:
versionSpec: '12.x' versionSpec: '14.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
inputs: inputs:
@ -83,7 +83,7 @@ stages:
steps: steps:
- task: NodeTool@0 - task: NodeTool@0
inputs: inputs:
versionSpec: '12.x' versionSpec: '14.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
inputs: inputs:

13
web/package-lock.json generated
View File

@ -3723,6 +3723,14 @@
"resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz",
"integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==" "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw=="
}, },
"eslint-plugin-custom-elements": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-custom-elements/-/eslint-plugin-custom-elements-0.0.2.tgz",
"integrity": "sha512-lIRBhxh0M/1seyMzSPJwdfdNtlVSPArJ+erF2xqjPsd/6SdCuT43hCQNV2A2te3GqBWhgh/unXSVRO09c1kyPA==",
"requires": {
"eslint-rule-documentation": ">=1.0.0"
}
},
"eslint-plugin-lit": { "eslint-plugin-lit": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-lit/-/eslint-plugin-lit-1.3.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-lit/-/eslint-plugin-lit-1.3.0.tgz",
@ -3733,6 +3741,11 @@
"requireindex": "^1.2.0" "requireindex": "^1.2.0"
} }
}, },
"eslint-rule-documentation": {
"version": "1.0.23",
"resolved": "https://registry.npmjs.org/eslint-rule-documentation/-/eslint-rule-documentation-1.0.23.tgz",
"integrity": "sha512-pWReu3fkohwyvztx/oQWWgld2iad25TfUdi6wvhhaDPIQjHU/pyvlKgXFw1kX31SQK2Nq9MH+vRDWB0ZLy8fYw=="
},
"eslint-scope": { "eslint-scope": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",

View File

@ -67,6 +67,7 @@
"construct-style-sheets-polyfill": "^2.4.16", "construct-style-sheets-polyfill": "^2.4.16",
"eslint": "^7.25.0", "eslint": "^7.25.0",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.2",
"eslint-plugin-lit": "^1.3.0", "eslint-plugin-lit": "^1.3.0",
"flowchart.js": "^1.15.0", "flowchart.js": "^1.15.0",
"lit-element": "^2.5.0", "lit-element": "^2.5.0",

View File

@ -272,7 +272,7 @@ body {
.pf-c-login__main-header-desc { .pf-c-login__main-header-desc {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }
.pf-c-login__main-footer-links-item-link > img { .pf-c-login__main-footer-links-item img {
filter: invert(1); filter: invert(1);
} }
.pf-c-login__main-footer-band { .pf-c-login__main-footer-band {

View File

@ -23,6 +23,7 @@ import "./stages/email/EmailStage";
import "./stages/identification/IdentificationStage"; import "./stages/identification/IdentificationStage";
import "./stages/password/PasswordStage"; import "./stages/password/PasswordStage";
import "./stages/prompt/PromptStage"; import "./stages/prompt/PromptStage";
import "./sources/plex/PlexLoginInit";
import { ShellChallenge, RedirectChallenge } from "../api/Flows"; import { ShellChallenge, RedirectChallenge } from "../api/Flows";
import { IdentificationChallenge } from "./stages/identification/IdentificationStage"; import { IdentificationChallenge } from "./stages/identification/IdentificationStage";
import { PasswordChallenge } from "./stages/password/PasswordStage"; import { PasswordChallenge } from "./stages/password/PasswordStage";
@ -44,6 +45,7 @@ import { AccessDeniedChallenge } from "./access_denied/FlowAccessDenied";
import { PFSize } from "../elements/Spinner"; import { PFSize } from "../elements/Spinner";
import { TITLE_DEFAULT } from "../constants"; import { TITLE_DEFAULT } from "../constants";
import { configureSentry } from "../api/Sentry"; import { configureSentry } from "../api/Sentry";
import { PlexAuthenticationChallenge } from "./sources/plex/PlexLoginInit";
@customElement("ak-flow-executor") @customElement("ak-flow-executor")
export class FlowExecutor extends LitElement implements StageHost { export class FlowExecutor extends LitElement implements StageHost {
@ -223,6 +225,8 @@ export class FlowExecutor extends LitElement implements StageHost {
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} .challenge=${this.challenge as WebAuthnAuthenticatorRegisterChallenge}></ak-stage-authenticator-webauthn>`;
case "ak-stage-authenticator-validate": 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} .challenge=${this.challenge as AuthenticatorValidateStageChallenge}></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>`;
default: default:
break; break;
} }

View File

@ -0,0 +1,95 @@
import { VERSION } from "../../../constants";
export interface PlexPinResponse {
// Only has the fields we care about
authToken?: string;
code: string;
id: number;
}
export interface PlexResource {
name: string;
provides: string;
clientIdentifier: string;
}
export const DEFAULT_HEADERS = {
"Accept": "application/json",
"Content-Type": "application/json",
"X-Plex-Product": "authentik",
"X-Plex-Version": VERSION,
"X-Plex-Device-Vendor": "BeryJu.org",
};
export function popupCenterScreen(url: string, title: string, w: number, h: number): Window | null {
const top = (screen.height - h) / 4, left = (screen.width - w) / 2;
const popup = window.open(url, title, `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`);
return popup;
}
export class PlexAPIClient {
token: string;
constructor(token: string) {
this.token = token;
}
static async getPin(clientIdentifier: string): Promise<{ authUrl: string, pin: PlexPinResponse }> {
const headers = { ...DEFAULT_HEADERS, ...{
"X-Plex-Client-Identifier": clientIdentifier
}};
const pinResponse = await fetch("https://plex.tv/api/v2/pins.json?strong=true", {
method: "POST",
headers: headers
});
const pin: PlexPinResponse = await pinResponse.json();
return {
authUrl: `https://app.plex.tv/auth#!?clientID=${encodeURIComponent(clientIdentifier)}&code=${pin.code}`,
pin: pin
};
}
static async pinStatus(clientIdentifier: string, id: number): Promise<string | undefined> {
const headers = { ...DEFAULT_HEADERS, ...{
"X-Plex-Client-Identifier": clientIdentifier
}};
const pinResponse = await fetch(`https://plex.tv/api/v2/pins/${id}`, {
headers: headers
});
const pin: PlexPinResponse = await pinResponse.json();
return pin.authToken || "";
}
static async pinPoll(clientIdentifier: string, id: number): Promise<string> {
const executePoll = async (
resolve: (authToken: string) => void,
reject: (e: Error) => void
) => {
try {
const response = await PlexAPIClient.pinStatus(clientIdentifier, id);
if (response) {
resolve(response);
} else {
setTimeout(executePoll, 500, resolve, reject);
}
} catch (e) {
reject(e);
}
};
return new Promise(executePoll);
}
async getServers(): Promise<PlexResource[]> {
const resourcesResponse = await fetch(`https://plex.tv/api/v2/resources?X-Plex-Token=${this.token}&X-Plex-Client-Identifier=authentik`, {
headers: DEFAULT_HEADERS
});
const resources: PlexResource[] = await resourcesResponse.json();
return resources.filter(r => {
return r.provides === "server";
});
}
}

View File

@ -0,0 +1,69 @@
import { t } from "@lingui/macro";
import { Challenge } 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";
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 { html, TemplateResult } from "lit-html";
import { BaseStage } from "../../stages/base";
import { PlexAPIClient, popupCenterScreen } from "./API";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { SourcesApi } from "authentik-api";
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;
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 authWindow = popupCenterScreen(authInfo.authUrl, "plex auth", 550, 700);
PlexAPIClient.pinPoll(this.challenge?.client_id || "", authInfo.pin.id).then(token => {
authWindow?.close();
new SourcesApi(DEFAULT_CONFIG).sourcesPlexRedeemToken({
data: {
plexToken: token,
},
slug: this.challenge?.slug || "",
}).then(r => {
window.location.assign(r.to);
});
});
}
render(): TemplateResult {
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${t`Authenticating with Plex...`}
</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-empty-state
?loading="${true}">
</ak-empty-state>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
</footer>`;
}
}

View File

@ -1,6 +1,8 @@
import { Challenge } from "authentik-api";
import { LitElement } from "lit-element"; import { LitElement } from "lit-element";
export interface StageHost { export interface StageHost {
challenge?: Challenge;
submit<T>(formData?: T): Promise<void>; submit<T>(formData?: T): Promise<void>;
} }

View File

@ -35,7 +35,7 @@ export interface IdentificationChallenge extends Challenge {
export interface UILoginButton { export interface UILoginButton {
name: string; name: string;
url: string; challenge: Challenge;
icon_url?: string; icon_url?: string;
} }
@ -49,7 +49,11 @@ export class IdentificationStage extends BaseStage {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal].concat( return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal].concat(
css` css`
/* login page's icons */ /* login page's icons */
.pf-c-login__main-footer-links-item-link img { .pf-c-login__main-footer-links-item button {
background-color: transparent;
border: 0;
}
.pf-c-login__main-footer-links-item img {
fill: var(--pf-c-login__main-footer-links-item-link-svg--Fill); fill: var(--pf-c-login__main-footer-links-item-link-svg--Fill);
width: 100px; width: 100px;
max-width: var(--pf-c-login__main-footer-links-item-link-svg--Width); max-width: var(--pf-c-login__main-footer-links-item-link-svg--Width);
@ -131,9 +135,12 @@ export class IdentificationStage extends BaseStage {
icon = html`<img src="${source.icon_url}" alt="${source.name}">`; icon = html`<img src="${source.icon_url}" alt="${source.name}">`;
} }
return html`<li class="pf-c-login__main-footer-links-item"> return html`<li class="pf-c-login__main-footer-links-item">
<a href="${source.url}" class="pf-c-login__main-footer-links-item-link"> <button type="button" @click=${() => {
if (!this.host) return;
this.host.challenge = source.challenge;
}}>
${icon} ${icon}
</a> </button>
</li>`; </li>`;
} }

View File

@ -156,6 +156,10 @@ msgstr "Allow users to use Applications based on properties, enforce Password Cr
msgid "Allowed count" msgid "Allowed count"
msgstr "Allowed count" msgstr "Allowed count"
#: src/pages/sources/plex/PlexSourceForm.ts:119
msgid "Allowed servers"
msgstr "Allowed servers"
#: src/pages/sources/saml/SAMLSourceForm.ts:144 #: src/pages/sources/saml/SAMLSourceForm.ts:144
msgid "Allows authentication flows initiated by the IdP. This can be a security risk, as no validation of the request ID is done." msgid "Allows authentication flows initiated by the IdP. This can be a security risk, as no validation of the request ID is done."
msgstr "Allows authentication flows initiated by the IdP. This can be a security risk, as no validation of the request ID is done." msgstr "Allows authentication flows initiated by the IdP. This can be a security risk, as no validation of the request ID is done."
@ -277,11 +281,16 @@ msgstr "Attributes"
msgid "Audience" msgid "Audience"
msgstr "Audience" msgstr "Audience"
#: src/flows/sources/plex/PlexLoginInit.ts:56
msgid "Authenticating with Plex..."
msgstr "Authenticating with Plex..."
#: src/pages/flows/FlowForm.ts:55 #: src/pages/flows/FlowForm.ts:55
msgid "Authentication" msgid "Authentication"
msgstr "Authentication" msgstr "Authentication"
#: src/pages/sources/oauth/OAuthSourceForm.ts:189 #: src/pages/sources/oauth/OAuthSourceForm.ts:189
#: src/pages/sources/plex/PlexSourceForm.ts:149
#: src/pages/sources/saml/SAMLSourceForm.ts:245 #: src/pages/sources/saml/SAMLSourceForm.ts:245
msgid "Authentication flow" msgid "Authentication flow"
msgstr "Authentication flow" msgstr "Authentication flow"
@ -395,8 +404,8 @@ msgstr "Binding Type"
msgid "Build hash: {0}" msgid "Build hash: {0}"
msgstr "Build hash: {0}" msgstr "Build hash: {0}"
#: src/pages/sources/SourcesListPage.ts:103 #: src/pages/sources/SourcesListPage.ts:104
#: src/pages/sources/SourcesListPage.ts:105 #: src/pages/sources/SourcesListPage.ts:106
msgid "Built-in" msgid "Built-in"
msgstr "Built-in" msgstr "Built-in"
@ -544,6 +553,7 @@ msgstr "Click to copy token"
#: src/pages/providers/oauth2/OAuth2ProviderForm.ts:107 #: src/pages/providers/oauth2/OAuth2ProviderForm.ts:107
#: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:99 #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:99
#: src/pages/sources/plex/PlexSourceForm.ts:113
msgid "Client ID" msgid "Client ID"
msgstr "Client ID" msgstr "Client ID"
@ -744,8 +754,8 @@ msgstr "Copy Key"
#: src/pages/providers/ProviderListPage.ts:116 #: src/pages/providers/ProviderListPage.ts:116
#: src/pages/providers/RelatedApplicationButton.ts:27 #: src/pages/providers/RelatedApplicationButton.ts:27
#: src/pages/providers/RelatedApplicationButton.ts:35 #: src/pages/providers/RelatedApplicationButton.ts:35
#: src/pages/sources/SourcesListPage.ts:113 #: src/pages/sources/SourcesListPage.ts:114
#: src/pages/sources/SourcesListPage.ts:122 #: src/pages/sources/SourcesListPage.ts:123
#: src/pages/stages/StageListPage.ts:119 #: src/pages/stages/StageListPage.ts:119
#: src/pages/stages/StageListPage.ts:128 #: src/pages/stages/StageListPage.ts:128
#: src/pages/stages/invitation/InvitationListPage.ts:77 #: src/pages/stages/invitation/InvitationListPage.ts:77
@ -842,7 +852,7 @@ msgstr "Create provider"
#: src/pages/policies/PolicyListPage.ts:136 #: src/pages/policies/PolicyListPage.ts:136
#: src/pages/property-mappings/PropertyMappingListPage.ts:125 #: src/pages/property-mappings/PropertyMappingListPage.ts:125
#: src/pages/providers/ProviderListPage.ts:119 #: src/pages/providers/ProviderListPage.ts:119
#: src/pages/sources/SourcesListPage.ts:125 #: src/pages/sources/SourcesListPage.ts:126
#: src/pages/stages/StageListPage.ts:131 #: src/pages/stages/StageListPage.ts:131
msgid "Create {0}" msgid "Create {0}"
msgstr "Create {0}" msgstr "Create {0}"
@ -898,7 +908,7 @@ msgstr "Define how notifications are sent to users, like Email or Webhook."
#: src/pages/policies/PolicyListPage.ts:115 #: src/pages/policies/PolicyListPage.ts:115
#: src/pages/property-mappings/PropertyMappingListPage.ts:104 #: src/pages/property-mappings/PropertyMappingListPage.ts:104
#: src/pages/providers/ProviderListPage.ts:98 #: src/pages/providers/ProviderListPage.ts:98
#: src/pages/sources/SourcesListPage.ts:94 #: src/pages/sources/SourcesListPage.ts:95
#: src/pages/stages/StageListPage.ts:110 #: src/pages/stages/StageListPage.ts:110
#: src/pages/stages/invitation/InvitationListPage.ts:68 #: src/pages/stages/invitation/InvitationListPage.ts:68
#: src/pages/stages/prompt/PromptListPage.ts:87 #: src/pages/stages/prompt/PromptListPage.ts:87
@ -1008,7 +1018,7 @@ msgstr "Disable Static Tokens"
msgid "Disable Time-based OTP" msgid "Disable Time-based OTP"
msgstr "Disable Time-based OTP" msgstr "Disable Time-based OTP"
#: src/pages/sources/SourcesListPage.ts:63 #: src/pages/sources/SourcesListPage.ts:64
msgid "Disabled" msgid "Disabled"
msgstr "Disabled" msgstr "Disabled"
@ -1049,7 +1059,7 @@ msgstr "Each provider has a different issuer, based on the application slug."
#: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:128 #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:128
#: src/pages/providers/proxy/ProxyProviderViewPage.ts:127 #: src/pages/providers/proxy/ProxyProviderViewPage.ts:127
#: src/pages/providers/saml/SAMLProviderViewPage.ts:121 #: src/pages/providers/saml/SAMLProviderViewPage.ts:121
#: src/pages/sources/SourcesListPage.ts:82 #: src/pages/sources/SourcesListPage.ts:83
#: src/pages/sources/ldap/LDAPSourceViewPage.ts:105 #: src/pages/sources/ldap/LDAPSourceViewPage.ts:105
#: src/pages/sources/oauth/OAuthSourceViewPage.ts:125 #: src/pages/sources/oauth/OAuthSourceViewPage.ts:125
#: src/pages/sources/saml/SAMLSourceViewPage.ts:111 #: src/pages/sources/saml/SAMLSourceViewPage.ts:111
@ -1086,7 +1096,7 @@ msgstr "Edit User"
msgid "Either no applications are defined, or you don't have access to any." msgid "Either no applications are defined, or you don't have access to any."
msgstr "Either no applications are defined, or you don't have access to any." msgstr "Either no applications are defined, or you don't have access to any."
#: src/flows/stages/identification/IdentificationStage.ts:138 #: src/flows/stages/identification/IdentificationStage.ts:146
#: src/pages/events/TransportForm.ts:46 #: src/pages/events/TransportForm.ts:46
#: src/pages/stages/identification/IdentificationStageForm.ts:81 #: src/pages/stages/identification/IdentificationStageForm.ts:81
#: src/pages/user-settings/UserDetailsPage.ts:71 #: src/pages/user-settings/UserDetailsPage.ts:71
@ -1099,7 +1109,7 @@ msgstr "Email"
msgid "Email address" msgid "Email address"
msgstr "Email address" msgstr "Email address"
#: src/flows/stages/identification/IdentificationStage.ts:145 #: src/flows/stages/identification/IdentificationStage.ts:153
msgid "Email or username" msgid "Email or username"
msgstr "Email or username" msgstr "Email or username"
@ -1136,6 +1146,7 @@ msgstr "Enable this if you don't want to use this provider as a proxy, and want
#: src/pages/policies/PolicyBindingForm.ts:199 #: src/pages/policies/PolicyBindingForm.ts:199
#: src/pages/sources/ldap/LDAPSourceForm.ts:69 #: src/pages/sources/ldap/LDAPSourceForm.ts:69
#: src/pages/sources/oauth/OAuthSourceForm.ts:115 #: src/pages/sources/oauth/OAuthSourceForm.ts:115
#: src/pages/sources/plex/PlexSourceForm.ts:102
#: src/pages/sources/saml/SAMLSourceForm.ts:69 #: src/pages/sources/saml/SAMLSourceForm.ts:69
msgid "Enabled" msgid "Enabled"
msgstr "Enabled" msgstr "Enabled"
@ -1145,6 +1156,7 @@ msgid "Enrollment"
msgstr "Enrollment" msgstr "Enrollment"
#: src/pages/sources/oauth/OAuthSourceForm.ts:210 #: src/pages/sources/oauth/OAuthSourceForm.ts:210
#: src/pages/sources/plex/PlexSourceForm.ts:170
#: src/pages/sources/saml/SAMLSourceForm.ts:266 #: src/pages/sources/saml/SAMLSourceForm.ts:266
#: src/pages/stages/identification/IdentificationStageForm.ts:106 #: src/pages/stages/identification/IdentificationStageForm.ts:106
msgid "Enrollment flow" msgid "Enrollment flow"
@ -1357,16 +1369,19 @@ msgid "Flow Overview"
msgstr "Flow Overview" msgstr "Flow Overview"
#: src/pages/sources/oauth/OAuthSourceForm.ts:185 #: src/pages/sources/oauth/OAuthSourceForm.ts:185
#: src/pages/sources/plex/PlexSourceForm.ts:145
#: src/pages/sources/saml/SAMLSourceForm.ts:220 #: src/pages/sources/saml/SAMLSourceForm.ts:220
msgid "Flow settings" msgid "Flow settings"
msgstr "Flow settings" msgstr "Flow settings"
#: src/pages/sources/oauth/OAuthSourceForm.ts:207 #: src/pages/sources/oauth/OAuthSourceForm.ts:207
#: src/pages/sources/plex/PlexSourceForm.ts:167
#: src/pages/sources/saml/SAMLSourceForm.ts:263 #: src/pages/sources/saml/SAMLSourceForm.ts:263
msgid "Flow to use when authenticating existing users." msgid "Flow to use when authenticating existing users."
msgstr "Flow to use when authenticating existing users." msgstr "Flow to use when authenticating existing users."
#: src/pages/sources/oauth/OAuthSourceForm.ts:228 #: src/pages/sources/oauth/OAuthSourceForm.ts:228
#: src/pages/sources/plex/PlexSourceForm.ts:188
#: src/pages/sources/saml/SAMLSourceForm.ts:284 #: src/pages/sources/saml/SAMLSourceForm.ts:284
msgid "Flow to use when enrolling new users." msgid "Flow to use when enrolling new users."
msgstr "Flow to use when enrolling new users." msgstr "Flow to use when enrolling new users."
@ -1410,7 +1425,7 @@ msgstr "Force the user to configure an authenticator"
msgid "Forgot password?" msgid "Forgot password?"
msgstr "Forgot password?" msgstr "Forgot password?"
#: src/flows/stages/identification/IdentificationStage.ts:124 #: src/flows/stages/identification/IdentificationStage.ts:132
msgid "Forgot username or password?" msgid "Forgot username or password?"
msgstr "Forgot username or password?" msgstr "Forgot username or password?"
@ -1510,6 +1525,7 @@ msgstr "Hide managed mappings"
#: src/pages/providers/saml/SAMLProviderForm.ts:177 #: src/pages/providers/saml/SAMLProviderForm.ts:177
#: src/pages/sources/ldap/LDAPSourceForm.ts:167 #: src/pages/sources/ldap/LDAPSourceForm.ts:167
#: src/pages/sources/ldap/LDAPSourceForm.ts:193 #: src/pages/sources/ldap/LDAPSourceForm.ts:193
#: src/pages/sources/plex/PlexSourceForm.ts:132
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts:114 #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts:114
#: src/pages/stages/identification/IdentificationStageForm.ts:85 #: src/pages/stages/identification/IdentificationStageForm.ts:85
#: src/pages/stages/password/PasswordStageForm.ts:86 #: src/pages/stages/password/PasswordStageForm.ts:86
@ -1692,9 +1708,13 @@ msgstr "Let the user identify themselves with their username or Email address."
msgid "Library" msgid "Library"
msgstr "Library" msgstr "Library"
#: src/pages/sources/plex/PlexSourceForm.ts:137
msgid "Load servers"
msgstr "Load servers"
#: src/elements/table/Table.ts:120 #: src/elements/table/Table.ts:120
#: src/flows/FlowExecutor.ts:167 #: src/flows/FlowExecutor.ts:168
#: src/flows/FlowExecutor.ts:213 #: src/flows/FlowExecutor.ts:216
#: src/flows/access_denied/FlowAccessDenied.ts:27 #: src/flows/access_denied/FlowAccessDenied.ts:27
#: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts:43 #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts:43
#: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts:33 #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts:33
@ -1705,7 +1725,7 @@ msgstr "Library"
#: src/flows/stages/consent/ConsentStage.ts:28 #: src/flows/stages/consent/ConsentStage.ts:28
#: src/flows/stages/dummy/DummyStage.ts:27 #: src/flows/stages/dummy/DummyStage.ts:27
#: src/flows/stages/email/EmailStage.ts:26 #: src/flows/stages/email/EmailStage.ts:26
#: src/flows/stages/identification/IdentificationStage.ts:171 #: src/flows/stages/identification/IdentificationStage.ts:179
#: src/flows/stages/password/PasswordStage.ts:31 #: src/flows/stages/password/PasswordStage.ts:31
#: src/flows/stages/prompt/PromptStage.ts:126 #: src/flows/stages/prompt/PromptStage.ts:126
#: src/pages/applications/ApplicationViewPage.ts:43 #: src/pages/applications/ApplicationViewPage.ts:43
@ -1750,6 +1770,8 @@ msgstr "Loading"
#: src/pages/sources/oauth/OAuthSourceForm.ts:177 #: src/pages/sources/oauth/OAuthSourceForm.ts:177
#: src/pages/sources/oauth/OAuthSourceForm.ts:205 #: src/pages/sources/oauth/OAuthSourceForm.ts:205
#: src/pages/sources/oauth/OAuthSourceForm.ts:226 #: src/pages/sources/oauth/OAuthSourceForm.ts:226
#: src/pages/sources/plex/PlexSourceForm.ts:165
#: src/pages/sources/plex/PlexSourceForm.ts:186
#: src/pages/sources/saml/SAMLSourceForm.ts:126 #: src/pages/sources/saml/SAMLSourceForm.ts:126
#: src/pages/sources/saml/SAMLSourceForm.ts:240 #: src/pages/sources/saml/SAMLSourceForm.ts:240
#: src/pages/sources/saml/SAMLSourceForm.ts:261 #: src/pages/sources/saml/SAMLSourceForm.ts:261
@ -1780,7 +1802,7 @@ msgstr "Log the currently pending user in."
msgid "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP." msgid "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP."
msgstr "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP." msgstr "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP."
#: src/flows/stages/identification/IdentificationStage.ts:183 #: src/flows/stages/identification/IdentificationStage.ts:191
msgid "Login to continue to {0}." msgid "Login to continue to {0}."
msgstr "Login to continue to {0}." msgstr "Login to continue to {0}."
@ -1913,11 +1935,12 @@ msgstr "Monitor"
#: src/pages/providers/saml/SAMLProviderForm.ts:53 #: src/pages/providers/saml/SAMLProviderForm.ts:53
#: src/pages/providers/saml/SAMLProviderImportForm.ts:38 #: src/pages/providers/saml/SAMLProviderImportForm.ts:38
#: src/pages/providers/saml/SAMLProviderViewPage.ts:66 #: src/pages/providers/saml/SAMLProviderViewPage.ts:66
#: src/pages/sources/SourcesListPage.ts:51 #: src/pages/sources/SourcesListPage.ts:52
#: src/pages/sources/ldap/LDAPSourceForm.ts:54 #: src/pages/sources/ldap/LDAPSourceForm.ts:54
#: src/pages/sources/ldap/LDAPSourceViewPage.ts:64 #: src/pages/sources/ldap/LDAPSourceViewPage.ts:64
#: src/pages/sources/oauth/OAuthSourceForm.ts:100 #: src/pages/sources/oauth/OAuthSourceForm.ts:100
#: src/pages/sources/oauth/OAuthSourceViewPage.ts:64 #: src/pages/sources/oauth/OAuthSourceViewPage.ts:64
#: src/pages/sources/plex/PlexSourceForm.ts:87
#: src/pages/sources/saml/SAMLSourceForm.ts:54 #: src/pages/sources/saml/SAMLSourceForm.ts:54
#: src/pages/sources/saml/SAMLSourceViewPage.ts:66 #: src/pages/sources/saml/SAMLSourceViewPage.ts:66
#: src/pages/stages/StageListPage.ts:65 #: src/pages/stages/StageListPage.ts:65
@ -1957,7 +1980,7 @@ msgstr "NameID Policy"
msgid "NameID Property Mapping" msgid "NameID Property Mapping"
msgstr "NameID Property Mapping" msgstr "NameID Property Mapping"
#: src/flows/stages/identification/IdentificationStage.ts:119 #: src/flows/stages/identification/IdentificationStage.ts:127
msgid "Need an account?" msgid "Need an account?"
msgstr "Need an account?" msgstr "Need an account?"
@ -2348,7 +2371,7 @@ msgstr "Post binding"
msgid "Post binding (auto-submit)" msgid "Post binding (auto-submit)"
msgstr "Post binding (auto-submit)" msgstr "Post binding (auto-submit)"
#: src/flows/FlowExecutor.ts:255 #: src/flows/FlowExecutor.ts:258
msgid "Powered by authentik" msgid "Powered by authentik"
msgstr "Powered by authentik" msgstr "Powered by authentik"
@ -2412,6 +2435,7 @@ msgstr "Property mappings used to user creation."
#: src/pages/providers/proxy/ProxyProviderForm.ts:123 #: src/pages/providers/proxy/ProxyProviderForm.ts:123
#: src/pages/providers/saml/SAMLProviderForm.ts:78 #: src/pages/providers/saml/SAMLProviderForm.ts:78
#: src/pages/sources/oauth/OAuthSourceForm.ts:122 #: src/pages/sources/oauth/OAuthSourceForm.ts:122
#: src/pages/sources/plex/PlexSourceForm.ts:109
#: src/pages/sources/saml/SAMLSourceForm.ts:76 #: src/pages/sources/saml/SAMLSourceForm.ts:76
msgid "Protocol settings" msgid "Protocol settings"
msgstr "Protocol settings" msgstr "Protocol settings"
@ -2602,7 +2626,7 @@ msgstr "Retry Task"
msgid "Retry authentication" msgid "Retry authentication"
msgstr "Retry authentication" msgstr "Retry authentication"
#: src/flows/FlowExecutor.ts:145 #: src/flows/FlowExecutor.ts:146
msgid "Return" msgid "Return"
msgstr "Return" msgstr "Return"
@ -2710,7 +2734,7 @@ msgstr "Select all rows"
msgid "Select an identification method." msgid "Select an identification method."
msgstr "Select an identification method." msgstr "Select an identification method."
#: src/flows/stages/identification/IdentificationStage.ts:134 #: src/flows/stages/identification/IdentificationStage.ts:142
msgid "Select one of the sources below to login." msgid "Select one of the sources below to login."
msgstr "Select one of the sources below to login." msgstr "Select one of the sources below to login."
@ -2722,6 +2746,10 @@ msgstr "Select users to add"
msgid "Select which scopes can be used by the client. The client stil has to specify the scope to access the data." msgid "Select which scopes can be used by the client. The client stil has to specify the scope to access the data."
msgstr "Select which scopes can be used by the client. The client stil has to specify the scope to access the data." msgstr "Select which scopes can be used by the client. The client stil has to specify the scope to access the data."
#: src/pages/sources/plex/PlexSourceForm.ts:131
msgid "Select which server a user has to be a member of to be allowed to authenticate."
msgstr "Select which server a user has to be a member of to be allowed to authenticate."
#: src/pages/events/RuleForm.ts:92 #: src/pages/events/RuleForm.ts:92
msgid "Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI." msgid "Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI."
msgstr "Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI." msgstr "Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI."
@ -2820,7 +2848,7 @@ msgstr "Show matched user"
msgid "Shown as the Title in Flow pages." msgid "Shown as the Title in Flow pages."
msgstr "Shown as the Title in Flow pages." msgstr "Shown as the Title in Flow pages."
#: src/flows/stages/identification/IdentificationStage.ts:120 #: src/flows/stages/identification/IdentificationStage.ts:128
msgid "Sign up." msgid "Sign up."
msgstr "Sign up." msgstr "Sign up."
@ -2854,16 +2882,17 @@ msgstr "Skip path regex"
#: src/pages/flows/FlowForm.ts:94 #: src/pages/flows/FlowForm.ts:94
#: src/pages/sources/ldap/LDAPSourceForm.ts:60 #: src/pages/sources/ldap/LDAPSourceForm.ts:60
#: src/pages/sources/oauth/OAuthSourceForm.ts:106 #: src/pages/sources/oauth/OAuthSourceForm.ts:106
#: src/pages/sources/plex/PlexSourceForm.ts:93
#: src/pages/sources/saml/SAMLSourceForm.ts:60 #: src/pages/sources/saml/SAMLSourceForm.ts:60
msgid "Slug" msgid "Slug"
msgstr "Slug" msgstr "Slug"
#: src/flows/FlowExecutor.ts:138 #: src/flows/FlowExecutor.ts:139
msgid "Something went wrong! Please try again later." msgid "Something went wrong! Please try again later."
msgstr "Something went wrong! Please try again later." msgstr "Something went wrong! Please try again later."
#: src/pages/providers/ProviderListPage.ts:91 #: src/pages/providers/ProviderListPage.ts:91
#: src/pages/sources/SourcesListPage.ts:87 #: src/pages/sources/SourcesListPage.ts:88
msgid "Source" msgid "Source"
msgstr "Source" msgstr "Source"
@ -2872,11 +2901,11 @@ msgid "Source {0}"
msgstr "Source {0}" msgstr "Source {0}"
#: src/interfaces/AdminInterface.ts:20 #: src/interfaces/AdminInterface.ts:20
#: src/pages/sources/SourcesListPage.ts:30 #: src/pages/sources/SourcesListPage.ts:31
msgid "Sources" msgid "Sources"
msgstr "Sources" msgstr "Sources"
#: src/pages/sources/SourcesListPage.ts:33 #: src/pages/sources/SourcesListPage.ts:34
msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins"
msgstr "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" msgstr "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins"
@ -3073,6 +3102,7 @@ msgstr "Successfully created service-connection."
#: src/pages/sources/ldap/LDAPSourceForm.ts:47 #: src/pages/sources/ldap/LDAPSourceForm.ts:47
#: src/pages/sources/oauth/OAuthSourceForm.ts:51 #: src/pages/sources/oauth/OAuthSourceForm.ts:51
#: src/pages/sources/plex/PlexSourceForm.ts:60
#: src/pages/sources/saml/SAMLSourceForm.ts:47 #: src/pages/sources/saml/SAMLSourceForm.ts:47
msgid "Successfully created source." msgid "Successfully created source."
msgstr "Successfully created source." msgstr "Successfully created source."
@ -3209,6 +3239,7 @@ msgstr "Successfully updated service-connection."
#: src/pages/sources/ldap/LDAPSourceForm.ts:44 #: src/pages/sources/ldap/LDAPSourceForm.ts:44
#: src/pages/sources/oauth/OAuthSourceForm.ts:48 #: src/pages/sources/oauth/OAuthSourceForm.ts:48
#: src/pages/sources/plex/PlexSourceForm.ts:57
#: src/pages/sources/saml/SAMLSourceForm.ts:44 #: src/pages/sources/saml/SAMLSourceForm.ts:44
msgid "Successfully updated source." msgid "Successfully updated source."
msgstr "Successfully updated source." msgstr "Successfully updated source."
@ -3464,7 +3495,7 @@ msgstr "Transports"
#: src/pages/policies/PolicyListPage.ts:57 #: src/pages/policies/PolicyListPage.ts:57
#: src/pages/property-mappings/PropertyMappingListPage.ts:55 #: src/pages/property-mappings/PropertyMappingListPage.ts:55
#: src/pages/providers/ProviderListPage.ts:54 #: src/pages/providers/ProviderListPage.ts:54
#: src/pages/sources/SourcesListPage.ts:52 #: src/pages/sources/SourcesListPage.ts:53
#: src/pages/stages/prompt/PromptForm.ts:97 #: src/pages/stages/prompt/PromptForm.ts:97
#: src/pages/stages/prompt/PromptListPage.ts:48 #: src/pages/stages/prompt/PromptListPage.ts:48
msgid "Type" msgid "Type"
@ -3543,7 +3574,7 @@ msgstr "Up-to-date!"
#: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:118 #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:118
#: src/pages/providers/proxy/ProxyProviderViewPage.ts:117 #: src/pages/providers/proxy/ProxyProviderViewPage.ts:117
#: src/pages/providers/saml/SAMLProviderViewPage.ts:111 #: src/pages/providers/saml/SAMLProviderViewPage.ts:111
#: src/pages/sources/SourcesListPage.ts:69 #: src/pages/sources/SourcesListPage.ts:70
#: src/pages/sources/ldap/LDAPSourceViewPage.ts:95 #: src/pages/sources/ldap/LDAPSourceViewPage.ts:95
#: src/pages/sources/oauth/OAuthSourceViewPage.ts:115 #: src/pages/sources/oauth/OAuthSourceViewPage.ts:115
#: src/pages/sources/saml/SAMLSourceViewPage.ts:101 #: src/pages/sources/saml/SAMLSourceViewPage.ts:101
@ -3646,7 +3677,7 @@ msgstr "Update details"
#: src/pages/policies/PolicyListPage.ts:80 #: src/pages/policies/PolicyListPage.ts:80
#: src/pages/property-mappings/PropertyMappingListPage.ts:69 #: src/pages/property-mappings/PropertyMappingListPage.ts:69
#: src/pages/providers/ProviderListPage.ts:76 #: src/pages/providers/ProviderListPage.ts:76
#: src/pages/sources/SourcesListPage.ts:72 #: src/pages/sources/SourcesListPage.ts:73
#: src/pages/stages/StageListPage.ts:88 #: src/pages/stages/StageListPage.ts:88
#: src/pages/users/UserActiveForm.ts:41 #: src/pages/users/UserActiveForm.ts:41
msgid "Update {0}" msgid "Update {0}"
@ -3750,7 +3781,7 @@ msgstr "User/Group Attribute used for the user part of the HTTP-Basic Header. If
msgid "Userinfo URL" msgid "Userinfo URL"
msgstr "Userinfo URL" msgstr "Userinfo URL"
#: src/flows/stages/identification/IdentificationStage.ts:142 #: src/flows/stages/identification/IdentificationStage.ts:150
#: src/pages/stages/identification/IdentificationStageForm.ts:78 #: src/pages/stages/identification/IdentificationStageForm.ts:78
#: src/pages/user-settings/UserDetailsPage.ts:57 #: src/pages/user-settings/UserDetailsPage.ts:57
#: src/pages/users/UserForm.ts:47 #: src/pages/users/UserForm.ts:47
@ -3903,7 +3934,7 @@ msgstr "When selected, incoming assertion's Signatures will be validated against
msgid "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged." msgid "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged."
msgstr "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged." msgstr "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged."
#: src/flows/FlowExecutor.ts:134 #: src/flows/FlowExecutor.ts:135
msgid "Whoops!" msgid "Whoops!"
msgstr "Whoops!" msgstr "Whoops!"

View File

@ -156,6 +156,10 @@ msgstr ""
msgid "Allowed count" msgid "Allowed count"
msgstr "" msgstr ""
#: src/pages/sources/plex/PlexSourceForm.ts:119
msgid "Allowed servers"
msgstr ""
#: src/pages/sources/saml/SAMLSourceForm.ts:144 #: src/pages/sources/saml/SAMLSourceForm.ts:144
msgid "Allows authentication flows initiated by the IdP. This can be a security risk, as no validation of the request ID is done." msgid "Allows authentication flows initiated by the IdP. This can be a security risk, as no validation of the request ID is done."
msgstr "" msgstr ""
@ -273,11 +277,16 @@ msgstr ""
msgid "Audience" msgid "Audience"
msgstr "" msgstr ""
#: src/flows/sources/plex/PlexLoginInit.ts:56
msgid "Authenticating with Plex..."
msgstr ""
#: src/pages/flows/FlowForm.ts:55 #: src/pages/flows/FlowForm.ts:55
msgid "Authentication" msgid "Authentication"
msgstr "" msgstr ""
#: src/pages/sources/oauth/OAuthSourceForm.ts:189 #: src/pages/sources/oauth/OAuthSourceForm.ts:189
#: src/pages/sources/plex/PlexSourceForm.ts:149
#: src/pages/sources/saml/SAMLSourceForm.ts:245 #: src/pages/sources/saml/SAMLSourceForm.ts:245
msgid "Authentication flow" msgid "Authentication flow"
msgstr "" msgstr ""
@ -391,8 +400,8 @@ msgstr ""
msgid "Build hash: {0}" msgid "Build hash: {0}"
msgstr "" msgstr ""
#: src/pages/sources/SourcesListPage.ts:103 #: src/pages/sources/SourcesListPage.ts:104
#: src/pages/sources/SourcesListPage.ts:105 #: src/pages/sources/SourcesListPage.ts:106
msgid "Built-in" msgid "Built-in"
msgstr "" msgstr ""
@ -538,6 +547,7 @@ msgstr ""
#: src/pages/providers/oauth2/OAuth2ProviderForm.ts:107 #: src/pages/providers/oauth2/OAuth2ProviderForm.ts:107
#: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:99 #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:99
#: src/pages/sources/plex/PlexSourceForm.ts:113
msgid "Client ID" msgid "Client ID"
msgstr "" msgstr ""
@ -738,8 +748,8 @@ msgstr ""
#: src/pages/providers/ProviderListPage.ts:116 #: src/pages/providers/ProviderListPage.ts:116
#: src/pages/providers/RelatedApplicationButton.ts:27 #: src/pages/providers/RelatedApplicationButton.ts:27
#: src/pages/providers/RelatedApplicationButton.ts:35 #: src/pages/providers/RelatedApplicationButton.ts:35
#: src/pages/sources/SourcesListPage.ts:113 #: src/pages/sources/SourcesListPage.ts:114
#: src/pages/sources/SourcesListPage.ts:122 #: src/pages/sources/SourcesListPage.ts:123
#: src/pages/stages/StageListPage.ts:119 #: src/pages/stages/StageListPage.ts:119
#: src/pages/stages/StageListPage.ts:128 #: src/pages/stages/StageListPage.ts:128
#: src/pages/stages/invitation/InvitationListPage.ts:77 #: src/pages/stages/invitation/InvitationListPage.ts:77
@ -836,7 +846,7 @@ msgstr ""
#: src/pages/policies/PolicyListPage.ts:136 #: src/pages/policies/PolicyListPage.ts:136
#: src/pages/property-mappings/PropertyMappingListPage.ts:125 #: src/pages/property-mappings/PropertyMappingListPage.ts:125
#: src/pages/providers/ProviderListPage.ts:119 #: src/pages/providers/ProviderListPage.ts:119
#: src/pages/sources/SourcesListPage.ts:125 #: src/pages/sources/SourcesListPage.ts:126
#: src/pages/stages/StageListPage.ts:131 #: src/pages/stages/StageListPage.ts:131
msgid "Create {0}" msgid "Create {0}"
msgstr "" msgstr ""
@ -892,7 +902,7 @@ msgstr ""
#: src/pages/policies/PolicyListPage.ts:115 #: src/pages/policies/PolicyListPage.ts:115
#: src/pages/property-mappings/PropertyMappingListPage.ts:104 #: src/pages/property-mappings/PropertyMappingListPage.ts:104
#: src/pages/providers/ProviderListPage.ts:98 #: src/pages/providers/ProviderListPage.ts:98
#: src/pages/sources/SourcesListPage.ts:94 #: src/pages/sources/SourcesListPage.ts:95
#: src/pages/stages/StageListPage.ts:110 #: src/pages/stages/StageListPage.ts:110
#: src/pages/stages/invitation/InvitationListPage.ts:68 #: src/pages/stages/invitation/InvitationListPage.ts:68
#: src/pages/stages/prompt/PromptListPage.ts:87 #: src/pages/stages/prompt/PromptListPage.ts:87
@ -1000,7 +1010,7 @@ msgstr ""
msgid "Disable Time-based OTP" msgid "Disable Time-based OTP"
msgstr "" msgstr ""
#: src/pages/sources/SourcesListPage.ts:63 #: src/pages/sources/SourcesListPage.ts:64
msgid "Disabled" msgid "Disabled"
msgstr "" msgstr ""
@ -1041,7 +1051,7 @@ msgstr ""
#: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:128 #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:128
#: src/pages/providers/proxy/ProxyProviderViewPage.ts:127 #: src/pages/providers/proxy/ProxyProviderViewPage.ts:127
#: src/pages/providers/saml/SAMLProviderViewPage.ts:121 #: src/pages/providers/saml/SAMLProviderViewPage.ts:121
#: src/pages/sources/SourcesListPage.ts:82 #: src/pages/sources/SourcesListPage.ts:83
#: src/pages/sources/ldap/LDAPSourceViewPage.ts:105 #: src/pages/sources/ldap/LDAPSourceViewPage.ts:105
#: src/pages/sources/oauth/OAuthSourceViewPage.ts:125 #: src/pages/sources/oauth/OAuthSourceViewPage.ts:125
#: src/pages/sources/saml/SAMLSourceViewPage.ts:111 #: src/pages/sources/saml/SAMLSourceViewPage.ts:111
@ -1078,7 +1088,7 @@ msgstr ""
msgid "Either no applications are defined, or you don't have access to any." msgid "Either no applications are defined, or you don't have access to any."
msgstr "" msgstr ""
#: src/flows/stages/identification/IdentificationStage.ts:138 #: src/flows/stages/identification/IdentificationStage.ts:146
#: src/pages/events/TransportForm.ts:46 #: src/pages/events/TransportForm.ts:46
#: src/pages/stages/identification/IdentificationStageForm.ts:81 #: src/pages/stages/identification/IdentificationStageForm.ts:81
#: src/pages/user-settings/UserDetailsPage.ts:71 #: src/pages/user-settings/UserDetailsPage.ts:71
@ -1091,7 +1101,7 @@ msgstr ""
msgid "Email address" msgid "Email address"
msgstr "" msgstr ""
#: src/flows/stages/identification/IdentificationStage.ts:145 #: src/flows/stages/identification/IdentificationStage.ts:153
msgid "Email or username" msgid "Email or username"
msgstr "" msgstr ""
@ -1128,6 +1138,7 @@ msgstr ""
#: src/pages/policies/PolicyBindingForm.ts:199 #: src/pages/policies/PolicyBindingForm.ts:199
#: src/pages/sources/ldap/LDAPSourceForm.ts:69 #: src/pages/sources/ldap/LDAPSourceForm.ts:69
#: src/pages/sources/oauth/OAuthSourceForm.ts:115 #: src/pages/sources/oauth/OAuthSourceForm.ts:115
#: src/pages/sources/plex/PlexSourceForm.ts:102
#: src/pages/sources/saml/SAMLSourceForm.ts:69 #: src/pages/sources/saml/SAMLSourceForm.ts:69
msgid "Enabled" msgid "Enabled"
msgstr "" msgstr ""
@ -1137,6 +1148,7 @@ msgid "Enrollment"
msgstr "" msgstr ""
#: src/pages/sources/oauth/OAuthSourceForm.ts:210 #: src/pages/sources/oauth/OAuthSourceForm.ts:210
#: src/pages/sources/plex/PlexSourceForm.ts:170
#: src/pages/sources/saml/SAMLSourceForm.ts:266 #: src/pages/sources/saml/SAMLSourceForm.ts:266
#: src/pages/stages/identification/IdentificationStageForm.ts:106 #: src/pages/stages/identification/IdentificationStageForm.ts:106
msgid "Enrollment flow" msgid "Enrollment flow"
@ -1349,16 +1361,19 @@ msgid "Flow Overview"
msgstr "" msgstr ""
#: src/pages/sources/oauth/OAuthSourceForm.ts:185 #: src/pages/sources/oauth/OAuthSourceForm.ts:185
#: src/pages/sources/plex/PlexSourceForm.ts:145
#: src/pages/sources/saml/SAMLSourceForm.ts:220 #: src/pages/sources/saml/SAMLSourceForm.ts:220
msgid "Flow settings" msgid "Flow settings"
msgstr "" msgstr ""
#: src/pages/sources/oauth/OAuthSourceForm.ts:207 #: src/pages/sources/oauth/OAuthSourceForm.ts:207
#: src/pages/sources/plex/PlexSourceForm.ts:167
#: src/pages/sources/saml/SAMLSourceForm.ts:263 #: src/pages/sources/saml/SAMLSourceForm.ts:263
msgid "Flow to use when authenticating existing users." msgid "Flow to use when authenticating existing users."
msgstr "" msgstr ""
#: src/pages/sources/oauth/OAuthSourceForm.ts:228 #: src/pages/sources/oauth/OAuthSourceForm.ts:228
#: src/pages/sources/plex/PlexSourceForm.ts:188
#: src/pages/sources/saml/SAMLSourceForm.ts:284 #: src/pages/sources/saml/SAMLSourceForm.ts:284
msgid "Flow to use when enrolling new users." msgid "Flow to use when enrolling new users."
msgstr "" msgstr ""
@ -1402,7 +1417,7 @@ msgstr ""
msgid "Forgot password?" msgid "Forgot password?"
msgstr "" msgstr ""
#: src/flows/stages/identification/IdentificationStage.ts:124 #: src/flows/stages/identification/IdentificationStage.ts:132
msgid "Forgot username or password?" msgid "Forgot username or password?"
msgstr "" msgstr ""
@ -1502,6 +1517,7 @@ msgstr ""
#: src/pages/providers/saml/SAMLProviderForm.ts:177 #: src/pages/providers/saml/SAMLProviderForm.ts:177
#: src/pages/sources/ldap/LDAPSourceForm.ts:167 #: src/pages/sources/ldap/LDAPSourceForm.ts:167
#: src/pages/sources/ldap/LDAPSourceForm.ts:193 #: src/pages/sources/ldap/LDAPSourceForm.ts:193
#: src/pages/sources/plex/PlexSourceForm.ts:132
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts:114 #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts:114
#: src/pages/stages/identification/IdentificationStageForm.ts:85 #: src/pages/stages/identification/IdentificationStageForm.ts:85
#: src/pages/stages/password/PasswordStageForm.ts:86 #: src/pages/stages/password/PasswordStageForm.ts:86
@ -1684,9 +1700,13 @@ msgstr ""
msgid "Library" msgid "Library"
msgstr "" msgstr ""
#: src/pages/sources/plex/PlexSourceForm.ts:137
msgid "Load servers"
msgstr ""
#: src/elements/table/Table.ts:120 #: src/elements/table/Table.ts:120
#: src/flows/FlowExecutor.ts:167 #: src/flows/FlowExecutor.ts:168
#: src/flows/FlowExecutor.ts:213 #: src/flows/FlowExecutor.ts:216
#: src/flows/access_denied/FlowAccessDenied.ts:27 #: src/flows/access_denied/FlowAccessDenied.ts:27
#: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts:43 #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts:43
#: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts:33 #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts:33
@ -1697,7 +1717,7 @@ msgstr ""
#: src/flows/stages/consent/ConsentStage.ts:28 #: src/flows/stages/consent/ConsentStage.ts:28
#: src/flows/stages/dummy/DummyStage.ts:27 #: src/flows/stages/dummy/DummyStage.ts:27
#: src/flows/stages/email/EmailStage.ts:26 #: src/flows/stages/email/EmailStage.ts:26
#: src/flows/stages/identification/IdentificationStage.ts:171 #: src/flows/stages/identification/IdentificationStage.ts:179
#: src/flows/stages/password/PasswordStage.ts:31 #: src/flows/stages/password/PasswordStage.ts:31
#: src/flows/stages/prompt/PromptStage.ts:126 #: src/flows/stages/prompt/PromptStage.ts:126
#: src/pages/applications/ApplicationViewPage.ts:43 #: src/pages/applications/ApplicationViewPage.ts:43
@ -1742,6 +1762,8 @@ msgstr ""
#: src/pages/sources/oauth/OAuthSourceForm.ts:177 #: src/pages/sources/oauth/OAuthSourceForm.ts:177
#: src/pages/sources/oauth/OAuthSourceForm.ts:205 #: src/pages/sources/oauth/OAuthSourceForm.ts:205
#: src/pages/sources/oauth/OAuthSourceForm.ts:226 #: src/pages/sources/oauth/OAuthSourceForm.ts:226
#: src/pages/sources/plex/PlexSourceForm.ts:165
#: src/pages/sources/plex/PlexSourceForm.ts:186
#: src/pages/sources/saml/SAMLSourceForm.ts:126 #: src/pages/sources/saml/SAMLSourceForm.ts:126
#: src/pages/sources/saml/SAMLSourceForm.ts:240 #: src/pages/sources/saml/SAMLSourceForm.ts:240
#: src/pages/sources/saml/SAMLSourceForm.ts:261 #: src/pages/sources/saml/SAMLSourceForm.ts:261
@ -1772,7 +1794,7 @@ msgstr ""
msgid "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP." msgid "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP."
msgstr "" msgstr ""
#: src/flows/stages/identification/IdentificationStage.ts:183 #: src/flows/stages/identification/IdentificationStage.ts:191
msgid "Login to continue to {0}." msgid "Login to continue to {0}."
msgstr "" msgstr ""
@ -1905,11 +1927,12 @@ msgstr ""
#: src/pages/providers/saml/SAMLProviderForm.ts:53 #: src/pages/providers/saml/SAMLProviderForm.ts:53
#: src/pages/providers/saml/SAMLProviderImportForm.ts:38 #: src/pages/providers/saml/SAMLProviderImportForm.ts:38
#: src/pages/providers/saml/SAMLProviderViewPage.ts:66 #: src/pages/providers/saml/SAMLProviderViewPage.ts:66
#: src/pages/sources/SourcesListPage.ts:51 #: src/pages/sources/SourcesListPage.ts:52
#: src/pages/sources/ldap/LDAPSourceForm.ts:54 #: src/pages/sources/ldap/LDAPSourceForm.ts:54
#: src/pages/sources/ldap/LDAPSourceViewPage.ts:64 #: src/pages/sources/ldap/LDAPSourceViewPage.ts:64
#: src/pages/sources/oauth/OAuthSourceForm.ts:100 #: src/pages/sources/oauth/OAuthSourceForm.ts:100
#: src/pages/sources/oauth/OAuthSourceViewPage.ts:64 #: src/pages/sources/oauth/OAuthSourceViewPage.ts:64
#: src/pages/sources/plex/PlexSourceForm.ts:87
#: src/pages/sources/saml/SAMLSourceForm.ts:54 #: src/pages/sources/saml/SAMLSourceForm.ts:54
#: src/pages/sources/saml/SAMLSourceViewPage.ts:66 #: src/pages/sources/saml/SAMLSourceViewPage.ts:66
#: src/pages/stages/StageListPage.ts:65 #: src/pages/stages/StageListPage.ts:65
@ -1949,7 +1972,7 @@ msgstr ""
msgid "NameID Property Mapping" msgid "NameID Property Mapping"
msgstr "" msgstr ""
#: src/flows/stages/identification/IdentificationStage.ts:119 #: src/flows/stages/identification/IdentificationStage.ts:127
msgid "Need an account?" msgid "Need an account?"
msgstr "" msgstr ""
@ -2340,7 +2363,7 @@ msgstr ""
msgid "Post binding (auto-submit)" msgid "Post binding (auto-submit)"
msgstr "" msgstr ""
#: src/flows/FlowExecutor.ts:255 #: src/flows/FlowExecutor.ts:258
msgid "Powered by authentik" msgid "Powered by authentik"
msgstr "" msgstr ""
@ -2404,6 +2427,7 @@ msgstr ""
#: src/pages/providers/proxy/ProxyProviderForm.ts:123 #: src/pages/providers/proxy/ProxyProviderForm.ts:123
#: src/pages/providers/saml/SAMLProviderForm.ts:78 #: src/pages/providers/saml/SAMLProviderForm.ts:78
#: src/pages/sources/oauth/OAuthSourceForm.ts:122 #: src/pages/sources/oauth/OAuthSourceForm.ts:122
#: src/pages/sources/plex/PlexSourceForm.ts:109
#: src/pages/sources/saml/SAMLSourceForm.ts:76 #: src/pages/sources/saml/SAMLSourceForm.ts:76
msgid "Protocol settings" msgid "Protocol settings"
msgstr "" msgstr ""
@ -2594,7 +2618,7 @@ msgstr ""
msgid "Retry authentication" msgid "Retry authentication"
msgstr "" msgstr ""
#: src/flows/FlowExecutor.ts:145 #: src/flows/FlowExecutor.ts:146
msgid "Return" msgid "Return"
msgstr "" msgstr ""
@ -2702,7 +2726,7 @@ msgstr ""
msgid "Select an identification method." msgid "Select an identification method."
msgstr "" msgstr ""
#: src/flows/stages/identification/IdentificationStage.ts:134 #: src/flows/stages/identification/IdentificationStage.ts:142
msgid "Select one of the sources below to login." msgid "Select one of the sources below to login."
msgstr "" msgstr ""
@ -2714,6 +2738,10 @@ msgstr ""
msgid "Select which scopes can be used by the client. The client stil has to specify the scope to access the data." msgid "Select which scopes can be used by the client. The client stil has to specify the scope to access the data."
msgstr "" msgstr ""
#: src/pages/sources/plex/PlexSourceForm.ts:131
msgid "Select which server a user has to be a member of to be allowed to authenticate."
msgstr ""
#: src/pages/events/RuleForm.ts:92 #: src/pages/events/RuleForm.ts:92
msgid "Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI." msgid "Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI."
msgstr "" msgstr ""
@ -2812,7 +2840,7 @@ msgstr ""
msgid "Shown as the Title in Flow pages." msgid "Shown as the Title in Flow pages."
msgstr "" msgstr ""
#: src/flows/stages/identification/IdentificationStage.ts:120 #: src/flows/stages/identification/IdentificationStage.ts:128
msgid "Sign up." msgid "Sign up."
msgstr "" msgstr ""
@ -2846,16 +2874,17 @@ msgstr ""
#: src/pages/flows/FlowForm.ts:94 #: src/pages/flows/FlowForm.ts:94
#: src/pages/sources/ldap/LDAPSourceForm.ts:60 #: src/pages/sources/ldap/LDAPSourceForm.ts:60
#: src/pages/sources/oauth/OAuthSourceForm.ts:106 #: src/pages/sources/oauth/OAuthSourceForm.ts:106
#: src/pages/sources/plex/PlexSourceForm.ts:93
#: src/pages/sources/saml/SAMLSourceForm.ts:60 #: src/pages/sources/saml/SAMLSourceForm.ts:60
msgid "Slug" msgid "Slug"
msgstr "" msgstr ""
#: src/flows/FlowExecutor.ts:138 #: src/flows/FlowExecutor.ts:139
msgid "Something went wrong! Please try again later." msgid "Something went wrong! Please try again later."
msgstr "" msgstr ""
#: src/pages/providers/ProviderListPage.ts:91 #: src/pages/providers/ProviderListPage.ts:91
#: src/pages/sources/SourcesListPage.ts:87 #: src/pages/sources/SourcesListPage.ts:88
msgid "Source" msgid "Source"
msgstr "" msgstr ""
@ -2864,11 +2893,11 @@ msgid "Source {0}"
msgstr "" msgstr ""
#: src/interfaces/AdminInterface.ts:20 #: src/interfaces/AdminInterface.ts:20
#: src/pages/sources/SourcesListPage.ts:30 #: src/pages/sources/SourcesListPage.ts:31
msgid "Sources" msgid "Sources"
msgstr "" msgstr ""
#: src/pages/sources/SourcesListPage.ts:33 #: src/pages/sources/SourcesListPage.ts:34
msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins" msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins"
msgstr "" msgstr ""
@ -3065,6 +3094,7 @@ msgstr ""
#: src/pages/sources/ldap/LDAPSourceForm.ts:47 #: src/pages/sources/ldap/LDAPSourceForm.ts:47
#: src/pages/sources/oauth/OAuthSourceForm.ts:51 #: src/pages/sources/oauth/OAuthSourceForm.ts:51
#: src/pages/sources/plex/PlexSourceForm.ts:60
#: src/pages/sources/saml/SAMLSourceForm.ts:47 #: src/pages/sources/saml/SAMLSourceForm.ts:47
msgid "Successfully created source." msgid "Successfully created source."
msgstr "" msgstr ""
@ -3201,6 +3231,7 @@ msgstr ""
#: src/pages/sources/ldap/LDAPSourceForm.ts:44 #: src/pages/sources/ldap/LDAPSourceForm.ts:44
#: src/pages/sources/oauth/OAuthSourceForm.ts:48 #: src/pages/sources/oauth/OAuthSourceForm.ts:48
#: src/pages/sources/plex/PlexSourceForm.ts:57
#: src/pages/sources/saml/SAMLSourceForm.ts:44 #: src/pages/sources/saml/SAMLSourceForm.ts:44
msgid "Successfully updated source." msgid "Successfully updated source."
msgstr "" msgstr ""
@ -3452,7 +3483,7 @@ msgstr ""
#: src/pages/policies/PolicyListPage.ts:57 #: src/pages/policies/PolicyListPage.ts:57
#: src/pages/property-mappings/PropertyMappingListPage.ts:55 #: src/pages/property-mappings/PropertyMappingListPage.ts:55
#: src/pages/providers/ProviderListPage.ts:54 #: src/pages/providers/ProviderListPage.ts:54
#: src/pages/sources/SourcesListPage.ts:52 #: src/pages/sources/SourcesListPage.ts:53
#: src/pages/stages/prompt/PromptForm.ts:97 #: src/pages/stages/prompt/PromptForm.ts:97
#: src/pages/stages/prompt/PromptListPage.ts:48 #: src/pages/stages/prompt/PromptListPage.ts:48
msgid "Type" msgid "Type"
@ -3531,7 +3562,7 @@ msgstr ""
#: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:118 #: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts:118
#: src/pages/providers/proxy/ProxyProviderViewPage.ts:117 #: src/pages/providers/proxy/ProxyProviderViewPage.ts:117
#: src/pages/providers/saml/SAMLProviderViewPage.ts:111 #: src/pages/providers/saml/SAMLProviderViewPage.ts:111
#: src/pages/sources/SourcesListPage.ts:69 #: src/pages/sources/SourcesListPage.ts:70
#: src/pages/sources/ldap/LDAPSourceViewPage.ts:95 #: src/pages/sources/ldap/LDAPSourceViewPage.ts:95
#: src/pages/sources/oauth/OAuthSourceViewPage.ts:115 #: src/pages/sources/oauth/OAuthSourceViewPage.ts:115
#: src/pages/sources/saml/SAMLSourceViewPage.ts:101 #: src/pages/sources/saml/SAMLSourceViewPage.ts:101
@ -3634,7 +3665,7 @@ msgstr ""
#: src/pages/policies/PolicyListPage.ts:80 #: src/pages/policies/PolicyListPage.ts:80
#: src/pages/property-mappings/PropertyMappingListPage.ts:69 #: src/pages/property-mappings/PropertyMappingListPage.ts:69
#: src/pages/providers/ProviderListPage.ts:76 #: src/pages/providers/ProviderListPage.ts:76
#: src/pages/sources/SourcesListPage.ts:72 #: src/pages/sources/SourcesListPage.ts:73
#: src/pages/stages/StageListPage.ts:88 #: src/pages/stages/StageListPage.ts:88
#: src/pages/users/UserActiveForm.ts:41 #: src/pages/users/UserActiveForm.ts:41
msgid "Update {0}" msgid "Update {0}"
@ -3738,7 +3769,7 @@ msgstr ""
msgid "Userinfo URL" msgid "Userinfo URL"
msgstr "" msgstr ""
#: src/flows/stages/identification/IdentificationStage.ts:142 #: src/flows/stages/identification/IdentificationStage.ts:150
#: src/pages/stages/identification/IdentificationStageForm.ts:78 #: src/pages/stages/identification/IdentificationStageForm.ts:78
#: src/pages/user-settings/UserDetailsPage.ts:57 #: src/pages/user-settings/UserDetailsPage.ts:57
#: src/pages/users/UserForm.ts:47 #: src/pages/users/UserForm.ts:47
@ -3891,7 +3922,7 @@ msgstr ""
msgid "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged." msgid "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged."
msgstr "" msgstr ""
#: src/flows/FlowExecutor.ts:134 #: src/flows/FlowExecutor.ts:135
msgid "Whoops!" msgid "Whoops!"
msgstr "" msgstr ""

View File

@ -17,6 +17,7 @@ import { ifDefined } from "lit-html/directives/if-defined";
import "./ldap/LDAPSourceForm"; import "./ldap/LDAPSourceForm";
import "./saml/SAMLSourceForm"; import "./saml/SAMLSourceForm";
import "./oauth/OAuthSourceForm"; import "./oauth/OAuthSourceForm";
import "./plex/PlexSourceForm";
@customElement("ak-source-list") @customElement("ak-source-list")
export class SourceListPage extends TablePage<Source> { export class SourceListPage extends TablePage<Source> {

View File

@ -0,0 +1,183 @@
import { PlexSource, SourcesApi, FlowsApi, FlowDesignationEnum } from "authentik-api";
import { t } from "@lingui/macro";
import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { Form } from "../../../elements/forms/Form";
import "../../../elements/forms/FormGroup";
import "../../../elements/forms/HorizontalFormElement";
import { ifDefined } from "lit-html/directives/if-defined";
import { until } from "lit-html/directives/until";
import { first, randomString } from "../../../utils";
import { PlexAPIClient, PlexResource, popupCenterScreen} from "../../../flows/sources/plex/API";
@customElement("ak-source-plex-form")
export class PlexSourceForm extends Form<PlexSource> {
set sourceSlug(value: string) {
new SourcesApi(DEFAULT_CONFIG).sourcesPlexRead({
slug: value,
}).then(source => {
this.source = source;
});
}
@property({attribute: false})
source: PlexSource = {
clientId: randomString(40)
} as PlexSource;
@property()
plexToken?: string;
@property({attribute: false})
plexResources?: PlexResource[];
getSuccessMessage(): string {
if (this.source) {
return t`Successfully updated source.`;
} else {
return t`Successfully created source.`;
}
}
send = (data: PlexSource): Promise<PlexSource> => {
if (this.source.slug) {
return new SourcesApi(DEFAULT_CONFIG).sourcesPlexUpdate({
slug: this.source.slug,
data: data
});
} else {
return new SourcesApi(DEFAULT_CONFIG).sourcesPlexCreate({
data: data
});
}
};
async doAuth(): Promise<void> {
const authInfo = await PlexAPIClient.getPin(this.source?.clientId);
const authWindow = popupCenterScreen(authInfo.authUrl, "plex auth", 550, 700);
PlexAPIClient.pinPoll(this.source?.clientId || "", authInfo.pin.id).then(token => {
authWindow?.close();
this.plexToken = token;
this.loadServers();
});
}
async loadServers(): Promise<void> {
if (!this.plexToken) {
return;
}
this.plexResources = await new PlexAPIClient(this.plexToken).getServers();
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal
label=${t`Name`}
?required=${true}
name="name">
<input type="text" value="${ifDefined(this.source?.name)}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Slug`}
?required=${true}
name="slug">
<input type="text" value="${ifDefined(this.source?.slug)}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="enabled">
<div class="pf-c-check">
<input type="checkbox" class="pf-c-check__input" ?checked=${first(this.source?.enabled, true)}>
<label class="pf-c-check__label">
${t`Enabled`}
</label>
</div>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header">
${t`Protocol settings`}
</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${t`Client ID`}
?required=${true}
name="clientId">
<input type="text" value="${first(this.source?.clientId)}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Allowed servers`}
?required=${true}
name="allowedServers">
<select class="pf-c-form-control" multiple>
${this.plexResources?.map(r => {
const selected = Array.from(this.source?.allowedServers || []).some(server => {
return server == r.clientIdentifier;
});
return html`<option value=${r.clientIdentifier} ?selected=${selected}>${r.name}</option>`;
})}
</select>
<p class="pf-c-form__helper-text">${t`Select which server a user has to be a member of to be allowed to authenticate.`}</p>
<p class="pf-c-form__helper-text">${t`Hold control/command to select multiple items.`}</p>
<p class="pf-c-form__helper-text">
<button class="pf-c-button pf-m-primary" type="button" @click=${() => {
this.doAuth();
}}>
${t`Load servers`}
</button>
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">
${t`Flow settings`}
</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${t`Authentication flow`}
?required=${true}
name="authenticationFlow">
<select class="pf-c-form-control">
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
ordering: "pk",
designation: FlowDesignationEnum.Authentication,
}).then(flows => {
return flows.results.map(flow => {
let selected = this.source?.authenticationFlow === flow.pk;
if (!this.source?.pk && !this.source?.authenticationFlow && flow.slug === "default-source-authentication") {
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 to use when authenticating existing users.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Enrollment flow`}
?required=${true}
name="enrollmentFlow">
<select class="pf-c-form-control">
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
ordering: "pk",
designation: FlowDesignationEnum.Enrollment,
}).then(flows => {
return flows.results.map(flow => {
let selected = this.source?.enrollmentFlow === flow.pk;
if (!this.source?.pk && !this.source?.enrollmentFlow && flow.slug === "default-source-enrollment") {
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 to use when enrolling new users.`}</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>`;
}
}