diff --git a/.env.docker b/.env.docker index 27e16d81..ebd182a5 100644 --- a/.env.docker +++ b/.env.docker @@ -26,7 +26,7 @@ SYNCMASTER__AUTH__KEYCLOAK__SERVER_URL=http://keycloak:8080 SYNCMASTER__AUTH__KEYCLOAK__REALM_NAME=manually_created SYNCMASTER__AUTH__KEYCLOAK__CLIENT_ID=manually_created SYNCMASTER__AUTH__KEYCLOAK__CLIENT_SECRET=generated_by_keycloak -SYNCMASTER__AUTH__KEYCLOAK__REDIRECT_URI=http://localhost:8000/auth/callback +SYNCMASTER__AUTH__KEYCLOAK__REDIRECT_URI=http://localhost:3000/auth/callback SYNCMASTER__AUTH__KEYCLOAK__SCOPE=email SYNCMASTER__AUTH__KEYCLOAK__VERIFY_SSL=False diff --git a/.env.local b/.env.local index 8d5a630b..9ec39027 100644 --- a/.env.local +++ b/.env.local @@ -26,7 +26,7 @@ export SYNCMASTER__AUTH__KEYCLOAK__SERVER_URL=http://localhost:8080 export SYNCMASTER__AUTH__KEYCLOAK__REALM_NAME=manually_created export SYNCMASTER__AUTH__KEYCLOAK__CLIENT_ID=manually_created export SYNCMASTER__AUTH__KEYCLOAK__CLIENT_SECRET=generated_by_keycloak -export SYNCMASTER__AUTH__KEYCLOAK__REDIRECT_URI=http://localhost:8000/auth/callback +export SYNCMASTER__AUTH__KEYCLOAK__REDIRECT_URI=http://localhost:3000/auth/callback export SYNCMASTER__AUTH__KEYCLOAK__SCOPE=email export SYNCMASTER__AUTH__KEYCLOAK__VERIFY_SSL=False diff --git a/docs/changelog/next_release/274.improvement.rst b/docs/changelog/next_release/274.improvement.rst new file mode 100644 index 00000000..ae65e58e --- /dev/null +++ b/docs/changelog/next_release/274.improvement.rst @@ -0,0 +1 @@ +Replace 307 redirect to Keycloak auth page with 401 response, due to browser restrictions for redirect + CORS + localhost. diff --git a/docs/reference/server/auth/keycloak/local_installation.rst b/docs/reference/server/auth/keycloak/local_installation.rst index 3d85a5c4..d62e800d 100644 --- a/docs/reference/server/auth/keycloak/local_installation.rst +++ b/docs/reference/server/auth/keycloak/local_installation.rst @@ -73,11 +73,11 @@ Set ``client_authentication`` **ON** to receive client_secret Configure Redirect URI ~~~~~~~~~~~~~~~~~~~~~~ -To configure the redirect URI where the browser will redirect to exchange the code provided from Keycloak for an access token, set the `SYNCMASTER__AUTH__KEYCLOAK__REDIRECT_URI` environment variable. The default value for local development is `http://localhost:8000/auth/callback`. +To configure the redirect URI where the browser will redirect to exchange the code provided from Keycloak for an access token, set the `SYNCMASTER__AUTH__KEYCLOAK__REDIRECT_URI` environment variable. The default value for local development is `http://localhost:3000/auth/callback`. .. code-block:: console - $ export SYNCMASTER__AUTH__KEYCLOAK__REDIRECT_URI=http://localhost:8000/auth/callback + $ export SYNCMASTER__AUTH__KEYCLOAK__REDIRECT_URI=http://localhost:3000/auth/callback Configure the client redirect URI ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -112,7 +112,7 @@ After this you can user `KeycloakAuthProvider` in your application with provided .. code-block:: console $ export SYNCMASTER__AUTH__KEYCLOAK__SERVER_URL=http://keycloak:8080 - $ export SYNCMASTER__AUTH__KEYCLOAK__REDIRECT_URI=http://localhost:8000/auth/callback + $ export SYNCMASTER__AUTH__KEYCLOAK__REDIRECT_URI=http://localhost:3000/auth/callback $ export SYNCMASTER__AUTH__KEYCLOAK__REALM_NAME=fastapi_realm $ export SYNCMASTER__AUTH__KEYCLOAK__CLIENT_ID=fastapi_client $ export SYNCMASTER__AUTH__KEYCLOAK__CLIENT_SECRET=6x6gn8uJdWSBmP8FqbNRSoGdvaoaFeez diff --git a/poetry.lock b/poetry.lock index 82e3efae..da43e870 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "accessible-pygments" @@ -72,7 +72,7 @@ description = "Low-level AMQP client for Python (fork of amqplib)." optional = true python-versions = ">=3.6" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2"}, {file = "amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"}, @@ -239,7 +239,7 @@ description = "An asyncio PostgreSQL driver" optional = true python-versions = ">=3.8.0" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\"" +markers = "extra == \"server\" or extra == \"scheduler\"" files = [ {file = "asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e"}, {file = "asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0"}, @@ -461,7 +461,7 @@ description = "Python multiprocessing fork with improvements and bugfixes" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb"}, {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, @@ -520,7 +520,7 @@ description = "Distributed Task Queue." optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525"}, {file = "celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5"}, @@ -659,7 +659,7 @@ files = [ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] -markers = {main = "platform_python_implementation != \"PyPy\" and (extra == \"worker\" or extra == \"server\") or extra == \"worker\"", test = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} +markers = {main = "(extra == \"worker\" or extra == \"server\") and platform_python_implementation != \"PyPy\" or extra == \"worker\"", test = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} [package.dependencies] pycparser = "*" @@ -790,7 +790,7 @@ files = [ {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, ] -markers = {main = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\""} +markers = {main = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\""} [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -802,7 +802,7 @@ description = "Enables git-like *did-you-mean* feature in click" optional = true python-versions = ">=3.6.2" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, @@ -818,7 +818,7 @@ description = "An extension module for click to enable registering CLI commands optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6"}, {file = "click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261"}, @@ -837,7 +837,7 @@ description = "REPL plugin for Click" optional = true python-versions = ">=3.6" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, @@ -861,7 +861,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "(extra == \"scheduler\" or extra == \"server\" or extra == \"worker\") and platform_system == \"Windows\"", dev = "platform_system == \"Windows\"", docs = "platform_system == \"Windows\" or sys_platform == \"win32\"", test = "sys_platform == \"win32\""} +markers = {main = "(extra == \"server\" or extra == \"worker\" or extra == \"scheduler\") and platform_system == \"Windows\"", dev = "platform_system == \"Windows\"", docs = "platform_system == \"Windows\" or sys_platform == \"win32\"", test = "sys_platform == \"win32\""} [[package]] name = "coloredlogs" @@ -870,7 +870,7 @@ description = "Colored terminal output for Python's logging module" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, @@ -1116,6 +1116,21 @@ files = [ [package.dependencies] packaging = "*" +[[package]] +name = "dirty-equals" +version = "0.9.0" +description = "Doing dirty (but extremely useful) things with equals." +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "dirty_equals-0.9.0-py3-none-any.whl", hash = "sha256:ff4d027f5cfa1b69573af00f7ba9043ea652dbdce3fe5cbe828e478c7346db9c"}, + {file = "dirty_equals-0.9.0.tar.gz", hash = "sha256:17f515970b04ed7900b733c95fd8091f4f85e52f1fb5f268757f25c858eb1f7b"}, +] + +[package.extras] +pydantic = ["pydantic (>=2.4.2)"] + [[package]] name = "distlib" version = "0.4.0" @@ -1485,7 +1500,7 @@ files = [ {file = "greenlet-3.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:aaa7aae1e7f75eaa3ae400ad98f8644bb81e1dc6ba47ce8a93d3f17274e08322"}, {file = "greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365"}, ] -markers = {main = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") and (extra == \"server\" or extra == \"scheduler\" or extra == \"worker\")", dev = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")", test = "platform_python_implementation == \"CPython\""} +markers = {main = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") and (extra == \"server\" or extra == \"worker\" or extra == \"scheduler\")", dev = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")", test = "platform_python_implementation == \"CPython\""} [package.extras] docs = ["Sphinx", "furo"] @@ -1635,7 +1650,7 @@ description = "Human friendly output for text interfaces using Python" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, @@ -1795,7 +1810,7 @@ description = "Messaging library for Python." optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8"}, {file = "kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363"}, @@ -2370,7 +2385,7 @@ description = "Library for building powerful interactive command lines in Python optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, @@ -2532,7 +2547,7 @@ files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] -markers = {main = "platform_python_implementation != \"PyPy\" and (extra == \"worker\" or extra == \"server\") or extra == \"worker\"", test = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} +markers = {main = "(extra == \"worker\" or extra == \"server\") and platform_python_implementation != \"PyPy\" or extra == \"worker\"", test = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} [[package]] name = "pycryptodome" @@ -2731,7 +2746,7 @@ files = [ {file = "pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c"}, {file = "pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180"}, ] -markers = {main = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\""} +markers = {main = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\""} [package.dependencies] pydantic = ">=2.7.0" @@ -2826,7 +2841,7 @@ description = "A python implementation of GNU readline." optional = true python-versions = ">=3.8" groups = ["main"] -markers = "sys_platform == \"win32\" and (extra == \"scheduler\" or extra == \"server\" or extra == \"worker\")" +markers = "sys_platform == \"win32\" and (extra == \"server\" or extra == \"worker\" or extra == \"scheduler\")" files = [ {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, @@ -3024,7 +3039,7 @@ description = "Extensions to the standard Python datetime module" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -3044,7 +3059,7 @@ files = [ {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, ] -markers = {main = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\""} +markers = {main = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\""} [package.extras] cli = ["click (>=5.0)"] @@ -3056,7 +3071,7 @@ description = "JSON Log Formatter for the Python Logging Package" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7"}, {file = "python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84"}, @@ -3123,6 +3138,13 @@ optional = false python-versions = ">=3.8" groups = ["main", "dev", "test"] files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, @@ -3190,7 +3212,7 @@ files = [ {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] -markers = {main = "extra == \"worker\" or extra == \"scheduler\" or extra == \"server\""} +markers = {main = "extra == \"worker\" or extra == \"server\" or extra == \"scheduler\""} [[package]] name = "requests" @@ -3313,7 +3335,7 @@ description = "Python 2 and 3 compatibility utilities" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -3712,7 +3734,7 @@ files = [ {file = "sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc"}, {file = "sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417"}, ] -markers = {main = "extra == \"server\" or extra == \"scheduler\" or extra == \"worker\""} +markers = {main = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\""} [package.dependencies] greenlet = {version = ">=1", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} @@ -3751,7 +3773,7 @@ description = "Various utility functions for SQLAlchemy." optional = true python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "sqlalchemy_utils-0.42.0-py3-none-any.whl", hash = "sha256:c8c0b7f00f4734f6f20e9a4d06b39d79d58c8629cba50924fcaeb20e28eb4f48"}, {file = "sqlalchemy_utils-0.42.0.tar.gz", hash = "sha256:6d1ecd3eed8b941f0faf8a531f5d5cee7cffa2598fcf8163de8c31c7a417a5e0"}, @@ -3934,7 +3956,7 @@ files = [ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, ] -markers = {main = "extra == \"scheduler\" and platform_system == \"Windows\" or extra == \"scheduler\" or extra == \"server\" or extra == \"worker\""} +markers = {main = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\" or extra == \"scheduler\" and platform_system == \"Windows\""} [[package]] name = "tzlocal" @@ -4014,7 +4036,7 @@ description = "Python promises." optional = true python-versions = ">=3.6" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, @@ -4048,7 +4070,7 @@ description = "Measures the displayed width of unicode strings in a terminal" optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"scheduler\" or extra == \"server\" or extra == \"worker\"" +markers = "extra == \"server\" or extra == \"worker\" or extra == \"scheduler\"" files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, @@ -4174,4 +4196,4 @@ worker = ["asgi-correlation-id", "celery", "coloredlogs", "horizon-hwm-store", " [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "75bb4b0d9d68c2ec27dc716b02c9e785584a3428f1ae1689ccbb459b4006515b" +content-hash = "80d81f8b2a1066f4320f51769038d8604c329ecae63af5245721d915e2afb469" diff --git a/pyproject.toml b/pyproject.toml index 3ac81c6f..06b2bff9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,6 +136,7 @@ faker = "^37.4.0" coverage = "^7.9.1" gevent = ">=24.11.1,<26.0.0" responses = "^0.25.7" +dirty-equals = "^0.9.0" [tool.poetry.group.dev.dependencies] mypy = "^1.15.0" diff --git a/syncmaster/server/api/v1/auth.py b/syncmaster/server/api/v1/auth.py index cbea559a..9f3f1b98 100644 --- a/syncmaster/server/api/v1/auth.py +++ b/syncmaster/server/api/v1/auth.py @@ -1,10 +1,9 @@ # SPDX-FileCopyrightText: 2023-2024 MTS PJSC # SPDX-License-Identifier: Apache-2.0 -from http.client import NOT_FOUND +from http.client import NO_CONTENT from typing import Annotated -from fastapi import APIRouter, Depends, HTTPException, Request -from fastapi.responses import RedirectResponse +from fastapi import APIRouter, Depends, Request, Response from fastapi.security import OAuth2PasswordRequestForm from syncmaster.errors.registration import get_error_responses @@ -17,7 +16,6 @@ DummyAuthProvider, KeycloakAuthProvider, ) -from syncmaster.server.utils.state import validate_state router = APIRouter( prefix="/auth", @@ -42,16 +40,12 @@ async def token( return AuthTokenSchema.model_validate(token) -@router.get("/callback") +@router.get("/callback", status_code=NO_CONTENT) async def auth_callback( request: Request, code: str, - state: str, auth_provider: Annotated[KeycloakAuthProvider, Depends(Stub(AuthProvider))], ): - original_redirect_url = validate_state(state) - if not original_redirect_url: - raise HTTPException(status_code=NOT_FOUND, detail="Invalid state parameter") token = await auth_provider.get_token_authorization_code_grant( code=code, redirect_uri=auth_provider.settings.keycloak.redirect_uri, @@ -59,4 +53,4 @@ async def auth_callback( request.session["access_token"] = token["access_token"] request.session["refresh_token"] = token["refresh_token"] - return RedirectResponse(url=original_redirect_url) + return Response(status_code=NO_CONTENT) diff --git a/syncmaster/server/handler.py b/syncmaster/server/handler.py index 62e0e425..b0566537 100644 --- a/syncmaster/server/handler.py +++ b/syncmaster/server/handler.py @@ -5,7 +5,6 @@ from fastapi import HTTPException, Request, Response, status from fastapi.exceptions import RequestValidationError -from fastapi.responses import RedirectResponse from pydantic import ValidationError from syncmaster.errors.base import APIErrorSchema, BaseErrorSchema @@ -113,7 +112,7 @@ async def syncmsater_exception_handler(request: Request, exc: SyncmasterError): return unknown_exception_handler(request, exc) content = response.schema( # type: ignore[call-arg] - message=exc.message if hasattr(exc, "message") else "", + message=getattr(exc, "message", ""), ) if isinstance(exc, AuthDataNotFoundError): content.code = "not_found" @@ -124,7 +123,10 @@ async def syncmsater_exception_handler(request: Request, exc: SyncmasterError): ) if isinstance(exc, RedirectException): - return RedirectResponse(url=exc.redirect_url) + content.code = "unauthorized" + content.message = "Please authorize using provided URL" + content.details = exc.redirect_url + return exception_json_response(status=status.HTTP_401_UNAUTHORIZED, content=content) if isinstance(exc, AuthorizationError): content.code = "unauthorized" diff --git a/syncmaster/server/providers/auth/keycloak_provider.py b/syncmaster/server/providers/auth/keycloak_provider.py index 9ea05a1d..ab31bedb 100644 --- a/syncmaster/server/providers/auth/keycloak_provider.py +++ b/syncmaster/server/providers/auth/keycloak_provider.py @@ -13,7 +13,6 @@ from syncmaster.server.providers.auth.base_provider import AuthProvider from syncmaster.server.services.unit_of_work import UnitOfWork from syncmaster.server.settings.auth.keycloak import KeycloakAuthProviderSettings -from syncmaster.server.utils.state import generate_state log = logging.getLogger(__name__) @@ -137,10 +136,8 @@ async def refresh_access_token(self, refresh_token: str) -> dict[str, Any]: return new_tokens def redirect_to_auth(self, path: str) -> None: - state = generate_state(path) auth_url = self.keycloak_openid.auth_url( redirect_uri=self.settings.keycloak.redirect_uri, scope=self.settings.keycloak.scope, - state=state, ) raise RedirectException(redirect_url=auth_url) diff --git a/syncmaster/server/utils/state.py b/syncmaster/server/utils/state.py deleted file mode 100644 index 536be0a4..00000000 --- a/syncmaster/server/utils/state.py +++ /dev/null @@ -1,15 +0,0 @@ -# SPDX-FileCopyrightText: 2023-2024 MTS PJSC -# SPDX-License-Identifier: Apache-2.0 -import secrets - -state_store: dict[str, str] = {} - - -def generate_state(redirect_url: str) -> str: - state = secrets.token_urlsafe(16) # noqa: WPS432 - state_store[state] = redirect_url - return state - - -def validate_state(state: str) -> str | None: - return state_store.pop(state, None) diff --git a/tests/test_unit/test_auth/test_auth_keycloak.py b/tests/test_unit/test_auth/test_auth_keycloak.py index fde9f480..1cf55470 100644 --- a/tests/test_unit/test_auth/test_auth_keycloak.py +++ b/tests/test_unit/test_auth/test_auth_keycloak.py @@ -2,6 +2,7 @@ import pytest import responses +from dirty_equals import IsStr from httpx import AsyncClient from syncmaster.server.settings import ServerAppSettings as Settings @@ -23,14 +24,18 @@ ], indirect=True, ) -async def test_get_keycloak_user_unauthorized(client: AsyncClient, mock_keycloak_well_known): +async def test_keycloak_get_user_unauthorized(client: AsyncClient, mock_keycloak_well_known): response = await client.get("/v1/users/some_user_id") # redirect unauthorized user to Keycloak - assert response.status_code == 307, response.text - assert "protocol/openid-connect/auth?" in str( - response.next_request.url, - ) + assert response.status_code == 401, response.text + assert response.json() == { + "error": { + "code": "unauthorized", + "message": "Please authorize using provided URL", + "details": IsStr(regex=r".*protocol/openid-connect/auth\?.*"), + }, + } @responses.activate @@ -46,7 +51,7 @@ async def test_get_keycloak_user_unauthorized(client: AsyncClient, mock_keycloak ], indirect=True, ) -async def test_get_keycloak_user_authorized( +async def test_keycloak_get_user_authorized( client: AsyncClient, simple_user: MockUser, settings: Settings, @@ -84,7 +89,7 @@ async def test_get_keycloak_user_authorized( ], indirect=True, ) -async def test_get_keycloak_user_expired_access_token( +async def test_keycloak_get_user_expired_access_token( caplog, client: AsyncClient, simple_user: MockUser, @@ -129,7 +134,7 @@ async def test_get_keycloak_user_expired_access_token( ], indirect=True, ) -async def test_get_keycloak_user_inactive( +async def test_keycloak_get_user_inactive( client: AsyncClient, simple_user: MockUser, inactive_user: MockUser, @@ -155,3 +160,33 @@ async def test_get_keycloak_user_inactive( "details": None, }, } + + +@responses.activate +@pytest.mark.parametrize( + "settings", + [ + { + "auth": { + "provider": KEYCLOAK_PROVIDER, + }, + }, + ], + indirect=True, +) +async def test_keycloak_auth_callback( + client: AsyncClient, + settings: Settings, + mock_keycloak_well_known, + mock_keycloak_realm, + mock_keycloak_token_refresh, + caplog, +): + with caplog.at_level(logging.DEBUG): + response = await client.get( + "/v1/auth/callback", + params={"code": "testcode"}, + ) + + assert response.cookies.get("session"), caplog.text # cookie is set + assert response.status_code == 204, response.json()