├── .coveragerc ├── .gitignore ├── .travis.yml ├── DockerfileDocs ├── LICENSE ├── Makefile ├── README.rst ├── docs ├── Makefile ├── _static │ └── .gitkeep ├── _templates │ └── .gitkeep ├── add-realm.png ├── add-server.png ├── clear_client_tokens.png ├── conf.py ├── index.rst ├── keycloak_manage_clients.png ├── keycloak_remote_resource_management.png ├── keycloak_resources.png ├── keycloak_scopes.png ├── refresh_certificates.png ├── refresh_well_known.png ├── requirements.txt ├── scenario │ ├── example_project.rst │ ├── initial_setup.rst │ ├── local_user_setup.rst │ ├── migrating.rst │ ├── multi_tenancy.rst │ ├── permissions_by_resources_and_scopes.rst │ ├── permissions_by_roles.rst │ └── remote_user_setup.rst ├── synchronize_permissions.png └── synchronize_resources.png ├── example ├── README.md ├── docker-compose.yml ├── keycloak │ └── export │ │ ├── example-realm.json │ │ ├── example-users-0.json │ │ ├── master-realm.json │ │ └── master-users-0.json ├── nginx │ ├── certs │ │ ├── README.md │ │ ├── ca.key │ │ ├── ca.pem │ │ ├── localhost.yarf.nl.cert │ │ ├── localhost.yarf.nl.csr │ │ ├── localhost.yarf.nl.key │ │ └── ssl.ext │ └── conf.d │ │ ├── identity.localhost.yarf.nl.conf │ │ ├── resource-provider-api.localhost.yarf.nl.conf │ │ └── resource-provider.localhost.yarf.nl.conf ├── resource-provider-api │ ├── Dockerfile │ ├── __init__.py │ ├── docker │ │ └── entrypoint-dev.sh │ ├── manage.py │ ├── myapp │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── fixtures │ │ │ ├── 0001_admin_user.py │ │ │ ├── 0002_server.py │ │ │ ├── 0003_realm.py │ │ │ ├── 0004_client.py │ │ │ └── __init__.py │ │ ├── migrations │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── settings.py │ │ ├── urls.py │ │ ├── views.py │ │ └── wsgi.py │ └── requirements.txt └── resource-provider │ ├── Dockerfile │ ├── __init__.py │ ├── docker │ └── entrypoint-dev.sh │ ├── manage.py │ ├── myapp │ ├── __init__.py │ ├── apps.py │ ├── fixtures │ │ ├── 0001_admin_user.py │ │ ├── 0002_server.py │ │ ├── 0003_realm.py │ │ ├── 0004_client.py │ │ └── __init__.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20180322_2059.py │ │ └── __init__.py │ ├── models.py │ ├── settings.py │ ├── templates │ │ ├── 403.html │ │ └── myapp │ │ │ ├── base.html │ │ │ ├── home.html │ │ │ ├── permission.html │ │ │ └── secured.html │ ├── urls.py │ ├── views.py │ └── wsgi.py │ └── requirements.txt ├── pytest.ini ├── setup.cfg ├── setup.py ├── sonar-project.properties └── src └── django_keycloak ├── __init__.py ├── admin ├── __init__.py ├── realm.py └── server.py ├── app_settings.py ├── apps.py ├── auth ├── __init__.py └── backends.py ├── factories.py ├── hashers.py ├── management ├── __init__.py └── commands │ ├── __init__.py │ ├── keycloak_add_user.py │ ├── keycloak_refresh_realm.py │ └── keycloak_sync_resources.py ├── middleware.py ├── migrations ├── 0001_initial.py ├── 0002_auto_20180322_2059.py ├── 0003_auto_20190204_1949.py ├── 0004_client_service_account_profile.py ├── 0005_auto_20190219_2002.py ├── 0006_remove_client_service_account.py └── __init__.py ├── models.py ├── remote_user.py ├── response.py ├── services ├── __init__.py ├── client.py ├── exceptions.py ├── oidc_profile.py ├── permissions.py ├── realm.py ├── remote_client.py ├── uma.py └── users.py ├── templates └── django_keycloak │ ├── includes │ └── session_iframe_support.html │ └── session_iframe.html ├── tests ├── __init__.py ├── backends │ ├── __init__.py │ └── keycloak_authorization_base │ │ ├── __init__.py │ │ ├── test_get_keycloak_permissions.py │ │ └── test_has_perm.py ├── mixins.py ├── services │ ├── __init__.py │ ├── oidc_profile │ │ ├── __init__.py │ │ ├── test_get_active_access_token.py │ │ ├── test_get_entitlement.py │ │ ├── test_get_or_create_from_id_token.py │ │ └── test_update_or_create.py │ └── realm │ │ ├── __init__.py │ │ ├── test_get_realm_api_client.py │ │ └── test_refresh_well_known_oidc.py └── settings.py ├── urls.py └── views.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | */tests/* 4 | src/django_keycloak/factories.py -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | .idea 104 | db.sqlite3 105 | .pytest_cache -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | env: 7 | - DJANGO_VERSION=1.11 8 | - DJANGO_VERSION=2.0 9 | addons: 10 | sonarcloud: 11 | organization: $SC_ORG 12 | token: $SC_TOKEN 13 | 14 | matrix: 15 | exclude: 16 | - python: "3.3" 17 | env: DJANGO_VERSION=1.11 18 | - python: "2.7" 19 | env: DJANGO_VERSION=2.0 20 | before_install: 21 | - pip install -U pip 22 | - pip install -q Django==$DJANGO_VERSION 23 | - pip install -U wheel setuptools flake8 24 | install: 25 | - python setup.py -q install 26 | script: 27 | - flake8 ./src 28 | - python setup.py test --addopts "--cov=django_keycloak --cov-report xml:coverage.xml" 29 | - sonar-scanner 30 | 31 | cache: 32 | directories: 33 | - '$HOME/.sonar/cache' 34 | 35 | after_success: 36 | - pip install codecov 37 | - codecov 38 | -------------------------------------------------------------------------------- /DockerfileDocs: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | # Install required packages 4 | RUN apk --no-cache add \ 5 | py2-pip \ 6 | python-dev \ 7 | make \ 8 | git \ 9 | gcc \ 10 | alpine-sdk 11 | 12 | RUN mkdir -p /src 13 | WORKDIR /src 14 | ADD . /src 15 | 16 | RUN pip install -r docs/requirements.txt 17 | RUN pip install sphinx-autobuild 18 | RUN pip install -e . 19 | 20 | EXPOSE 8050 21 | 22 | CMD sphinx-autobuild --host 0.0.0.0 --port 8050 -z src docs docs/_build/html 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Peter Slump 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install-python: 2 | pip install --upgrade setuptools 3 | pip install -e . 4 | pip install "file://`pwd`#egg=django-keycloak[dev,doc]" 5 | 6 | bump-patch: 7 | bumpversion patch 8 | 9 | bump-minor: 10 | bumpversion minor 11 | 12 | deploy-pypi: clear 13 | python3 -c "import sys; sys.version_info >= (3, 5, 3) or sys.stdout.write('Python version must be greatest then 3.5.2\n') or exit(1)" 14 | python3 setup.py sdist bdist_wheel 15 | twine upload dist/* 16 | 17 | clear: 18 | rm -rf dist/* 19 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Django Keycloak 3 | =============== 4 | 5 | .. image:: https://www.travis-ci.org/Peter-Slump/django-keycloak.svg?branch=master 6 | :target: https://www.travis-ci.org/Peter-Slump/django-keycloak 7 | :alt: Build Status 8 | .. image:: https://readthedocs.org/projects/django-keycloak/badge/?version=latest 9 | :target: http://django-keycloak.readthedocs.io/en/latest/?badge=latest 10 | :alt: Documentation Status 11 | .. image:: https://codecov.io/gh/Peter-Slump/django-keycloak/branch/master/graph/badge.svg 12 | :target: https://codecov.io/gh/Peter-Slump/django-keycloak 13 | :alt: codecov 14 | .. image:: https://api.codeclimate.com/v1/badges/eb19f47dc03dec40cea7/maintainability 15 | :target: https://codeclimate.com/github/Peter-Slump/django-keycloak/maintainability 16 | :alt: Maintainability 17 | 18 | Django app to add Keycloak support to your project. 19 | 20 | `Read documentation `_ 21 | 22 | http://www.keycloak.org/ 23 | 24 | An showcase/demo project is added in the `example folder `_. 25 | 26 | Development 27 | =========== 28 | 29 | Install development environment: 30 | 31 | .. code:: bash 32 | 33 | $ make install-python 34 | 35 | ------------ 36 | Writing docs 37 | ------------ 38 | 39 | Documentation is written using Sphinx and maintained in the docs folder. 40 | 41 | To make it easy to write docs Docker support is available. 42 | 43 | First build the Docker container: 44 | 45 | .. code:: bash 46 | 47 | $ docker build . -f DockerfileDocs -t django-keycloak-docs 48 | 49 | Run the container 50 | 51 | .. code:: bash 52 | 53 | $ docker run -v `pwd`:/src --rm -t -i -p 8050:8050 django-keycloak-docs 54 | 55 | Go in the browser to http://localhost:8050 and view the documentation which get 56 | refreshed and updated on every update in the documentation source. 57 | 58 | -------------- 59 | Create release 60 | -------------- 61 | 62 | .. code:: bash 63 | 64 | $ git checkout master 65 | $ git pull 66 | $ bumpversion release 67 | $ make deploy-pypi 68 | $ bumpversion --no-tag patch 69 | $ git push origin master --tags 70 | 71 | Release Notes 72 | ============= 73 | 74 | **unreleased** 75 | 76 | **v0.1.2-dev** 77 | 78 | **v0.1.1** 79 | 80 | * Added support for remote user. Handling identities without registering a User 81 | model. (thanks to `bossan `_) 82 | * Addes support for permissions using resources and scopes. 83 | (thanks to `bossan `_) 84 | * Added example project. 85 | * Updated documentation. 86 | 87 | **v0.1.0** 88 | 89 | * Correctly extract email field name on UserModel (thanks to `swist `_) 90 | * Add support for Oauth2 Token Exchange to exchange tokens with remote clients. 91 | Handy when using multiple applications with different clients which have to 92 | communicate with each other. 93 | * Support for session iframe -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = DjangoKeycloak 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/_templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/_templates/.gitkeep -------------------------------------------------------------------------------- /docs/add-realm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/add-realm.png -------------------------------------------------------------------------------- /docs/add-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/add-server.png -------------------------------------------------------------------------------- /docs/clear_client_tokens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/clear_client_tokens.png -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/stable/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = u'Django Keycloak' 23 | copyright = u'2018, Peter Slump' 24 | author = u'Peter Slump' 25 | 26 | # The short X.Y version 27 | version = u'' 28 | # The full version, including alpha/beta/rc tags 29 | release = u'0.1.2-dev' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.intersphinx', 44 | ] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # The suffix(es) of source filenames. 50 | # You can specify multiple suffix as a list of string: 51 | # 52 | # source_suffix = ['.rst', '.md'] 53 | source_suffix = '.rst' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | # 61 | # This is also used if you do content translation via gettext catalogs. 62 | # Usually you set "language" from the command line for these cases. 63 | language = None 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | # This pattern also affects html_static_path and html_extra_path . 68 | exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store'] 69 | 70 | # The name of the Pygments (syntax highlighting) style to use. 71 | pygments_style = 'sphinx' 72 | 73 | 74 | # -- Options for HTML output ------------------------------------------------- 75 | 76 | # The theme to use for HTML and HTML Help pages. See the documentation for 77 | # a list of builtin themes. 78 | # 79 | html_theme = 'sphinx_rtd_theme' 80 | 81 | # Theme options are theme-specific and customize the look and feel of a theme 82 | # further. For a list of options available for each theme, see the 83 | # documentation. 84 | # 85 | # html_theme_options = {} 86 | 87 | # Add any paths that contain custom static files (such as style sheets) here, 88 | # relative to this directory. They are copied after the builtin static files, 89 | # so a file named "default.css" will overwrite the builtin "default.css". 90 | html_static_path = ['_static'] 91 | 92 | # Custom sidebar templates, must be a dictionary that maps document names 93 | # to template names. 94 | # 95 | # The default sidebars (for documents that don't match any pattern) are 96 | # defined by theme itself. Builtin themes are using these templates by 97 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 98 | # 'searchbox.html']``. 99 | # 100 | # html_sidebars = {} 101 | 102 | 103 | # -- Options for HTMLHelp output --------------------------------------------- 104 | 105 | # Output file base name for HTML help builder. 106 | htmlhelp_basename = 'DjangoKeycloakdoc' 107 | 108 | 109 | # -- Options for LaTeX output ------------------------------------------------ 110 | 111 | latex_elements = { 112 | # The paper size ('letterpaper' or 'a4paper'). 113 | # 114 | # 'papersize': 'letterpaper', 115 | 116 | # The font size ('10pt', '11pt' or '12pt'). 117 | # 118 | # 'pointsize': '10pt', 119 | 120 | # Additional stuff for the LaTeX preamble. 121 | # 122 | # 'preamble': '', 123 | 124 | # Latex figure (float) alignment 125 | # 126 | # 'figure_align': 'htbp', 127 | } 128 | 129 | # Grouping the document tree into LaTeX files. List of tuples 130 | # (source start file, target name, title, 131 | # author, documentclass [howto, manual, or own class]). 132 | latex_documents = [ 133 | (master_doc, 'DjangoKeycloak.tex', u'Django Keycloak Documentation', 134 | u'Peter Slump', 'manual'), 135 | ] 136 | 137 | 138 | # -- Options for manual page output ------------------------------------------ 139 | 140 | # One entry per manual page. List of tuples 141 | # (source start file, name, description, authors, manual section). 142 | man_pages = [ 143 | (master_doc, 'djangokeycloak', u'Django Keycloak Documentation', 144 | [author], 1) 145 | ] 146 | 147 | 148 | # -- Options for Texinfo output ---------------------------------------------- 149 | 150 | # Grouping the document tree into Texinfo files. List of tuples 151 | # (source start file, target name, title, author, 152 | # dir menu entry, description, category) 153 | texinfo_documents = [ 154 | (master_doc, 'DjangoKeycloak', u'Django Keycloak Documentation', 155 | author, 'DjangoKeycloak', 'One line description of project.', 156 | 'Miscellaneous'), 157 | ] 158 | 159 | 160 | # -- Extension configuration ------------------------------------------------- 161 | 162 | # -- Options for intersphinx extension --------------------------------------- 163 | 164 | # Example configuration for intersphinx: refer to the Python standard library. 165 | intersphinx_mapping = {'https://docs.python.org/': None} -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | =========================================== 2 | Welcome to Django Keycloak's documentation! 3 | =========================================== 4 | 5 | .. toctree:: 6 | :hidden: 7 | :caption: Scenario's 8 | :maxdepth: 2 9 | 10 | scenario/example_project 11 | scenario/local_user_setup 12 | scenario/remote_user_setup 13 | scenario/initial_setup 14 | scenario/migrating 15 | scenario/permissions_by_roles 16 | scenario/permissions_by_resources_and_scopes 17 | scenario/multi_tenancy 18 | 19 | Django Keycloak adds Keycloak support to your Django project. It's build on top 20 | of `Django's authentication system `_. 21 | It works side-by-side with the standard Django authentication implementation and 22 | has tools to migrate your current users and permissions to Keycloak. 23 | 24 | Features 25 | ======== 26 | 27 | - Multi tenancy support 28 | - Permissions by roles or by resource/scope 29 | - Choose if you want to create a local User model for every logged in identity or not. 30 | 31 | Read :ref:`example_project` to quickly test this project. 32 | 33 | .. note:: The documentation and the example project are all based on 34 | Keycloak version 3.4 since that is the latest version which is commercially 35 | supported by Red Hat (SSO). 36 | 37 | Installation 38 | ============ 39 | 40 | Install requirement. 41 | 42 | .. code-block:: bash 43 | 44 | $ pip install git+https://github.com/Peter-Slump/django-keycloak.git 45 | 46 | Setup 47 | ===== 48 | 49 | Some settings are always required and some other settings are dependant on how 50 | you want to integrate Keycloak in your project. 51 | 52 | Add `django-keycloak` to your installed apps, add the authentication back-end, 53 | add the middleware, configure the urls and point to the correct login page. 54 | 55 | .. code-block:: python 56 | 57 | # your-project/settings.py 58 | INSTALLED_APPS = [ 59 | .... 60 | 61 | 'django_keycloak.apps.KeycloakAppConfig' 62 | ] 63 | 64 | MIDDLEWARE = [ 65 | ... 66 | 67 | 'django_keycloak.middleware.BaseKeycloakMiddleware', 68 | ] 69 | 70 | AUTHENTICATION_BACKENDS = [ 71 | ... 72 | 73 | 'django_keycloak.auth.backends.KeycloakAuthorizationCodeBackend', 74 | ] 75 | 76 | LOGIN_URL = 'keycloak_login' 77 | 78 | .. code-block:: python 79 | 80 | # your-project/urls.py 81 | ... 82 | 83 | urlpatterns = [ 84 | ... 85 | 86 | url(r'^keycloak/', include('django_keycloak.urls')), 87 | ] 88 | 89 | 90 | Before you actually start using Django Keycloak make an educated choice between 91 | :ref:`local_user_setup` and :ref:`remote_user_setup`. 92 | 93 | Then walk through the :ref:`initial_setup` to found out how to link your 94 | Keycloak instance to your Django project. 95 | 96 | If you don't want to take all that effort please read about :ref:`example_project` 97 | 98 | Usage 99 | ===== 100 | 101 | For requiring a logged in user you can just use the `standard Django 102 | functionality `_. 103 | This also counts for `enforcing permissions `_. 104 | 105 | This app makes use of the `Python Keycloak client `_ 106 | -------------------------------------------------------------------------------- /docs/keycloak_manage_clients.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/keycloak_manage_clients.png -------------------------------------------------------------------------------- /docs/keycloak_remote_resource_management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/keycloak_remote_resource_management.png -------------------------------------------------------------------------------- /docs/keycloak_resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/keycloak_resources.png -------------------------------------------------------------------------------- /docs/keycloak_scopes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/keycloak_scopes.png -------------------------------------------------------------------------------- /docs/refresh_certificates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/refresh_certificates.png -------------------------------------------------------------------------------- /docs/refresh_well_known.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/refresh_well_known.png -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | docutils 2 | Jinja2 3 | MarkupSafe 4 | Pygments 5 | Sphinx 6 | sphinx_rtd_theme 7 | -------------------------------------------------------------------------------- /docs/scenario/example_project.rst: -------------------------------------------------------------------------------- 1 | .. _example_project: 2 | 3 | ================================ 4 | Testing with the Example project 5 | ================================ 6 | 7 | The quickest way to experiment with this project is by running the example 8 | project. This project is setup using Docker compose. 9 | 10 | Given you have installed Docker and Docker compose run: 11 | 12 | .. code-block:: bash 13 | 14 | $ cd example 15 | $ docker-compose up 16 | 17 | The project exists of a resource provider which mimics a web app and a resource 18 | provider which is only accessible by an API. Next to it is a Keycloak instance 19 | available which is backed by a Postgres database. 20 | 21 | Once you have the containers running you can access it by navigating to: 22 | https://resource-provider.localhost.yarf.nl/ you can login with 23 | username: `testuser` and password: `password`. The admin is 24 | accessible at /admin with username: `admin` and password: `password`. 25 | 26 | The Keycloak instance is available at: https://identity.localhost.yarf.nl/ 27 | the username of the admin user is `admin` and the password is `password`. 28 | 29 | The API is available at: https://resource-provider-api.localhost.yarf.nl/ 30 | You probably don't actually use this server or only for the admin. The admin is 31 | accessible at /admin with username: `admin` and password: `password`. 32 | -------------------------------------------------------------------------------- /docs/scenario/initial_setup.rst: -------------------------------------------------------------------------------- 1 | .. _initial_setup: 2 | 3 | ============= 4 | Initial setup 5 | ============= 6 | 7 | Server configuration 8 | ==================== 9 | 10 | First you have to add your Keycloak server. You can do this in the Django Admin. 11 | 12 | .. image:: /add-server.png 13 | 14 | .. note:: When your application access the Keycloak server using a different url 15 | than the public one you can configure this URL as "internal url". Django 16 | Keycloak will use that url for all direct communication but uses the standard 17 | server url to redirect users for authentication. 18 | 19 | Realm configuration 20 | =================== 21 | 22 | After you have created a 23 | `REALM `_ 24 | and `Client `_ 25 | in Keycloak you can add these in the Django admin. 26 | 27 | .. note:: Django-Keycloak supports multiple realms. However when you configure 28 | multiple realms you have to write your own middleware which selects 29 | the correct realm based on the request. The default middleware always 30 | selects the first realm available in the database. 31 | 32 | .. image:: /add-realm.png 33 | 34 | After you have added the realm please make sure to run te following actions: 35 | 36 | * :ref:`refresh_openid_connect_well_known` 37 | * :ref:`refresh_certificates` 38 | * :ref:`synchronize_permissions` (when using the permission system) 39 | 40 | Tools 41 | ===== 42 | 43 | .. _refresh_openid_connect_well_known: 44 | 45 | ---------------------------------- 46 | Refresh OpenID Connect .well-known 47 | ---------------------------------- 48 | 49 | In the Django Admin you can apply the action "Refresh OpenID Connect 50 | .well-known" for a realm. This retrieves the 51 | `.well-known `_ 52 | content for the OpenID Connect functionality and caches this in the database. In 53 | this way it's not required to fetch this file before each request regarding 54 | OpenID Connect to the Keycloak server. 55 | 56 | .. image:: /refresh_well_known.png 57 | 58 | .. _refresh_certificates: 59 | 60 | -------------------- 61 | Refresh Certificates 62 | -------------------- 63 | 64 | This refreshes the cached certificates from the Keycloak server. These 65 | certificates are used for valiation of the JWT's. 66 | 67 | .. image:: /refresh_certificates.png 68 | 69 | ------------------- 70 | Clear client tokens 71 | ------------------- 72 | 73 | While debugging client service account permissions it's sometimes required to 74 | refresh te session in order to fetch the new permissions. This can be done with 75 | this action in the Django admin. 76 | 77 | .. image:: /clear_client_tokens.png 78 | -------------------------------------------------------------------------------- /docs/scenario/local_user_setup.rst: -------------------------------------------------------------------------------- 1 | .. _local_user_setup: 2 | 3 | ============================ 4 | Setup for local user storage 5 | ============================ 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | By local user storage a 11 | `User object `_ 12 | get created for every logged in identity. This can be handy when you want to 13 | link objects to this User. If that's not the case please read the 14 | scenario :ref:`remote_user_setup`. 15 | 16 | Since **this is the default behaviour for Django Keycloak** you don't have to 17 | configure any setting. 18 | 19 | Important to point out the `KEYCLOAK_OIDC_PROFILE_MODEL` setting. This should 20 | contain `django_keycloak.OpenIdConnectProfile` (which is the case by default). 21 | The model to store the Open ID Connect profile is a swappable model. When 22 | configured to this model a foreign key to the configured Django User model is 23 | available. 24 | 25 | .. code-block:: python 26 | 27 | # settings.py 28 | KEYCLOAK_OIDC_PROFILE_MODEL = 'django_keycloak.OpenIdConnectProfile' -------------------------------------------------------------------------------- /docs/scenario/migrating.rst: -------------------------------------------------------------------------------- 1 | ====================================== 2 | Migrating from Django Auth to Keycloak 3 | ====================================== 4 | 5 | There are some tools available which can help by migrating a running project to 6 | Keycloak. 7 | 8 | -------- 9 | Add user 10 | -------- 11 | 12 | A management command is available to create a Keycloak user based on a local 13 | one. 14 | 15 | .. code:: bash 16 | 17 | $ python manage.py keycloak_add_user --realm --username 18 | 19 | .. note:: In theory it would be possible to synchronize (hashed) passwords to 20 | Keycloak however Keycloak uses a 512 bit hash for pbkdf2_sha256 hashed 21 | passwords, Django generates a 256 bits hash. In that way passwords will not 22 | work when they are copied to Keycloak. The project includes a sha512 hasher 23 | (:class:`django_keycloak.hashers.PBKDF2SHA512PasswordHasher`) which you can 24 | configure to hash passwords in a Keycloak-complient way. 25 | 26 | .. code:: python 27 | 28 | # your-project/settings.py 29 | PASSWORD_HASHERS = [ 30 | 'django_keycloak.hashers.PBKDF2SHA512PasswordHasher', 31 | ] 32 | 33 | 34 | .. _synchronize_permissions: 35 | 36 | -------------------------------------------------------------------------------- /docs/scenario/multi_tenancy.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Multi-tenancy 3 | ============= 4 | 5 | Django Keycloak supports multi tenancy by supporting multiple realms. The way to 6 | determine the currently active realm is set to the request in the middleware. 7 | In the project there is currently one middleware available. This is 8 | `django_keycloak.middleware.BaseKeycloakMiddleware`. This middleware add the 9 | first found Realm model to the request object. 10 | 11 | .. code-block:: python 12 | 13 | # your-project/settings.py 14 | MIDDLEWARE = [ 15 | ... 16 | 17 | 'django_keycloak.middleware.BaseKeycloakMiddleware', 18 | ] 19 | 20 | If you want to support multiple reams you have to create your own middleware. 21 | There are several methods to determine the currently active realm. You can think 22 | of realm determination by: 23 | 24 | - Hostname 25 | - Environment variable 26 | - Selection during login 27 | - etc. 28 | 29 | It's up to you how the realm get determined and therefore it's also up to 30 | you to `write a proper middleware `_ 31 | for it. The only think the middleware has to make the correct Realm model to the 32 | request as `request.realm`. This middleware has to be configured above other 33 | middlewares which have to be configured for authentication purposes. -------------------------------------------------------------------------------- /docs/scenario/permissions_by_resources_and_scopes.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Permissions by resources and scopes 3 | =================================== 4 | 5 | Next to :ref:`permission_by_roles` you can also implement permissions by syncing 6 | Django models as resources in Keycloak and the 7 | `default permissions `_ 8 | in Django as scopes in Keycloak. 9 | 10 | Setup 11 | ===== 12 | 13 | To configure Django Keycloak to make use of the Resource / Scope method of 14 | permission assigning add the following setting: 15 | 16 | .. code-block:: python 17 | 18 | # your-project/settings.py 19 | KEYCLOAK_PERMISSIONS_METHOD = 'resource' 20 | 21 | 22 | Synchronization 23 | =============== 24 | 25 | In Keycloak enable "Remote Resource Management" for the client: 26 | 27 | .. image:: /keycloak_remote_resource_management.png 28 | 29 | You can use the Django Admin action "Synchronize models as Keycloak resources" 30 | to synchronize models and scopes to Keycloak. 31 | 32 | .. image:: /synchronize_resources.png 33 | 34 | An alternative is to run the Django management command `keycloak_sync_resources`: 35 | 36 | .. code-block:: bash 37 | 38 | $ python manage.py keycloak_sync_resources 39 | 40 | Optionally you can supply a client to which the resources should be synchronized. 41 | 42 | Usage 43 | ===== 44 | 45 | After synchronizing you can find the the models as resources and the default permissions as scopes: 46 | 47 | Resources: 48 | 49 | .. image:: /keycloak_resources.png 50 | 51 | Scopes: 52 | 53 | .. image:: /keycloak_scopes.png 54 | 55 | From here you are able to configure your `policies` and `permissions` and assign 56 | them to `users` of `groups` using `roles` in Keycloak. Once assigned you get 57 | them back as permissions in Django where the policies are combined with the 58 | resources just like you are used to in the default Django permission system 59 | i.e. `foo.add_bar` or `foo.change_bar`. 60 | -------------------------------------------------------------------------------- /docs/scenario/permissions_by_roles.rst: -------------------------------------------------------------------------------- 1 | .. _permission_by_roles: 2 | 3 | ==================== 4 | Permissions by roles 5 | ==================== 6 | 7 | There are two ways of using permissions one by roles and the other one by 8 | resources/scopes. The roles method is the default one. In this method the 9 | available client roles are available as permissions in your Django Project. 10 | 11 | .. note:: Please read :ref:`synchronize_permissions` if you want to synchronize 12 | all available permissions in your current project to roles in Keycloak. 13 | 14 | Setup 15 | ===== 16 | 17 | Since this is the default method of handling permission you don't have to 18 | configure anything. However it's good to know that the 19 | `KEYCLOAK_PERMISSIONS_METHOD` is used to configure the way how permissions are 20 | interpreted. 21 | 22 | .. code-block:: python 23 | 24 | # your-project/settings.py 25 | KEYCLOAK_PERMISSIONS_METHOD = 'role' 26 | 27 | 28 | Synchronize 29 | =========== 30 | 31 | This Django Admin action which can be triggered for a realm synchronizes all 32 | available permission to Keycloak. In keycloak the permissions will get 33 | registered as roles. These roles can be added to a user. 34 | 35 | For this feature the service account should have the 36 | realm-management/manage-clients role assigned. 37 | 38 | .. image:: /keycloak_manage_clients.png 39 | 40 | This only makes sense when you use the `roles` permission method. You can read 41 | about this at scenario: :ref:`permission_by_roles`. -------------------------------------------------------------------------------- /docs/scenario/remote_user_setup.rst: -------------------------------------------------------------------------------- 1 | .. _remote_user_setup: 2 | 3 | ===================== 4 | Setup for remote user 5 | ===================== 6 | 7 | It's not required to create a local User object for every logged in identity. 8 | If you don't need a local user object you can setup the app to work with a 9 | remote user. This user behaves like Django's User object but it is not a real 10 | one. 11 | 12 | .. note:: For logging purposes Django admin only works with User objects which 13 | are stored in the database. So you cannot use this method to authenticate 14 | users for admin usage. 15 | 16 | .. warning:: Set the configuration setting below before running the migrations! 17 | 18 | Set the OIDC Profile model to the remote variant: 19 | 20 | .. code-block:: python 21 | 22 | # your-project/settings.py 23 | KEYCLOAK_OIDC_PROFILE_MODEL = 'django_keycloak.RemoteUserOpenIdConnectProfile' 24 | 25 | Configure the remote user middleware: 26 | 27 | .. code-block:: python 28 | 29 | MIDDLEWARE = [ 30 | ... 31 | 32 | 'django_keycloak.middleware.BaseKeycloakMiddleware', 33 | 'django_keycloak.middleware.RemoteUserAuthenticationMiddleware', 34 | ] 35 | 36 | By default the class `django_keycloak.remote_user.KeycloakRemoteUser` is used as 37 | user, this one will be available on the request when authenticated and will be 38 | returned when you access `RemoteUserOpenIdConnectProfile.user`. If you want 39 | another class (i.e. you need extra properties) you can configure this class 40 | using the setting `KEYCLOAK_REMOTE_USER_MODEL`: 41 | 42 | .. code-block:: python 43 | 44 | KEYCLOAK_REMOTE_USER_MODEL = 'django_keycloak.remote_user.KeycloakRemoteUser' -------------------------------------------------------------------------------- /docs/synchronize_permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/synchronize_permissions.png -------------------------------------------------------------------------------- /docs/synchronize_resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/docs/synchronize_resources.png -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example project 2 | 3 | This folder contains an example showcase project for the Django Keycloak project. 4 | 5 | It consists out of three applications: 6 | - identity server (Keycloak) 7 | - resource provider (small web application) 8 | - resource provider (API accessible) 9 | 10 | When running the application everything is pre-configured and ready to use. 11 | 12 | Keycloak version 3.4.3-Final is used since that is the latest version which is 13 | commercially supported by Red Hat (SSO). 14 | 15 | You can find the docs for this version here: https://www.keycloak.org/archive/documentation-3.4.html 16 | 17 | You can find the following features in the project: 18 | 19 | - Authentication (login) 20 | - Authorisation (permissions) 21 | - Token Exchange 22 | 23 | ## Run the project 24 | 25 | Installation of Docker and Docker-compose is required 26 | 27 | Run: 28 | 29 | $ docker-compose up 30 | 31 | In your browser visit: https://resource-provider.localhost.yarf.nl/ 32 | 33 | Accept the insecure certificate, or add the CA which you can find in `nginx/certs/ca.pem` to your trusted CA's. 34 | 35 | ## Credentials 36 | 37 | Keycloak admin user: 38 | 39 | - username: admin 40 | - password: password 41 | 42 | "Test user" in example Realm 43 | 44 | - username: testuser 45 | - password: password 46 | 47 | Django admin user (Resource Provider & Resource Provider API) 48 | 49 | - username: admin 50 | - password: password 51 | 52 | ## Import/Export 53 | 54 | Using the `command` section in the keycloak configuration in the docker-compose file. 55 | Docs: https://www.keycloak.org/docs/latest/server_admin/index.html#_export_import 56 | -------------------------------------------------------------------------------- /example/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | nginx: 6 | image: nginx:stable-alpine 7 | volumes: 8 | - ./nginx/conf.d:/etc/nginx/conf.d/:ro 9 | - ./nginx/certs:/etc/nginx/certs:ro 10 | networks: 11 | default: 12 | aliases: 13 | - resource-provider.localhost.yarf.nl 14 | - resource-provider-api.localhost.yarf.nl 15 | - identity.localhost.yarf.nl 16 | ports: 17 | - "80:80" 18 | - "443:443" 19 | depends_on: 20 | - keycloak 21 | - resource-provider 22 | - resource-provider-api 23 | 24 | keycloak: 25 | image: jboss/keycloak:3.4.3.Final # Pinned to 3.4.3 because this is currently the latest version which has commercial support from Red Hat: https://www.keycloak.org/support.html 26 | command: [ 27 | "-b", "0.0.0.0", 28 | "-Dkeycloak.migration.action=import", # Replace with 'export' in order to export everything 29 | "-Dkeycloak.migration.provider=dir", 30 | "-Dkeycloak.migration.dir=/opt/jboss/keycloak/standalone/configuration/export/", 31 | "-Dkeycloak.migration.strategy=IGNORE_EXISTING" 32 | ] 33 | environment: 34 | - POSTGRES_DATABASE=keycloak 35 | - POSTGRES_USER=keycloak 36 | - POSTGRES_PASSWORD=password 37 | - KEYCLOAK_HOSTNAME=identity.localhost.yarf.nl 38 | # Legacy linking functionality is used 39 | - POSTGRES_PORT_5432_TCP_ADDR=postgres 40 | - POSTGRES_PORT_5432_TCP_PORT=5432 41 | - PROXY_ADDRESS_FORWARDING=true 42 | - KEYCLOAK_LOGLEVEL=DEBUG 43 | # - JAVA_TOOL_OPTIONS=-Dkeycloak.profile.feature.admin_fine_grained_authz=enabled -Dkeycloak.profile.feature.token_exchange=enabled # Required to enable Token exchange feature in newer versions of Keycloak 44 | volumes: 45 | - ./keycloak/export:/opt/jboss/keycloak/standalone/configuration/export 46 | networks: 47 | default: 48 | aliases: 49 | - keycloak 50 | 51 | postgres: 52 | image: postgres:latest 53 | environment: 54 | - POSTGRES_DB=keycloak 55 | - POSTGRES_USER=keycloak 56 | - POSTGRES_PASSWORD=password 57 | networks: 58 | default: 59 | aliases: 60 | - postgres 61 | 62 | resource-provider: 63 | build: ./resource-provider 64 | entrypoint: docker/entrypoint-dev.sh 65 | command: [ "python", "manage.py", "runserver", "0.0.0.0:8001" ] 66 | volumes: 67 | - ./resource-provider:/usr/src/app 68 | - ./../:/usr/src/django-keycloak 69 | - ./../../python-keycloak-client:/usr/src/python-keycloak-client 70 | - ./nginx/certs/ca.pem:/usr/src/ca.pem 71 | networks: 72 | default: 73 | aliases: 74 | - resource-provider 75 | 76 | resource-provider-api: 77 | build: ./resource-provider-api 78 | entrypoint: docker/entrypoint-dev.sh 79 | command: [ "python", "manage.py", "runserver", "0.0.0.0:8002" ] 80 | volumes: 81 | - ./resource-provider-api:/usr/src/app 82 | - ./../:/usr/src/django-keycloak 83 | - ./nginx/certs/ca.pem:/usr/src/ca.pem 84 | networks: 85 | default: 86 | aliases: 87 | - resource-provider-api -------------------------------------------------------------------------------- /example/keycloak/export/example-users-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "realm" : "example", 3 | "users" : [ { 4 | "id" : "a39d3fb5-6e11-47c5-a48e-50e254804f98", 5 | "createdTimestamp" : 1548797565484, 6 | "username" : "service-account-resource-provider", 7 | "enabled" : true, 8 | "totp" : false, 9 | "emailVerified" : false, 10 | "email" : "service-account-resource-provider@placeholder.org", 11 | "serviceAccountClientId" : "resource-provider", 12 | "credentials" : [ ], 13 | "disableableCredentialTypes" : [ ], 14 | "requiredActions" : [ ], 15 | "realmRoles" : [ "offline_access", "uma_authorization" ], 16 | "clientRoles" : { 17 | "resource-provider" : [ "uma_protection" ], 18 | "realm-management" : [ "manage-clients" ], 19 | "account" : [ "manage-account", "view-profile" ] 20 | }, 21 | "notBefore" : 0, 22 | "groups" : [ ] 23 | }, { 24 | "id" : "7ef9e62d-6674-4ca2-9d8c-593ee3519c33", 25 | "createdTimestamp" : 1548797581171, 26 | "username" : "service-account-resource-provider-api", 27 | "enabled" : true, 28 | "totp" : false, 29 | "emailVerified" : false, 30 | "email" : "service-account-resource-provider-api@placeholder.org", 31 | "serviceAccountClientId" : "resource-provider-api", 32 | "credentials" : [ ], 33 | "disableableCredentialTypes" : [ ], 34 | "requiredActions" : [ ], 35 | "realmRoles" : [ "offline_access", "uma_authorization" ], 36 | "clientRoles" : { 37 | "resource-provider-api" : [ "uma_protection" ], 38 | "account" : [ "manage-account", "view-profile" ] 39 | }, 40 | "notBefore" : 0, 41 | "groups" : [ ] 42 | }, { 43 | "id" : "483bfd18-32fb-4666-9fed-48cabc376498", 44 | "createdTimestamp" : 1548798984518, 45 | "username" : "testuser", 46 | "enabled" : true, 47 | "totp" : false, 48 | "emailVerified" : true, 49 | "firstName" : "John", 50 | "lastName" : "Doe", 51 | "email" : "john@example.com", 52 | "credentials" : [ { 53 | "type" : "password", 54 | "hashedSaltedValue" : "qhYpBE+XIEwcnZ1Db3UuNlYypzajnn71g6g3nTupnmG0QCAIb8pykNGyumdZB4GFVltCAX0d+m+pEz5zSM+1Ig==", 55 | "salt" : "/ixUwYokI9O3iHeFe30BIA==", 56 | "hashIterations" : 27500, 57 | "counter" : 0, 58 | "algorithm" : "pbkdf2-sha256", 59 | "digits" : 0, 60 | "period" : 0, 61 | "createdDate" : 1548798995962, 62 | "config" : { } 63 | } ], 64 | "disableableCredentialTypes" : [ "password" ], 65 | "requiredActions" : [ ], 66 | "realmRoles" : [ "offline_access", "uma_authorization" ], 67 | "clientRoles" : { 68 | "resource-provider" : [ "some-permission" ], 69 | "realm-management" : [ "manage-users" ], 70 | "account" : [ "manage-account", "view-profile" ] 71 | }, 72 | "notBefore" : 0, 73 | "groups" : [ ] 74 | } ] 75 | } -------------------------------------------------------------------------------- /example/keycloak/export/master-users-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "realm" : "master", 3 | "users" : [ { 4 | "id" : "13376567-82f9-4ff7-bf0e-c74237363105", 5 | "createdTimestamp" : 1548796136436, 6 | "username" : "admin", 7 | "enabled" : true, 8 | "totp" : false, 9 | "emailVerified" : false, 10 | "credentials" : [ { 11 | "type" : "password", 12 | "hashedSaltedValue" : "+NYGHHcl8ScJLcwIOZ5r0jRva/3ht2aWey+vFkNcSyV+gDj9IMqmqlrMM9OobSsNYV2+SnHhrS0Z6ZWswv3CYQ==", 13 | "salt" : "lD4zbiXX8PFYe9/FKDP+0w==", 14 | "hashIterations" : 27500, 15 | "counter" : 0, 16 | "algorithm" : "pbkdf2-sha256", 17 | "digits" : 0, 18 | "period" : 0, 19 | "config" : { } 20 | } ], 21 | "disableableCredentialTypes" : [ "password" ], 22 | "requiredActions" : [ ], 23 | "realmRoles" : [ "admin", "uma_authorization", "offline_access" ], 24 | "clientRoles" : { 25 | "account" : [ "manage-account", "view-profile" ] 26 | }, 27 | "notBefore" : 0, 28 | "groups" : [ ] 29 | } ] 30 | } -------------------------------------------------------------------------------- /example/nginx/certs/README.md: -------------------------------------------------------------------------------- 1 | SSL Certificates 2 | ================ 3 | 4 | This folder contains a set of very insecure SSL certificates to be used for 5 | running the server in SSL. 6 | 7 | As you can see the private key and everything is here. This is absolutely not 8 | done for anything that smells production ready. 9 | 10 | However with a CA and a SSL certificate we can have an SSL connection between 11 | all servers in this example setup. 12 | 13 | Below a summary of how these certificates where created so I can redo whenever 14 | I need it. 15 | 16 | 1. Create CA key 17 | 18 | 19 | $ openssl genrsa -out ca.key 2048 20 | 21 | 2. Create CA certificate 22 | 23 | 24 | $ openssl req -x509 -new -nodes -key ca.key -sha256 -days 1825 -out ca.pem 25 | 26 | 27 | 3. Generate private key and CSR 28 | 29 | 30 | $ openssl req -out localhost.yarf.nl.csr -newkey rsa:2048 -nodes -keyout localhost.yarf.nl.key 31 | 32 | 33 | 4. Sign with the CA certificate 34 | 35 | 36 | $ openssl x509 -req -in localhost.yarf.nl.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out localhost.yarf.nl.cert -days 1825 -sha256 -extfile ssl.ext 37 | 38 | 39 | 5. Verify the certifcate 40 | 41 | 42 | $ openssl x509 -noout -text -in localhost.yarf.nl.cert 43 | 44 | 45 | 6. Add `ca.pem` to your browser's set of trusted CA's -------------------------------------------------------------------------------- /example/nginx/certs/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA1s7NCNqKe4MZEb/Hhhy0nXiFO32GlgJwcaiVZccKXv0XK5pl 3 | vChHPxNdlxjA9n5bD2tak3IluLcsXAknn8/u6hGJwYizol84z0U+bJpn6EoXesVO 4 | TbQxw3ZfwG1y4HdoJPU3EYjzqoO/i0PUOGP8ZdeERJrcjsb21k6qUMGuuP6XXCVd 5 | UVznHjJcbN3pIber+hO5mmqNgJAgemHI+GmPd+2Kr7GpJluOyhfbc+lE445z6ulw 6 | ts50xnrl8PQlufayhIhNIx5xo6QXxvuJPpPIdcpZZHVgD1Fa/Dct7/Fd0hojSUle 7 | fvSrfazjG6UKPYqXUKMAeYvs9WQfB0llPXagrQIDAQABAoIBAQCSfQM8V3T7GAM/ 8 | pG6X2fmeLfOGB2uB33X5PMNtXhiHgMeNV8SrVTiJHlbD1QC62J6W3FovlTd+SYR5 9 | 21BnXvEKNR2hmu0N/SwLxaf1fjMAKY7rjaSBvzH0n0RbQQiHZUbjdUqxrZimHyS/ 10 | /2i5cA5PEXNFKerhf3QT4B+r5PmXc9+zu5rLftCFeorquNi1bk96eN7xbMftu/n2 11 | Iwb9kV1dSxzMtWURgj2nks3RBXu+xyu6rwv+c9L2RLcratFsYxUgnrz1NC/zDOBE 12 | 4y05yN8d2qx7DNKkBPEXLDyaETbZXo5J4HKwZQxWcbAh9we/M1TXk1jXVFaB7Xrq 13 | hSYfukYNAoGBAO8k5Ti7/I2UMVB6mX5Ffys7ThT/JDSfW13zqJctSlOLjcXEjvWq 14 | obsJjLCyEOtlLT59eHxNfglbzahpGtrfn582bR3jBejXCynIFVSGtHogx132E+8E 15 | t2KPs49WMdsO/lmQzVYjGT3DsqWaW2NcTb8Ex/ae1730IJugITHXS5yzAoGBAOXy 16 | yEyjexlprkln6lHcnx604jHMmZKQJ+ntpMGi7rxbledYN8OdIqXjrSShGLu3II3i 17 | pHZjNYwd/KChI0mEYK2iqcdre/5wVoj0qqJqLXbzXkLX41YgaQNKB6f7MvPkUSVc 18 | Ik55ywIRB+65fVTL+CNAxVsSsB4g+sNmykIpKD0fAoGAc/jqiJbxpR8mwyaRZWEC 19 | iM6b0SbiQfq97lQJgDbggp98w1nNEmoLQI8jAVV5Sw6n4FQsp+tUoek5VOCTu20T 20 | FbzpMcM0zHPs3/g+D927jDZ1OKXriNA6truFko90Yg1lX74PNiSTSxaqfhDbHNZY 21 | hbgl2P2zFlVbstz2/Bqyem0CgYEAg5/ne8cQjclqlGZBQL6y7pbH642cUsLltgfs 22 | CVNEqNkcA6MBuJ1X2fFriM4WJE18+vrC/Wlom14G38OdOVXnKT01RguGnGydfCPh 23 | ELsKb057pHODlCdVNSbJHySxU95bfLEyig05YWNyUPoofcOLtFI9JhaabYSfRf6u 24 | xBRfDi0CgYEAr4wuKkDhrYZP7eZG4yRVmSULcIR17+FWwKQxbh9PfBHIMG8k8bn0 25 | fqCXDavrS3jRGZSa+fI/1zdRrc6+gN/3Itap84URl/ibD7QPNIBu3sAR0dYAFXih 26 | NwNOR6Zbimh792G21chx3i+548cEKubUsAxzG7875DqTtanB0kCuJ/s= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /example/nginx/certs/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDkTCCAnmgAwIBAgIUMTe94H1fBJEKrHFgWTMYI8D4kXQwDQYJKoZIhvcNAQEL 3 | BQAwWDELMAkGA1UEBhMCTkwxEzARBgNVBAgMCk92ZXJpanNzZWwxDzANBgNVBAcM 4 | Blp3b2xsZTENMAsGA1UECgwEWWFyZjEUMBIGA1UEAwwLaW5zZWN1cmUuY2EwHhcN 5 | MTkwMTI5MTk0OTI3WhcNMjQwMTI4MTk0OTI3WjBYMQswCQYDVQQGEwJOTDETMBEG 6 | A1UECAwKT3Zlcmlqc3NlbDEPMA0GA1UEBwwGWndvbGxlMQ0wCwYDVQQKDARZYXJm 7 | MRQwEgYDVQQDDAtpbnNlY3VyZS5jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 8 | AQoCggEBANbOzQjainuDGRG/x4YctJ14hTt9hpYCcHGolWXHCl79FyuaZbwoRz8T 9 | XZcYwPZ+Ww9rWpNyJbi3LFwJJ5/P7uoRicGIs6JfOM9FPmyaZ+hKF3rFTk20McN2 10 | X8BtcuB3aCT1NxGI86qDv4tD1Dhj/GXXhESa3I7G9tZOqlDBrrj+l1wlXVFc5x4y 11 | XGzd6SG3q/oTuZpqjYCQIHphyPhpj3ftiq+xqSZbjsoX23PpROOOc+rpcLbOdMZ6 12 | 5fD0Jbn2soSITSMecaOkF8b7iT6TyHXKWWR1YA9RWvw3Le/xXdIaI0lJXn70q32s 13 | 4xulCj2Kl1CjAHmL7PVkHwdJZT12oK0CAwEAAaNTMFEwHQYDVR0OBBYEFFbRJ+tA 14 | iYRKflzljONfTuWL7QSZMB8GA1UdIwQYMBaAFFbRJ+tAiYRKflzljONfTuWL7QSZ 15 | MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHeJK2XeRcCNcY/A 16 | 5nMBG4H+u+FSI3Fc2nXaAlXcXxVaKAxeFrPlis1uZUZ+GQPCJyCKb+nuEPqLMpww 17 | sR7ICoUdUHlq+ciQvY7m2saLw7MI7IXOQl3WymEY1yu/GKPFqPzMMdnn5LOU26nc 18 | U9KtTbcEj23wk0ERFWbk8w2gIDFzmakiaX2S4P/WR2gaXlXGTI3yitMOjg0/sfx4 19 | 0jiMD0vqmk4JVMI76epifJ26Z2ucuTLk9Ea20mKW1VsqUn+6sBIH1uQuh3seoouT 20 | Fg97jLSwnH2sY0vAA8iAKSBewnKgaQQEUEpA3lU0slCIX7Jfioez7wGsL23mLo3R 21 | sB9UL/Q= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /example/nginx/certs/localhost.yarf.nl.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID9jCCAt6gAwIBAgIUT8ajdKSoj+0agwezVw01d4b4wEIwDQYJKoZIhvcNAQEL 3 | BQAwWDELMAkGA1UEBhMCTkwxEzARBgNVBAgMCk92ZXJpanNzZWwxDzANBgNVBAcM 4 | Blp3b2xsZTENMAsGA1UECgwEWWFyZjEUMBIGA1UEAwwLaW5zZWN1cmUuY2EwHhcN 5 | MTkwMTI5MjAyNTM0WhcNMjQwMTI4MjAyNTM0WjBeMQswCQYDVQQGEwJOTDETMBEG 6 | A1UECAwKT3Zlcmlqc3NlbDEPMA0GA1UEBwwGWndvbGxlMQ0wCwYDVQQKDARZYXJm 7 | MRowGAYDVQQDDBFsb2NhbGhvc3QueWFyZi5ubDCCASIwDQYJKoZIhvcNAQEBBQAD 8 | ggEPADCCAQoCggEBAJ1dCtmkdSHADvqjnD8zRSdJlkFBO/M8MY+dVOqtbVd2yKLH 9 | gNNriGARJy8nvx1c9J8y+ahCJgavunlhVT0Knev9VvlLBuY5wFKVcfOqTnzdcz6s 10 | 1aiwSWD0a5YNheBywPCH2Bf+C19rqNK1MZ1uhkjkgPC7NbIqM5eELPIgkEXOEcBe 11 | Pj1/2phIjG9XCqdfugOgnwBD4wRWW82BbTW4zfdi7ZRLAWVX1mgBb+MNVag3MJv1 12 | KnMPj/1/eU2al9mg28rqXaiB3xyJKvomdKjLJ0zv9Ie9DNSn2RaTj9yZGX2pp3VI 13 | +XzTl950Rf1W2OhjRw2JFdr3JfKo3kEiOB6DuZMCAwEAAaOBsTCBrjAfBgNVHSME 14 | GDAWgBRW0SfrQImESn5c5YzjX07li+0EmTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE 15 | 8DBzBgNVHREEbDBqghppZGVudGl0eS5sb2NhbGhvc3QueWFyZi5ubIIjcmVzb3Vy 16 | Y2UtcHJvdmlkZXIubG9jYWxob3N0LnlhcmYubmyCJ3Jlc291cmNlLXByb3ZpZGVy 17 | LWFwaS5sb2NhbGhvc3QueWFyZi5ubDANBgkqhkiG9w0BAQsFAAOCAQEAJouCHu/r 18 | kKAJYKoKmSRhHdN/JUtYhjnkOCfLrhaFo1l2l1jWEeTU9NH4tA6lH3bhQwxOFvtx 19 | 6ujj17yhHoT/Sdvonw0RR8Iuub8JgolepIi+6yRMFQikwamvaTBs+Cc+PIo1n+bw 20 | qSwLzxIYOovD1beCNt0QIG0GSmfJtFtTijdf6t35vPAWA5sx4WtvAPlTNKHfSTt6 21 | dQGfhPwsrkAIb0ItfowOcnN8ZJFgf1HfV6sy1hX4UaxzsvOgwUEYYtSvjjn3GNvG 22 | 9IuEGX7uM+jGPw3M/aWv0R7WAPvwwOrOwv8bZm7zvgO7H8E297eeKdfLy3oiF9wz 23 | J2CzzhVis5EMTQ== 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /example/nginx/certs/localhost.yarf.nl.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICozCCAYsCAQAwXjELMAkGA1UEBhMCTkwxEzARBgNVBAgMCk92ZXJpanNzZWwx 3 | DzANBgNVBAcMBlp3b2xsZTENMAsGA1UECgwEWWFyZjEaMBgGA1UEAwwRbG9jYWxo 4 | b3N0LnlhcmYubmwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdXQrZ 5 | pHUhwA76o5w/M0UnSZZBQTvzPDGPnVTqrW1Xdsiix4DTa4hgEScvJ78dXPSfMvmo 6 | QiYGr7p5YVU9Cp3r/Vb5SwbmOcBSlXHzqk583XM+rNWosElg9GuWDYXgcsDwh9gX 7 | /gtfa6jStTGdboZI5IDwuzWyKjOXhCzyIJBFzhHAXj49f9qYSIxvVwqnX7oDoJ8A 8 | Q+MEVlvNgW01uM33Yu2USwFlV9ZoAW/jDVWoNzCb9SpzD4/9f3lNmpfZoNvK6l2o 9 | gd8ciSr6JnSoyydM7/SHvQzUp9kWk4/cmRl9qad1SPl805fedEX9VtjoY0cNiRXa 10 | 9yXyqN5BIjgeg7mTAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAV6mAduvBgGf7 11 | b70w3mwVj4iVC1z+vhvB5icLWFZ4O0yXFNvlI8pILtUXPox7SNkb8Nlc6vVelWCL 12 | /FsdGTc5m50VnKSxKzw0PEj4VqjI9Pkj7qgGrC1A0Iz234EGsu/x+ycwM+J/qfzm 13 | VwZEDlI1SvpVjUgtFfm/9zYENO63yXrwcKmogZvmGyrxIOQNEZJ4Y7KlqPfg9Tmr 14 | LMmw+FUOBXU7LI255BYb2J9WRJ9udGv6iPRhyMch/gfQm6OBJx1FNUCBNTjeynWg 15 | YtNYmmVMMMqLnNOQa0J9xS68nKa9T1XWndAzM35Bl21pTQJO2K8uFFSGNgo0k7mn 16 | 84qWiKpwTg== 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /example/nginx/certs/localhost.yarf.nl.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCdXQrZpHUhwA76 3 | o5w/M0UnSZZBQTvzPDGPnVTqrW1Xdsiix4DTa4hgEScvJ78dXPSfMvmoQiYGr7p5 4 | YVU9Cp3r/Vb5SwbmOcBSlXHzqk583XM+rNWosElg9GuWDYXgcsDwh9gX/gtfa6jS 5 | tTGdboZI5IDwuzWyKjOXhCzyIJBFzhHAXj49f9qYSIxvVwqnX7oDoJ8AQ+MEVlvN 6 | gW01uM33Yu2USwFlV9ZoAW/jDVWoNzCb9SpzD4/9f3lNmpfZoNvK6l2ogd8ciSr6 7 | JnSoyydM7/SHvQzUp9kWk4/cmRl9qad1SPl805fedEX9VtjoY0cNiRXa9yXyqN5B 8 | Ijgeg7mTAgMBAAECggEATfZryro8wdTaVvi6D2HCUB2YEIpWPkLDNyi2inITqvKR 9 | onv+6j/rV9UHucgSWmTUWQ8zO1ZSapujYkGDrnNVHDbuYSH3sBZKn1+lDhiBPEGm 10 | uBV+4U09fYh6yOO4QSP5TPGwcOqPDd5TzNiyVRIN+40iCKJnjvZziwyUC/1wHPSd 11 | WZvEV/EIlYk+rKwqowleAOXJ/rl1BsUIc5mikN4QHOS5Wbsr6gZibve9ZQ1WlKei 12 | da0c2qxsq/0nFHrSGk6JyIgKxtnm/F/NrQraHe8fO5dMNT0ZmM3bRiy6TDAcgCkn 13 | 1PdtIAhEgZuYAjIhk8IKLzePvYIxPs8D/F2RgD1VeQKBgQDKfIeiYsNiizDZomEj 14 | ovIdJvWXgU5p4LBWZEYLpzbLlOv6kGVZ5Hn6Z+VRrLYWv9/TScicannJNu/qRGia 15 | 4hlcjUWbkjWLjFt52/kT9VfeONIce04K1/HHBRtPada0hUdzwHCHfNCsT0yB2vxe 16 | HD3079e7Ct0CyoD+hEsMgEYbFwKBgQDG86kVm7qIelMzX3Zwp4ib18H5TsoMMrcY 17 | 22Pf0G6/biaSxixtiRBpKypYuXADf5Q3PJb9maAGBXjd5CGbia+gzegYxP8NYa+C 18 | QwhqCbBBKjRhF3w1JmqVlJyWYamjxIfDdmgTU/IzbUhxOx4MFcAFmnNJ6n3qulbT 19 | 1ELFO3Uy5QKBgBmJNO3EuNFPrnxz3v5IiXIlvKk7tHDj1jk+8hp8HwvznwL9fNqm 20 | Vr++pIv1VQ1va0HRN0yKnQtEM2N+9xY5V2t1oYaqHLiZnduzYykiMs+iqNTQtBnn 21 | ++TWfwg318zyVf2CEm7zzbk9Uu+5d0RDGYkvSiybhR3Z/gSbGH/eGXlHAoGBAK/C 22 | SIjfZ730GxaSakcBqmzLAgEmetal4x1hi+0I7R3OyOL3kf4+jTHrwWBaijt89MqL 23 | i3SEyFspcGrOhGYtD+wGm9luB0iiGPQCFiffYUdgap+vqLonsxdsD53Gr6APGkUy 24 | kKUqjxihndLygAv7FwWcOed98jlw3w4KQeaOLW6FAoGAblQuh68PSYNSEnr5SSF6 25 | crHL8iQx5+k+e+7dp/VpNCUJGxYjRipNLzRstqQgTzhawupUVYpgmymOGt82+skq 26 | WmDHSgPLm5hcjbEOuw1wyDzJaVrR58W6po9Oq4LKUkAq5ST2IGS+ZEtPelUPgBSj 27 | yY3FtxXRPlqi2RyJ5MJ13wA= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /example/nginx/certs/ssl.ext: -------------------------------------------------------------------------------- 1 | authorityKeyIdentifier=keyid,issuer 2 | basicConstraints=CA:FALSE 3 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 4 | subjectAltName = @alt_names 5 | 6 | [alt_names] 7 | DNS.1 = identity.localhost.yarf.nl 8 | DNS.2 = resource-provider.localhost.yarf.nl 9 | DNS.3 = resource-provider-api.localhost.yarf.nl 10 | -------------------------------------------------------------------------------- /example/nginx/conf.d/identity.localhost.yarf.nl.conf: -------------------------------------------------------------------------------- 1 | upstream keycloak { 2 | server keycloak:8080; 3 | } 4 | 5 | server { 6 | listen *:80; 7 | server_name identity.localhost.yarf.nl; 8 | return 301 https://$server_name$request_uri; 9 | } 10 | 11 | server { 12 | 13 | listen *:443; 14 | server_name identity.localhost.yarf.nl; 15 | 16 | # Set up SSL 17 | ssl on; 18 | 19 | ssl_session_cache shared:SSL:1m; 20 | ssl_session_timeout 5m; 21 | 22 | ssl_certificate /etc/nginx/certs/localhost.yarf.nl.cert; 23 | ssl_certificate_key /etc/nginx/certs/localhost.yarf.nl.key; 24 | 25 | # Settings to avoid SSL warnings in browser 26 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don’t use SSLv3 ref: POODLE 27 | ssl_prefer_server_ciphers on; 28 | ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES"; 29 | 30 | 31 | # Logging 32 | access_log /var/log/nginx/identity.localhost.yarf.nl.access.log; # could also put these into 1 file 33 | error_log /var/log/nginx/identity.localhost.yarf.nl.error.log; 34 | 35 | 36 | # Allow empty bodies 37 | client_max_body_size 0; 38 | 39 | # Reverse proxy to Keycloak 40 | location / { 41 | 42 | proxy_pass http://keycloak; 43 | 44 | proxy_buffering off; 45 | proxy_set_header Host $http_host; 46 | proxy_set_header X-Real-IP $remote_addr; 47 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 48 | proxy_set_header X-Forwarded-Proto $scheme; 49 | 50 | proxy_cookie_domain identity.localhost.yarf.nl $host; 51 | proxy_pass_header X-XSRF-TOKEN; 52 | 53 | # untested, but taken from https://gist.github.com/nikmartin/5902176#file-nginx-ssl-conf-L25 54 | # and seems useful 55 | proxy_set_header X-NginX-Proxy true; 56 | proxy_read_timeout 5m; 57 | proxy_connect_timeout 5m; 58 | 59 | proxy_redirect off; 60 | } 61 | 62 | 63 | } 64 | -------------------------------------------------------------------------------- /example/nginx/conf.d/resource-provider-api.localhost.yarf.nl.conf: -------------------------------------------------------------------------------- 1 | upstream resource-provider-api { 2 | server resource-provider-api:8002; 3 | } 4 | 5 | server { 6 | listen *:80; 7 | server_name resource-provider-api.localhost.yarf.nl; 8 | return 301 https://$server_name$request_uri; 9 | } 10 | 11 | server { 12 | 13 | listen 443; 14 | server_name resource-provider-api.localhost.yarf.nl; 15 | 16 | # Set up SSL 17 | ssl on; 18 | 19 | ssl_session_cache shared:SSL:1m; 20 | ssl_session_timeout 5m; 21 | 22 | ssl_certificate /etc/nginx/certs/localhost.yarf.nl.cert; 23 | ssl_certificate_key /etc/nginx/certs/localhost.yarf.nl.key; 24 | 25 | # Settings to avoid SSL warnings in browser 26 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don’t use SSLv3 ref: POODLE 27 | ssl_prefer_server_ciphers on; 28 | ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES"; 29 | 30 | 31 | # Logging 32 | access_log /var/log/nginx/resource-provider-api.localhost.yarf.nl.access.log; # could also put these into 1 file 33 | error_log /var/log/nginx/resource-provider-api.localhost.yarf.nl.error.log; 34 | 35 | 36 | # Allow empty bodies 37 | client_max_body_size 0; 38 | 39 | # Reverse proxy to Keycloak 40 | location / { 41 | 42 | proxy_pass http://resource-provider-api; 43 | 44 | proxy_buffering off; 45 | proxy_set_header Host $http_host; 46 | proxy_set_header X-Real-IP $remote_addr; 47 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 48 | proxy_set_header X-Forwarded-Proto $scheme; 49 | 50 | proxy_cookie_domain resource-provider-api.localhost.yarf.nl $host; 51 | proxy_pass_header X-XSRF-TOKEN; 52 | 53 | # untested, but taken from https://gist.github.com/nikmartin/5902176#file-nginx-ssl-conf-L25 54 | # and seems useful 55 | proxy_set_header X-NginX-Proxy true; 56 | proxy_read_timeout 5m; 57 | proxy_connect_timeout 5m; 58 | 59 | proxy_redirect off; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /example/nginx/conf.d/resource-provider.localhost.yarf.nl.conf: -------------------------------------------------------------------------------- 1 | upstream resource-provider { 2 | server resource-provider:8001; 3 | } 4 | 5 | server { 6 | listen *:80; 7 | server_name resource-provider.localhost.yarf.nl; 8 | return 301 https://$server_name$request_uri; 9 | } 10 | 11 | server { 12 | 13 | listen *:443; 14 | server_name resource-provider.localhost.yarf.nl; 15 | 16 | # Set up SSL 17 | ssl on; 18 | 19 | ssl_session_cache shared:SSL:1m; 20 | ssl_session_timeout 5m; 21 | 22 | ssl_certificate /etc/nginx/certs/localhost.yarf.nl.cert; 23 | ssl_certificate_key /etc/nginx/certs/localhost.yarf.nl.key; 24 | 25 | # Settings to avoid SSL warnings in browser 26 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don’t use SSLv3 ref: POODLE 27 | ssl_prefer_server_ciphers on; 28 | ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES"; 29 | 30 | 31 | # Logging 32 | access_log /var/log/nginx/resource-provider.localhost.yarf.nl.access.log; # could also put these into 1 file 33 | error_log /var/log/nginx/resource-provider.localhost.yarf.nl.error.log; 34 | 35 | 36 | # Allow empty bodies 37 | client_max_body_size 0; 38 | 39 | # Reverse proxy to Keycloak 40 | location / { 41 | 42 | proxy_pass http://resource-provider; 43 | 44 | proxy_buffering off; 45 | proxy_set_header Host $http_host; 46 | proxy_set_header X-Real-IP $remote_addr; 47 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 48 | proxy_set_header X-Forwarded-Proto $scheme; 49 | 50 | proxy_cookie_domain resource-provider.localhost.yarf.nl $host; 51 | proxy_pass_header X-XSRF-TOKEN; 52 | 53 | # untested, but taken from https://gist.github.com/nikmartin/5902176#file-nginx-ssl-conf-L25 54 | # and seems useful 55 | proxy_set_header X-NginX-Proxy true; 56 | proxy_read_timeout 5m; 57 | proxy_connect_timeout 5m; 58 | 59 | proxy_redirect off; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /example/resource-provider-api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | 3 | RUN apk update \ 4 | && apk add git openssl-dev libffi-dev python-dev build-base 5 | 6 | RUN mkdir -p /usr/src/app 7 | 8 | WORKDIR /usr/src/app 9 | 10 | COPY requirements.txt /usr/src/app/ 11 | 12 | RUN pip install --no-cache-dir -r requirements.txt 13 | 14 | COPY . /usr/src/app 15 | 16 | EXPOSE 8002 17 | 18 | CMD [ "python", "manage.py", "runserver", "0.0.0.0:8002" ] 19 | -------------------------------------------------------------------------------- /example/resource-provider-api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider-api/__init__.py -------------------------------------------------------------------------------- /example/resource-provider-api/docker/entrypoint-dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -e 3 | 4 | pip install -e ./../django-keycloak/ 5 | pip install -e ./../python-keycloak-client/ || true 6 | 7 | if [ -f db.sqlite3 ]; then 8 | echo "Application already initialized." 9 | else 10 | echo "Initializing application" 11 | 12 | # Run migrations 13 | python manage.py migrate 14 | 15 | python manage.py load_dynamic_fixtures myapp 16 | fi 17 | 18 | if grep -q Yarf /usr/local/lib/python3.7/site-packages/certifi/cacert.pem 19 | then 20 | echo "CA already added" 21 | else 22 | echo "Add CA to trusted pool" 23 | echo "\n\n# Yarf" >> /usr/local/lib/python3.7/site-packages/certifi/cacert.pem 24 | cat /usr/src/ca.pem >> /usr/local/lib/python3.7/site-packages/certifi/cacert.pem 25 | fi 26 | 27 | exec "$@" 28 | -------------------------------------------------------------------------------- /example/resource-provider-api/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myapp.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django #noqa: F401 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /example/resource-provider-api/myapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider-api/myapp/__init__.py -------------------------------------------------------------------------------- /example/resource-provider-api/myapp/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class MyAppConfig(AppConfig): 8 | name = 'myapp' 9 | -------------------------------------------------------------------------------- /example/resource-provider-api/myapp/fixtures/0001_admin_user.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | 3 | from dynamic_fixtures.fixtures import BaseFixture 4 | 5 | 6 | class Fixture(BaseFixture): 7 | 8 | def load(self): 9 | user_model = get_user_model() 10 | 11 | if not user_model.objects.filter(username='admin').exists(): 12 | user_model.objects.create_superuser(username='admin', 13 | password='password', 14 | email='admin@example.com') 15 | -------------------------------------------------------------------------------- /example/resource-provider-api/myapp/fixtures/0002_server.py: -------------------------------------------------------------------------------- 1 | from dynamic_fixtures.fixtures import BaseFixture 2 | 3 | from django_keycloak.models import Server 4 | 5 | 6 | class Fixture(BaseFixture): 7 | 8 | def load(self): 9 | Server.objects.get_or_create( 10 | url='https://identity.localhost.yarf.nl' 11 | ) 12 | -------------------------------------------------------------------------------- /example/resource-provider-api/myapp/fixtures/0003_realm.py: -------------------------------------------------------------------------------- 1 | from dynamic_fixtures.fixtures import BaseFixture 2 | 3 | from django_keycloak.models import Realm, Server 4 | 5 | 6 | class Fixture(BaseFixture): 7 | 8 | dependencies = ( 9 | ('myapp', '0002_server'), 10 | ) 11 | 12 | def load(self): 13 | server = Server.objects.get(url='https://identity.localhost.yarf.nl') 14 | 15 | Realm.objects.get_or_create( 16 | server=server, 17 | name='example', 18 | _certs='{"keys": [{"kid": "jr15h-1NMapltoxqLH8aBTD4-XjyfGD8pqUEtqsUJmU", "kty": "RSA", "alg": "RS256", "use": "sig", "n": "4hGf6zcb6KghN61pUROjPptdivncqkgaDcNwFcubw95Lw1IiTsgo__l1P3720Lhsb4Br0w2XWr44fzR8i1kgvow_s5-CG_F3S7OJ1Abz1Au_zSHg3nLd901lWMVrXvVZR1jFCuvIjr9DQAmyHv1-cL8OuBjGKWX7qi3LXQp2oEVRIUO_pM83vQJf1rZH-YUz6-w_g4XeOF-1GRXtaC2xYHbUCEsH1Lo7J-i3im4SgvER74Zh-cpoF_Q2DjK520VJletJBM4SrUh3DcScGVFmwharPI4wDdRXq_-SqQ52r1-X0k3fmj1gcwhLRH6jW0dTTt-T2ROENfjGTBBd5nBJWQ", "e": "AQAB"}]}', 19 | _well_known_oidc='{"issuer": "https://identity.localhost.yarf.nl/auth/realms/example", "authorization_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/auth", "token_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/token", "token_introspection_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/token/introspect", "userinfo_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/userinfo", "end_session_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/logout", "jwks_uri": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/certs", "check_session_iframe": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/login-status-iframe.html", "grant_types_supported": ["authorization_code", "implicit", "refresh_token", "password", "client_credentials"], "response_types_supported": ["code", "none", "id_token", "token", "id_token token", "code id_token", "code token", "code id_token token"], "subject_types_supported": ["public", "pairwise"], "id_token_signing_alg_values_supported": ["RS256"], "userinfo_signing_alg_values_supported": ["RS256"], "request_object_signing_alg_values_supported": ["none", "RS256"], "response_modes_supported": ["query", "fragment", "form_post"], "registration_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/clients-registrations/openid-connect", "token_endpoint_auth_methods_supported": ["private_key_jwt", "client_secret_basic", "client_secret_post"], "token_endpoint_auth_signing_alg_values_supported": ["RS256"], "claims_supported": ["sub", "iss", "auth_time", "name", "given_name", "family_name", "preferred_username", "email"], "claim_types_supported": ["normal"], "claims_parameter_supported": false, "scopes_supported": ["openid", "offline_access"], "request_parameter_supported": true, "request_uri_parameter_supported": true}' 20 | ) 21 | -------------------------------------------------------------------------------- /example/resource-provider-api/myapp/fixtures/0004_client.py: -------------------------------------------------------------------------------- 1 | from dynamic_fixtures.fixtures import BaseFixture 2 | 3 | from django_keycloak.models import Realm, Client 4 | 5 | 6 | class Fixture(BaseFixture): 7 | 8 | dependencies = ( 9 | ('myapp', '0003_realm'), 10 | ) 11 | 12 | def load(self): 13 | realm = Realm.objects.get(name='example') 14 | 15 | Client.objects.get_or_create( 16 | realm=realm, 17 | client_id='resource-provider-api', 18 | secret='145a828b-bbb1-44b0-81f5-d3d669ab59f7' 19 | ) 20 | -------------------------------------------------------------------------------- /example/resource-provider-api/myapp/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider-api/myapp/fixtures/__init__.py -------------------------------------------------------------------------------- /example/resource-provider-api/myapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider-api/myapp/migrations/__init__.py -------------------------------------------------------------------------------- /example/resource-provider-api/myapp/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models 5 | -------------------------------------------------------------------------------- /example/resource-provider-api/myapp/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for resource_provider project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '1y=4ruy1(qz70k1l$kwe3o^i4^2a_7p)popv@6$sj%!04t^s9l' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = [ 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 40 | 'myapp.apps.MyAppConfig', 41 | 'django_keycloak.apps.KeycloakAppConfig', 42 | 'dynamic_fixtures' 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | 54 | 'django_keycloak.middleware.KeycloakStatelessBearerAuthenticationMiddleware', 55 | # 'django_keycloak.middleware.RemoteUserAuthenticationMiddleware', 56 | ] 57 | 58 | PASSWORD_HASHERS = [ 59 | 'django_keycloak.hashers.PBKDF2SHA512PasswordHasher', 60 | ] 61 | 62 | AUTHENTICATION_BACKENDS = [ 63 | 'django.contrib.auth.backends.ModelBackend', 64 | 'django_keycloak.auth.backends.KeycloakIDTokenAuthorizationBackend' 65 | ] 66 | 67 | ROOT_URLCONF = 'myapp.urls' 68 | 69 | TEMPLATES = [ 70 | { 71 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 72 | 'DIRS': [], 73 | 'APP_DIRS': True, 74 | 'OPTIONS': { 75 | 'context_processors': [ 76 | 'django.template.context_processors.debug', 77 | 'django.template.context_processors.request', 78 | 'django.contrib.auth.context_processors.auth', 79 | 'django.contrib.messages.context_processors.messages', 80 | ], 81 | }, 82 | }, 83 | ] 84 | 85 | WSGI_APPLICATION = 'myapp.wsgi.application' 86 | 87 | 88 | # Database 89 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 90 | 91 | DATABASES = { 92 | 'default': { 93 | 'ENGINE': 'django.db.backends.sqlite3', 94 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 95 | } 96 | } 97 | 98 | 99 | # Password validation 100 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 101 | 102 | AUTH_PASSWORD_VALIDATORS = [ 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 114 | }, 115 | ] 116 | 117 | KEYCLOAK_OIDC_PROFILE_MODEL = 'django_keycloak.RemoteUserOpenIdConnectProfile' 118 | 119 | # Internationalization 120 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 121 | 122 | LANGUAGE_CODE = 'en-us' 123 | 124 | TIME_ZONE = 'UTC' 125 | 126 | USE_I18N = True 127 | 128 | USE_L10N = True 129 | 130 | USE_TZ = True 131 | 132 | 133 | # Static files (CSS, JavaScript, Images) 134 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 135 | 136 | STATIC_URL = '/static/' 137 | 138 | LOGGING = { 139 | 'version': 1, 140 | 'disable_existing_loggers': False, 141 | 'handlers': { 142 | 'console': { 143 | 'level': 'DEBUG', 144 | 'class': 'logging.StreamHandler', 145 | }, 146 | }, 147 | 'loggers': { 148 | '': { 149 | 'handlers': ['console'], 150 | 'level': 'DEBUG', 151 | }, 152 | } 153 | } 154 | 155 | KEYCLOAK_BEARER_AUTHENTICATION_EXEMPT_PATHS = [ 156 | r'^admin/', 157 | ] 158 | 159 | KEYCLOAK_SKIP_SSL_VERIFY = True 160 | -------------------------------------------------------------------------------- /example/resource-provider-api/myapp/urls.py: -------------------------------------------------------------------------------- 1 | """resource_provider URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | 19 | from myapp import views 20 | 21 | 22 | urlpatterns = [ 23 | url(r'^api/end-point$', views.api_end_point), 24 | url(r'^api/authenticated-end-point$', views.authenticated_end_point), 25 | url(r'^admin/', admin.site.urls), 26 | ] 27 | -------------------------------------------------------------------------------- /example/resource-provider-api/myapp/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import logging 5 | 6 | from django.http.response import JsonResponse 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def api_end_point(request): 12 | return JsonResponse({'foo': 'bar'}) 13 | 14 | 15 | def authenticated_end_point(request): 16 | 17 | return JsonResponse({ 18 | 'user': { 19 | 'first_name': request.user.first_name, 20 | 'last_name': request.user.last_name 21 | } 22 | }) -------------------------------------------------------------------------------- /example/resource-provider-api/myapp/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for resource_provider project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myapp.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /example/resource-provider-api/requirements.txt: -------------------------------------------------------------------------------- 1 | django>=2.0.10 2 | git+git://github.com/Peter-Slump/django-keycloak.git#egg=django-keycloak 3 | django-dynamic-fixtures==0.1.7 -------------------------------------------------------------------------------- /example/resource-provider/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | 3 | RUN apk update \ 4 | && apk add git openssl-dev libffi-dev python-dev build-base 5 | 6 | RUN mkdir -p /usr/src/app 7 | 8 | WORKDIR /usr/src/app 9 | 10 | COPY requirements.txt /usr/src/app/ 11 | 12 | RUN pip install --no-cache-dir -r requirements.txt 13 | 14 | COPY . /usr/src/app 15 | 16 | EXPOSE 8001 17 | 18 | CMD [ "python", "manage.py", "runserver", "0.0.0.0:8001" ] 19 | -------------------------------------------------------------------------------- /example/resource-provider/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider/__init__.py -------------------------------------------------------------------------------- /example/resource-provider/docker/entrypoint-dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -e 3 | 4 | pip install -e ./../django-keycloak/ 5 | pip install -e ./../python-keycloak-client/ || true 6 | pip install -e ./../django-dynamic-fixtures/ || true 7 | 8 | if [ -f db.sqlite3 ]; then 9 | echo "Application already initialized." 10 | else 11 | echo "Initializing application" 12 | 13 | # Run migrations 14 | python manage.py migrate 15 | 16 | python manage.py load_dynamic_fixtures myapp 17 | fi 18 | 19 | if grep -q Yarf /usr/local/lib/python3.7/site-packages/certifi/cacert.pem 20 | then 21 | echo "CA already added" 22 | else 23 | echo "Add CA to trusted pool" 24 | echo "\n\n# Yarf" >> /usr/local/lib/python3.7/site-packages/certifi/cacert.pem 25 | cat /usr/src/ca.pem >> /usr/local/lib/python3.7/site-packages/certifi/cacert.pem 26 | fi 27 | 28 | exec "$@" 29 | -------------------------------------------------------------------------------- /example/resource-provider/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myapp.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django #noqa: F401 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /example/resource-provider/myapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider/myapp/__init__.py -------------------------------------------------------------------------------- /example/resource-provider/myapp/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class MyAppConfig(AppConfig): 8 | name = 'myapp' 9 | -------------------------------------------------------------------------------- /example/resource-provider/myapp/fixtures/0001_admin_user.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | 3 | from dynamic_fixtures.fixtures import BaseFixture 4 | 5 | 6 | class Fixture(BaseFixture): 7 | 8 | def load(self): 9 | user_model = get_user_model() 10 | 11 | if not user_model.objects.filter(username='admin').exists(): 12 | user_model.objects.create_superuser(username='admin', 13 | password='password', 14 | email='admin@example.com') 15 | -------------------------------------------------------------------------------- /example/resource-provider/myapp/fixtures/0002_server.py: -------------------------------------------------------------------------------- 1 | from dynamic_fixtures.fixtures import BaseFixture 2 | 3 | from django_keycloak.models import Server 4 | 5 | 6 | class Fixture(BaseFixture): 7 | 8 | def load(self): 9 | Server.objects.get_or_create( 10 | url='https://identity.localhost.yarf.nl' 11 | ) 12 | -------------------------------------------------------------------------------- /example/resource-provider/myapp/fixtures/0003_realm.py: -------------------------------------------------------------------------------- 1 | from dynamic_fixtures.fixtures import BaseFixture 2 | 3 | from django_keycloak.models import Realm, Server, RemoteClient 4 | 5 | 6 | class Fixture(BaseFixture): 7 | 8 | dependencies = ( 9 | ('myapp', '0002_server'), 10 | ) 11 | 12 | def load(self): 13 | server = Server.objects.get(url='https://identity.localhost.yarf.nl') 14 | 15 | realm, created = Realm.objects.get_or_create( 16 | server=server, 17 | name='example', 18 | _certs='{"keys": [{"kid": "jr15h-1NMapltoxqLH8aBTD4-XjyfGD8pqUEtqsUJmU", "kty": "RSA", "alg": "RS256", "use": "sig", "n": "4hGf6zcb6KghN61pUROjPptdivncqkgaDcNwFcubw95Lw1IiTsgo__l1P3720Lhsb4Br0w2XWr44fzR8i1kgvow_s5-CG_F3S7OJ1Abz1Au_zSHg3nLd901lWMVrXvVZR1jFCuvIjr9DQAmyHv1-cL8OuBjGKWX7qi3LXQp2oEVRIUO_pM83vQJf1rZH-YUz6-w_g4XeOF-1GRXtaC2xYHbUCEsH1Lo7J-i3im4SgvER74Zh-cpoF_Q2DjK520VJletJBM4SrUh3DcScGVFmwharPI4wDdRXq_-SqQ52r1-X0k3fmj1gcwhLRH6jW0dTTt-T2ROENfjGTBBd5nBJWQ", "e": "AQAB"}]}', 19 | _well_known_oidc='{"issuer": "https://identity.localhost.yarf.nl/auth/realms/example", "authorization_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/auth", "token_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/token", "token_introspection_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/token/introspect", "userinfo_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/userinfo", "end_session_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/logout", "jwks_uri": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/certs", "check_session_iframe": "https://identity.localhost.yarf.nl/auth/realms/example/protocol/openid-connect/login-status-iframe.html", "grant_types_supported": ["authorization_code", "implicit", "refresh_token", "password", "client_credentials"], "response_types_supported": ["code", "none", "id_token", "token", "id_token token", "code id_token", "code token", "code id_token token"], "subject_types_supported": ["public", "pairwise"], "id_token_signing_alg_values_supported": ["RS256"], "userinfo_signing_alg_values_supported": ["RS256"], "request_object_signing_alg_values_supported": ["none", "RS256"], "response_modes_supported": ["query", "fragment", "form_post"], "registration_endpoint": "https://identity.localhost.yarf.nl/auth/realms/example/clients-registrations/openid-connect", "token_endpoint_auth_methods_supported": ["private_key_jwt", "client_secret_basic", "client_secret_post"], "token_endpoint_auth_signing_alg_values_supported": ["RS256"], "claims_supported": ["sub", "iss", "auth_time", "name", "given_name", "family_name", "preferred_username", "email"], "claim_types_supported": ["normal"], "claims_parameter_supported": false, "scopes_supported": ["openid", "offline_access"], "request_parameter_supported": true, "request_uri_parameter_supported": true}' 20 | ) 21 | 22 | RemoteClient.objects.get_or_create( 23 | name='resource-provider-api', 24 | realm=realm 25 | ) 26 | -------------------------------------------------------------------------------- /example/resource-provider/myapp/fixtures/0004_client.py: -------------------------------------------------------------------------------- 1 | from dynamic_fixtures.fixtures import BaseFixture 2 | 3 | from django_keycloak.models import Realm, Client 4 | 5 | 6 | class Fixture(BaseFixture): 7 | 8 | dependencies = ( 9 | ('myapp', '0003_realm'), 10 | ) 11 | 12 | def load(self): 13 | realm = Realm.objects.get(name='example') 14 | 15 | Client.objects.get_or_create( 16 | realm=realm, 17 | client_id='resource-provider', 18 | secret='f40347aa-728d-4599-aba9-26f4a69d6f1e' 19 | ) 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/resource-provider/myapp/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider/myapp/fixtures/__init__.py -------------------------------------------------------------------------------- /example/resource-provider/myapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.3 on 2018-03-20 21:28 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('django_keycloak', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='ExchangedToken', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('access_token', models.TextField(null=True)), 21 | ('expires_before', models.DateTimeField(null=True)), 22 | ('refresh_token', models.TextField(null=True)), 23 | ('refresh_expires_before', models.DateTimeField(null=True)), 24 | ('oidc_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_keycloak.OpenIdConnectProfile')), 25 | ], 26 | ), 27 | migrations.CreateModel( 28 | name='RemoteClient', 29 | fields=[ 30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('name', models.CharField(max_length=255)), 32 | ('realm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='remote_clients', to='django_keycloak.Realm')), 33 | ], 34 | ), 35 | migrations.AddField( 36 | model_name='exchangedtoken', 37 | name='remote_client', 38 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exchanged_tokens', to='myapp.RemoteClient'), 39 | ), 40 | migrations.AlterUniqueTogether( 41 | name='exchangedtoken', 42 | unique_together={('oidc_profile', 'remote_client')}, 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /example/resource-provider/myapp/migrations/0002_auto_20180322_2059.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.3 on 2018-03-22 20:59 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('myapp', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterUniqueTogether( 14 | name='exchangedtoken', 15 | unique_together=set(), 16 | ), 17 | migrations.RemoveField( 18 | model_name='exchangedtoken', 19 | name='oidc_profile', 20 | ), 21 | migrations.RemoveField( 22 | model_name='exchangedtoken', 23 | name='remote_client', 24 | ), 25 | migrations.RemoveField( 26 | model_name='remoteclient', 27 | name='realm', 28 | ), 29 | migrations.DeleteModel( 30 | name='ExchangedToken', 31 | ), 32 | migrations.DeleteModel( 33 | name='RemoteClient', 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /example/resource-provider/myapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/example/resource-provider/myapp/migrations/__init__.py -------------------------------------------------------------------------------- /example/resource-provider/myapp/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models 5 | -------------------------------------------------------------------------------- /example/resource-provider/myapp/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for resource_provider project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '1y=4ruy1(qz70k1l$kwe3o^i4^2a_7p)popv@6$sj%!04t^s9l' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | LOGIN_URL = 'keycloak_login' 31 | LOGOUT_REDIRECT_URL = 'keycloak_login' 32 | 33 | SECURE_SSL_REDIRECT = True 34 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 35 | 36 | # Application definition 37 | 38 | INSTALLED_APPS = [ 39 | 'django.contrib.admin', 40 | 'django.contrib.auth', 41 | 'django.contrib.contenttypes', 42 | 'django.contrib.sessions', 43 | 'django.contrib.messages', 44 | 'django.contrib.staticfiles', 45 | 46 | 'myapp.apps.MyAppConfig', 47 | 'django_keycloak.apps.KeycloakAppConfig', 48 | 'dynamic_fixtures' 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | 'django.middleware.security.SecurityMiddleware', 53 | 'django.contrib.sessions.middleware.SessionMiddleware', 54 | 'django.middleware.common.CommonMiddleware', 55 | 'django.middleware.csrf.CsrfViewMiddleware', 56 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 57 | 'django.contrib.messages.middleware.MessageMiddleware', 58 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 59 | 60 | 'django_keycloak.middleware.BaseKeycloakMiddleware', 61 | 'django_keycloak.middleware.RemoteUserAuthenticationMiddleware', 62 | ] 63 | 64 | PASSWORD_HASHERS = [ 65 | 'django_keycloak.hashers.PBKDF2SHA512PasswordHasher', 66 | ] 67 | 68 | AUTHENTICATION_BACKENDS = [ 69 | 'django.contrib.auth.backends.ModelBackend', 70 | 'django_keycloak.auth.backends.KeycloakAuthorizationCodeBackend', 71 | ] 72 | 73 | ROOT_URLCONF = 'myapp.urls' 74 | 75 | TEMPLATES = [ 76 | { 77 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 78 | 'DIRS': [], 79 | 'APP_DIRS': True, 80 | 'OPTIONS': { 81 | 'context_processors': [ 82 | 'django.template.context_processors.debug', 83 | 'django.template.context_processors.request', 84 | 'django.contrib.auth.context_processors.auth', 85 | 'django.contrib.messages.context_processors.messages', 86 | ], 87 | }, 88 | }, 89 | ] 90 | 91 | WSGI_APPLICATION = 'myapp.wsgi.application' 92 | 93 | 94 | # Database 95 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 96 | 97 | DATABASES = { 98 | 'default': { 99 | 'ENGINE': 'django.db.backends.sqlite3', 100 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 101 | } 102 | } 103 | 104 | 105 | # Password validation 106 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 107 | 108 | AUTH_PASSWORD_VALIDATORS = [ 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 114 | }, 115 | { 116 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 117 | }, 118 | { 119 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 120 | }, 121 | ] 122 | 123 | # KEYCLOAK_OIDC_PROFILE_MODEL = 'django_keycloak.RemoteUserOpenIdConnectProfile' 124 | # KEYCLOAK_PERMISSIONS_METHOD = 'resource' 125 | 126 | CONTEXT_PROCESSORS = [ 127 | 128 | ] 129 | 130 | # Internationalization 131 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 132 | 133 | LANGUAGE_CODE = 'en-us' 134 | 135 | TIME_ZONE = 'UTC' 136 | 137 | USE_I18N = True 138 | 139 | USE_L10N = True 140 | 141 | USE_TZ = True 142 | 143 | 144 | # Static files (CSS, JavaScript, Images) 145 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 146 | 147 | STATIC_URL = '/static/' 148 | 149 | LOGGING = { 150 | 'version': 1, 151 | 'disable_existing_loggers': False, 152 | 'handlers': { 153 | 'console': { 154 | 'level': 'DEBUG', 155 | 'class': 'logging.StreamHandler', 156 | }, 157 | }, 158 | 'loggers': { 159 | '': { 160 | 'handlers': ['console'], 161 | 'level': 'DEBUG', 162 | }, 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /example/resource-provider/myapp/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends 'myapp/base.html' %} 2 | 3 | {% block main %} 4 |

Forbidden

5 | {% endblock %} -------------------------------------------------------------------------------- /example/resource-provider/myapp/templates/myapp/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | Keycloak authentication 13 | 14 | 15 |
16 |
17 | 34 |
35 |
36 | {% block main %}{% endblock %} 37 |
38 |
39 | {% include 'django_keycloak/includes/session_iframe_support.html' %} 40 | 41 | -------------------------------------------------------------------------------- /example/resource-provider/myapp/templates/myapp/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'myapp/base.html' %} 2 | 3 | {% block main %} 4 | 19 | {% if not user.is_authenticated %} 20 |

Login with username: testuser and password: password.

21 | {% endif %} 22 | {% endblock %} -------------------------------------------------------------------------------- /example/resource-provider/myapp/templates/myapp/permission.html: -------------------------------------------------------------------------------- 1 | {% extends 'myapp/base.html' %} 2 | 3 | {% block main %} 4 |

This page is only accessible when the user has the permission named: "some-permission"

5 | {% endblock %} -------------------------------------------------------------------------------- /example/resource-provider/myapp/templates/myapp/secured.html: -------------------------------------------------------------------------------- 1 | {% extends 'myapp/base.html' %} 2 | 3 | {% block main %} 4 |

Secured area

5 |

6 | Available permissions: 7 |

    8 | {% for perm in permissions %} 9 |
  • {{ perm }}
  • 10 | {% endfor%} 11 |
12 | 13 |

JWT:

14 |

15 | Below you can find the contents of the access_token JWT (JSON Web Token). 16 | You can also inspect the token on jwt.io 17 |

18 |
{{ jwt }}
19 | 20 |

API call result:

21 |

22 | The application made a request to the resource-provider-api. The local access token is exchanged for a token which is valid for the client configured in this application. Please read more about token exchange in the Keycloak documentation. 23 |

24 |
{{ api_result }}
25 |

26 | {% endblock %} -------------------------------------------------------------------------------- /example/resource-provider/myapp/urls.py: -------------------------------------------------------------------------------- 1 | """resource_provider URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | 19 | from myapp import views 20 | 21 | 22 | urlpatterns = [ 23 | url(r'^$', views.Home.as_view(), name='index'), 24 | url(r'^secured$', views.Secured.as_view(), name='secured'), 25 | url(r'^permission$', views.Permission.as_view(), name='permission'), 26 | url(r'^keycloak/', include('django_keycloak.urls')), 27 | url(r'^admin/', admin.site.urls), 28 | ] 29 | -------------------------------------------------------------------------------- /example/resource-provider/myapp/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import json 5 | import logging 6 | import requests 7 | 8 | from django.contrib.auth.mixins import ( 9 | LoginRequiredMixin, 10 | PermissionRequiredMixin 11 | ) 12 | from django.views.generic.base import TemplateView 13 | from jose.exceptions import JWTError 14 | 15 | import django_keycloak.services.oidc_profile 16 | import django_keycloak.services.remote_client 17 | 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | class Home(TemplateView): 23 | template_name = 'myapp/home.html' 24 | 25 | 26 | class Secured(LoginRequiredMixin, TemplateView): 27 | template_name = 'myapp/secured.html' 28 | 29 | def call_api(self): 30 | if not hasattr(self.request.user, 'oidc_profile'): 31 | return None 32 | oidc_profile = self.request.user.oidc_profile 33 | remote_client = oidc_profile.realm.remote_clients.get( 34 | name='resource-provider-api') 35 | 36 | access_token = django_keycloak.services.remote_client.get_active_remote_client_token( 37 | oidc_profile=oidc_profile, remote_client=remote_client) 38 | 39 | result = requests.get( 40 | 'https://resource-provider-api.localhost.yarf.nl/api/authenticated-end-point', 41 | verify=False, 42 | headers={ 43 | 'Authorization': 'Bearer {}'.format(access_token) 44 | } 45 | ) 46 | return result.json() 47 | 48 | def get_context_data(self, **kwargs): 49 | api_result = self.call_api() 50 | try: 51 | jwt = self.get_decoded_jwt() 52 | except JWTError: 53 | jwt = None 54 | 55 | return super(Secured, self).get_context_data( 56 | permissions=self.request.user.get_all_permissions(), 57 | access_token=self.request.user.oidc_profile.access_token, 58 | api_result=api_result, 59 | jwt=json.dumps(jwt, sort_keys=True, indent=4, separators=(',', ': ')) if jwt else None, 60 | op_location=self.request.realm.well_known_oidc['check_session_iframe'], 61 | **kwargs 62 | ) 63 | 64 | def get_decoded_jwt(self): 65 | if not hasattr(self.request.user, 'oidc_profile'): 66 | return None 67 | 68 | oidc_profile = self.request.user.oidc_profile 69 | client = oidc_profile.realm.client 70 | 71 | return client.openid_api_client.decode_token( 72 | token=oidc_profile.access_token, 73 | key=client.realm.certs, 74 | algorithms=client.openid_api_client.well_known[ 75 | 'id_token_signing_alg_values_supported'] 76 | ) 77 | 78 | 79 | class Permission(PermissionRequiredMixin, TemplateView): 80 | raise_exception = True 81 | template_name = 'myapp/permission.html' 82 | permission_required = 'some-permission' 83 | -------------------------------------------------------------------------------- /example/resource-provider/myapp/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for resource_provider project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myapp.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /example/resource-provider/requirements.txt: -------------------------------------------------------------------------------- 1 | django>=2.0.10 2 | git+git://github.com/Peter-Slump/django-keycloak.git#egg=django-keycloak 3 | django-dynamic-fixtures==0.1.7 -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = django_keycloak.tests.settings 3 | python_files = tests.py test_*.py *_tests.py 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.2-dev 3 | commit = True 4 | tag = True 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? 6 | serialize = 7 | {major}.{minor}.{patch}-{release} 8 | {major}.{minor}.{patch} 9 | 10 | [bumpversion:file:setup.py] 11 | 12 | [bumpversion:file:docs/conf.py] 13 | 14 | [bumpversion:file:sonar-project.properties] 15 | 16 | [bumpversion:file:README.rst] 17 | search = **unreleased** 18 | replace = **unreleased** 19 | **v{new_version}** 20 | 21 | [bumpversion:part:release] 22 | optional_value = gamma 23 | values = 24 | dev 25 | gamma 26 | 27 | [aliases] 28 | test = pytest 29 | 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup, find_packages 4 | 5 | VERSION = '0.1.2-dev' 6 | 7 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: 8 | README = readme.read() 9 | 10 | # allow setup.py to be run from any path 11 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 12 | 13 | setup( 14 | name='django-keycloak', 15 | version=VERSION, 16 | long_description=README, 17 | package_dir={'': 'src'}, 18 | packages=find_packages('src'), 19 | extras_require={ 20 | 'dev': [ 21 | 'bumpversion==0.5.3', 22 | 'twine', 23 | ], 24 | 'doc': [ 25 | 'Sphinx==1.4.4', 26 | 'sphinx-autobuild==0.6.0', 27 | ] 28 | }, 29 | setup_requires=[ 30 | 'pytest-runner', 31 | 'python-keycloak-client', 32 | ], 33 | install_requires=[ 34 | 'python-keycloak-client>=0.2.2', 35 | 'Django>=1.11', 36 | ], 37 | tests_require=[ 38 | 'pytest-django', 39 | 'pytest-cov', 40 | 'mock>=2.0', 41 | 'factory-boy', 42 | 'freezegun' 43 | ], 44 | url='https://github.com/Peter-Slump/django-keycloak', 45 | license='MIT', 46 | author='Peter Slump', 47 | author_email='peter@yarf.nl', 48 | description='Install Django Keycloak.', 49 | classifiers=[] 50 | 51 | ) 52 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=Peter-Slump_django-keycloak 2 | sonar.organization=peter-slump-github 3 | sonar.projectName=Django Keycloak 4 | sonar.projectVersion=0.1.2-dev 5 | 6 | # ===================================================== 7 | # Meta-data for the project 8 | # ===================================================== 9 | 10 | sonar.links.homepage=https://github.com/Peter-Slump/django-keycloak 11 | sonar.links.ci=https://github.com/Peter-Slump/django-keycloak 12 | sonar.links.scm=https://github.com/Peter-Slump/django-keycloak 13 | 14 | # ===================================================== 15 | # Properties that will be shared amongst all modules 16 | # ===================================================== 17 | 18 | # SQ standard properties 19 | sonar.sources=src 20 | sonar.exclusions=/src/tests/**/*.py 21 | sonar.python.coverage.reportPath=coverage.xml -------------------------------------------------------------------------------- /src/django_keycloak/__init__.py: -------------------------------------------------------------------------------- 1 | from . import app_settings as defaults 2 | from django.conf import settings 3 | 4 | 5 | default_app_config = 'django_keycloak.apps.KeycloakAppConfig' 6 | 7 | # Set some app default settings 8 | for name in dir(defaults): 9 | if name.isupper() and not hasattr(settings, name): 10 | setattr(settings, name, getattr(defaults, name)) 11 | -------------------------------------------------------------------------------- /src/django_keycloak/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from django_keycloak.admin.realm import RealmAdmin 4 | from django_keycloak.admin.server import ServerAdmin 5 | from django_keycloak.models import Server, Realm 6 | 7 | admin.site.register(Realm, RealmAdmin) 8 | admin.site.register(Server, ServerAdmin) 9 | -------------------------------------------------------------------------------- /src/django_keycloak/admin/realm.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin, messages 2 | from keycloak.exceptions import KeycloakClientError 3 | from requests.exceptions import HTTPError 4 | 5 | from django_keycloak.models import ( 6 | Client, 7 | OpenIdConnectProfile, 8 | RemoteClient 9 | ) 10 | import django_keycloak.services.permissions 11 | import django_keycloak.services.realm 12 | import django_keycloak.services.uma 13 | 14 | 15 | def refresh_open_id_connect_well_known(modeladmin, request, queryset): 16 | for realm in queryset: 17 | django_keycloak.services.realm.refresh_well_known_oidc(realm=realm) 18 | modeladmin.message_user( 19 | request=request, 20 | message='OpenID Connect .well-known refreshed', 21 | level=messages.SUCCESS 22 | ) 23 | 24 | 25 | refresh_open_id_connect_well_known.short_description = 'Refresh OpenID ' \ 26 | 'Connect .well-known' 27 | 28 | 29 | def refresh_certs(modeladmin, request, queryset): 30 | for realm in queryset: 31 | django_keycloak.services.realm.refresh_certs(realm=realm) 32 | modeladmin.message_user( 33 | request=request, 34 | message='Certificates refreshed', 35 | level=messages.SUCCESS 36 | ) 37 | 38 | 39 | refresh_certs.short_description = 'Refresh Certificates' 40 | 41 | 42 | def clear_client_tokens(modeladmin, request, queryset): 43 | OpenIdConnectProfile.objects.filter(realm__in=queryset).update( 44 | access_token=None, 45 | expires_before=None, 46 | refresh_token=None, 47 | refresh_expires_before=None 48 | ) 49 | modeladmin.message_user( 50 | request=request, 51 | message='Tokens cleared', 52 | level=messages.SUCCESS 53 | ) 54 | 55 | 56 | clear_client_tokens.short_description = 'Clear client tokens' 57 | 58 | 59 | def synchronize_permissions(modeladmin, request, queryset): 60 | for realm in queryset: 61 | try: 62 | django_keycloak.services.permissions.synchronize( 63 | client=realm.client) 64 | except HTTPError as e: 65 | if e.response.status_code == 403: 66 | modeladmin.message_user( 67 | request=request, 68 | message='Forbidden for {}. Does the client\'s service ' 69 | 'account has the "keycloak_client" role?'.format( 70 | realm.name 71 | ), 72 | level=messages.ERROR 73 | ) 74 | return 75 | else: 76 | raise 77 | modeladmin.message_user( 78 | request=request, 79 | message='Permissions synchronized', 80 | level=messages.SUCCESS 81 | ) 82 | 83 | 84 | synchronize_permissions.short_description = 'Synchronize permissions' 85 | 86 | 87 | def synchronize_resources(modeladmin, request, queryset): 88 | for realm in queryset: 89 | try: 90 | django_keycloak.services.uma.synchronize_client( 91 | client=realm.client) 92 | except KeycloakClientError as e: 93 | if e.original_exc.response.status_code == 400: 94 | modeladmin.message_user( 95 | request=request, 96 | message='Forbidden for {}. Is "Remote Resource ' 97 | 'Management" enabled for the related client?' 98 | .format( 99 | realm.name 100 | ), 101 | level=messages.ERROR 102 | ) 103 | return 104 | else: 105 | raise 106 | modeladmin.message_user( 107 | request=request, 108 | message='Resources synchronized', 109 | level=messages.SUCCESS 110 | ) 111 | 112 | 113 | synchronize_resources.short_description = 'Synchronize models as Keycloak ' \ 114 | 'resources' 115 | 116 | 117 | class ClientAdmin(admin.TabularInline): 118 | 119 | model = Client 120 | 121 | fields = ('client_id', 'secret') 122 | 123 | 124 | class RemoteClientAdmin(admin.TabularInline): 125 | 126 | model = RemoteClient 127 | 128 | extra = 1 129 | 130 | fields = ('name',) 131 | 132 | 133 | class RealmAdmin(admin.ModelAdmin): 134 | 135 | inlines = [ClientAdmin, RemoteClientAdmin] 136 | 137 | actions = [ 138 | refresh_open_id_connect_well_known, 139 | refresh_certs, 140 | clear_client_tokens, 141 | synchronize_permissions, 142 | synchronize_resources 143 | ] 144 | 145 | fieldsets = ( 146 | (None, { 147 | 'fields': ('name',) 148 | }), 149 | ('Location', { 150 | 'fields': ('server', '_well_known_oidc',) 151 | }) 152 | 153 | ) 154 | 155 | readonly_fields = ('_well_known_oidc',) 156 | -------------------------------------------------------------------------------- /src/django_keycloak/admin/server.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | 4 | class ServerAdmin(admin.ModelAdmin): 5 | 6 | fieldsets = ( 7 | ('Location', { 8 | 'fields': ('url', 'internal_url') 9 | }), 10 | ) 11 | -------------------------------------------------------------------------------- /src/django_keycloak/app_settings.py: -------------------------------------------------------------------------------- 1 | # Configure the model which need to be used to store the Open ID connect 2 | # profile. There are two choices: 3 | # - django_keycloak.OpenIdConnectProfile (Default) a local User object get 4 | # created for the logged in identity. 5 | # - django_keycloak.RemoteUserOpenIdConnectProfile with this model there will 6 | # be no local user stored for the logged in identity. 7 | KEYCLOAK_OIDC_PROFILE_MODEL = 'django_keycloak.OpenIdConnectProfile' 8 | 9 | # Class which will be used as User object in case of the remote user OIDC 10 | # Profile 11 | KEYCLOAK_REMOTE_USER_MODEL = 'django_keycloak.remote_user.KeycloakRemoteUser' 12 | KEYCLOAK_PERMISSIONS_METHOD = 'role' # 'role' of 'resource' 13 | -------------------------------------------------------------------------------- /src/django_keycloak/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps.config import AppConfig 2 | 3 | 4 | class KeycloakAppConfig(AppConfig): 5 | name = 'django_keycloak' 6 | verbose_name = 'Keycloak' 7 | -------------------------------------------------------------------------------- /src/django_keycloak/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import HASH_SESSION_KEY, BACKEND_SESSION_KEY, \ 2 | _get_backends 3 | from django.contrib.auth.models import AnonymousUser 4 | from django.middleware.csrf import rotate_token 5 | from django.utils import timezone 6 | 7 | import django_keycloak.services.oidc_profile 8 | 9 | 10 | # Using a different session key than the standard django.contrib.auth to 11 | # make sure there is no cross-referencing between UserModel and RemoteUserModel 12 | REMOTE_SESSION_KEY = '_auth_remote_user_id' 13 | 14 | 15 | def _get_user_session_key(request): 16 | return str(request.session[REMOTE_SESSION_KEY]) 17 | 18 | 19 | def get_remote_user(request): 20 | """ 21 | 22 | :param request: 23 | :return: 24 | """ 25 | sub = request.session.get(REMOTE_SESSION_KEY) 26 | 27 | user = None 28 | 29 | OpenIdConnectProfile = django_keycloak.services.oidc_profile\ 30 | .get_openid_connect_profile_model() 31 | 32 | try: 33 | oidc_profile = OpenIdConnectProfile.objects.get( 34 | realm=request.realm, sub=sub) 35 | except OpenIdConnectProfile.DoesNotExist: 36 | pass 37 | else: 38 | if oidc_profile.refresh_expires_before > timezone.now(): 39 | user = oidc_profile.user 40 | 41 | return user or AnonymousUser() 42 | 43 | 44 | def remote_user_login(request, user, backend=None): 45 | """ 46 | Creates a session for the user. 47 | Based on the login function django.contrib.auth.login but uses a slightly 48 | different approach since the user is not backed by a database model. 49 | :param request: 50 | :param user: 51 | :param backend: 52 | :return: 53 | """ 54 | session_auth_hash = '' 55 | if user is None: 56 | user = request.user 57 | 58 | if REMOTE_SESSION_KEY in request.session: 59 | if _get_user_session_key(request) != user.identifier: 60 | request.session.flush() 61 | else: 62 | request.session.cycle_key() 63 | 64 | try: 65 | backend = backend or user.backend 66 | except AttributeError: 67 | backends = _get_backends(return_tuples=True) 68 | if len(backends) == 1: 69 | _, backend = backends[0] 70 | else: 71 | raise ValueError( 72 | 'You have multiple authentication backends configured and ' 73 | 'therefore must provide the `backend` argument or set the ' 74 | '`backend` attribute on the user.' 75 | ) 76 | 77 | if not hasattr(user, 'identifier'): 78 | raise ValueError( 79 | 'The user does not have an identifier or the identifier is empty.' 80 | ) 81 | 82 | request.session[REMOTE_SESSION_KEY] = user.identifier 83 | request.session[BACKEND_SESSION_KEY] = backend 84 | request.session[HASH_SESSION_KEY] = session_auth_hash 85 | if hasattr(request, 'user'): 86 | request.user = user 87 | rotate_token(request) 88 | -------------------------------------------------------------------------------- /src/django_keycloak/auth/backends.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | from django.contrib.auth import get_user_model 5 | from django.core.exceptions import ImproperlyConfigured 6 | from django.utils import timezone 7 | from jose.exceptions import ( 8 | ExpiredSignatureError, 9 | JWTClaimsError, 10 | JWTError, 11 | ) 12 | from keycloak.exceptions import KeycloakClientError 13 | 14 | import django_keycloak.services.oidc_profile 15 | 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class KeycloakAuthorizationBase(object): 21 | 22 | def get_user(self, user_id): 23 | UserModel = get_user_model() 24 | 25 | try: 26 | user = UserModel.objects.select_related('oidc_profile__realm').get( 27 | pk=user_id) 28 | except UserModel.DoesNotExist: 29 | return None 30 | 31 | if user.oidc_profile.refresh_expires_before > timezone.now(): 32 | return user 33 | 34 | return None 35 | 36 | def get_all_permissions(self, user_obj, obj=None): 37 | if not user_obj.is_active or user_obj.is_anonymous or obj is not None: 38 | return set() 39 | if not hasattr(user_obj, '_keycloak_perm_cache'): 40 | user_obj._keycloak_perm_cache = self.get_keycloak_permissions( 41 | user_obj=user_obj) 42 | return user_obj._keycloak_perm_cache 43 | 44 | def get_keycloak_permissions(self, user_obj): 45 | if not hasattr(user_obj, 'oidc_profile'): 46 | return set() 47 | 48 | rpt_decoded = django_keycloak.services.oidc_profile\ 49 | .get_entitlement(oidc_profile=user_obj.oidc_profile) 50 | 51 | if settings.KEYCLOAK_PERMISSIONS_METHOD == 'role': 52 | return [ 53 | role for role in rpt_decoded['resource_access'].get( 54 | user_obj.oidc_profile.realm.client.client_id, 55 | {'roles': []} 56 | )['roles'] 57 | ] 58 | elif settings.KEYCLOAK_PERMISSIONS_METHOD == 'resource': 59 | permissions = [] 60 | for p in rpt_decoded['authorization'].get('permissions', []): 61 | if 'scopes' in p: 62 | for scope in p['scopes']: 63 | if '.' in p['resource_set_name']: 64 | app, model = p['resource_set_name'].split('.', 1) 65 | permissions.append('{app}.{scope}_{model}'.format( 66 | app=app, 67 | scope=scope, 68 | model=model 69 | )) 70 | else: 71 | permissions.append('{scope}_{resource}'.format( 72 | scope=scope, 73 | resource=p['resource_set_name'] 74 | )) 75 | else: 76 | permissions.append(p['resource_set_name']) 77 | 78 | return permissions 79 | else: 80 | raise ImproperlyConfigured( 81 | 'Unsupported permission method configured for ' 82 | 'Keycloak: {}'.format(settings.KEYCLOAK_PERMISSIONS_METHOD) 83 | ) 84 | 85 | def has_perm(self, user_obj, perm, obj=None): 86 | 87 | if not user_obj.is_active: 88 | return False 89 | 90 | granted_perms = self.get_all_permissions(user_obj, obj) 91 | return perm in granted_perms 92 | 93 | 94 | class KeycloakAuthorizationCodeBackend(KeycloakAuthorizationBase): 95 | 96 | def authenticate(self, request, code, redirect_uri): 97 | 98 | if not hasattr(request, 'realm'): 99 | raise ImproperlyConfigured( 100 | 'Add BaseKeycloakMiddleware to middlewares') 101 | 102 | keycloak_openid_profile = django_keycloak.services\ 103 | .oidc_profile.update_or_create_from_code( 104 | client=request.realm.client, 105 | code=code, 106 | redirect_uri=redirect_uri 107 | ) 108 | 109 | return keycloak_openid_profile.user 110 | 111 | 112 | class KeycloakPasswordCredentialsBackend(KeycloakAuthorizationBase): 113 | 114 | def authenticate(self, request, username, password): 115 | 116 | if not hasattr(request, 'realm'): 117 | raise ImproperlyConfigured( 118 | 'Add BaseKeycloakMiddleware to middlewares') 119 | 120 | if not request.realm: 121 | # If request.realm does exist, but it is filled with None, we 122 | # can't authenticate using Keycloak 123 | return None 124 | 125 | try: 126 | keycloak_openid_profile = django_keycloak.services\ 127 | .oidc_profile.update_or_create_from_password_credentials( 128 | client=request.realm.client, 129 | username=username, 130 | password=password 131 | ) 132 | except KeycloakClientError: 133 | logger.debug('KeycloakPasswordCredentialsBackend: failed to ' 134 | 'authenticate.') 135 | else: 136 | return keycloak_openid_profile.user 137 | 138 | return None 139 | 140 | 141 | class KeycloakIDTokenAuthorizationBackend(KeycloakAuthorizationBase): 142 | 143 | def authenticate(self, request, access_token): 144 | 145 | if not hasattr(request, 'realm'): 146 | raise ImproperlyConfigured( 147 | 'Add BaseKeycloakMiddleware to middlewares') 148 | 149 | try: 150 | oidc_profile = django_keycloak.services.oidc_profile\ 151 | .get_or_create_from_id_token( 152 | client=request.realm.client, 153 | id_token=access_token 154 | ) 155 | except ExpiredSignatureError: 156 | # If the signature has expired. 157 | logger.debug('KeycloakBearerAuthorizationBackend: failed to ' 158 | 'authenticate due to an expired access token.') 159 | except JWTClaimsError as e: 160 | logger.debug('KeycloakBearerAuthorizationBackend: failed to ' 161 | 'authenticate due to failing claim checks: "%s"' 162 | % str(e)) 163 | except JWTError: 164 | # The signature is invalid in any way. 165 | logger.debug('KeycloakBearerAuthorizationBackend: failed to ' 166 | 'authenticate due to a malformed access token.') 167 | else: 168 | return oidc_profile.user 169 | 170 | return None 171 | -------------------------------------------------------------------------------- /src/django_keycloak/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from django.contrib.auth import get_user_model 4 | 5 | from django_keycloak.models import ( 6 | Client, 7 | OpenIdConnectProfile, 8 | Realm, 9 | Server 10 | ) 11 | 12 | 13 | class UserFactory(factory.DjangoModelFactory): 14 | 15 | class Meta(object): 16 | model = get_user_model() 17 | 18 | username = factory.Faker('user_name') 19 | 20 | 21 | class ServerFactory(factory.DjangoModelFactory): 22 | 23 | class Meta(object): 24 | model = Server 25 | 26 | url = factory.Faker('url', schemes=['https']) 27 | 28 | 29 | class RealmFactory(factory.DjangoModelFactory): 30 | 31 | class Meta(object): 32 | model = Realm 33 | 34 | server = factory.SubFactory(ServerFactory) 35 | 36 | name = factory.Faker('slug') 37 | 38 | _certs = '' 39 | _well_known_oidc = '{}' 40 | 41 | client = factory.RelatedFactory('django_keycloak.factories.ClientFactory', 42 | 'realm') 43 | 44 | 45 | class OpenIdConnectProfileFactory(factory.DjangoModelFactory): 46 | 47 | class Meta(object): 48 | model = OpenIdConnectProfile 49 | 50 | sub = factory.Faker('uuid4') 51 | realm = factory.SubFactory(RealmFactory) 52 | user = factory.SubFactory(UserFactory) 53 | 54 | 55 | class ClientFactory(factory.DjangoModelFactory): 56 | 57 | class Meta(object): 58 | model = Client 59 | 60 | realm = factory.SubFactory(RealmFactory, client=None) 61 | service_account_profile = factory.SubFactory( 62 | OpenIdConnectProfileFactory, 63 | realm=factory.SelfAttribute('..realm') 64 | ) 65 | 66 | client_id = factory.Faker('slug') 67 | secret = factory.Faker('uuid4') 68 | -------------------------------------------------------------------------------- /src/django_keycloak/hashers.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from django.contrib.auth.hashers import PBKDF2PasswordHasher 4 | 5 | 6 | class PBKDF2SHA512PasswordHasher(PBKDF2PasswordHasher): 7 | 8 | algorithm = "pbkdf2_sha512" 9 | digest = hashlib.sha512 10 | -------------------------------------------------------------------------------- /src/django_keycloak/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/management/__init__.py -------------------------------------------------------------------------------- /src/django_keycloak/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/management/commands/__init__.py -------------------------------------------------------------------------------- /src/django_keycloak/management/commands/keycloak_add_user.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import logging 4 | 5 | from django.core.management.base import BaseCommand 6 | from django.contrib.auth import get_user_model 7 | 8 | from django_keycloak.models import Realm 9 | 10 | import django_keycloak.services.users 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def realm(name): 16 | try: 17 | return Realm.objects.get(name=name) 18 | except Realm.DoesNotExist: 19 | raise TypeError('Realm does not exist') 20 | 21 | 22 | def user(username): 23 | UserModel = get_user_model() 24 | try: 25 | return UserModel.objects.get(username=username) 26 | except UserModel.DoesNotExist: 27 | raise TypeError('User does not exist') 28 | 29 | 30 | class Command(BaseCommand): 31 | 32 | def add_arguments(self, parser): 33 | parser.add_argument('--realm', type=realm, required=True) 34 | parser.add_argument('--user', type=user, required=True) 35 | 36 | def handle(self, *args, **options): 37 | user = options['user'] 38 | realm = options['realm'] 39 | 40 | django_keycloak.services.users.add_user(client=realm.client, user=user) 41 | -------------------------------------------------------------------------------- /src/django_keycloak/management/commands/keycloak_refresh_realm.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import logging 4 | 5 | from django.core.management.base import BaseCommand 6 | 7 | from django_keycloak.models import Realm 8 | 9 | import django_keycloak.services.realm 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class Command(BaseCommand): 15 | 16 | def handle(self, *args, **options): 17 | for realm in Realm.objects.all(): 18 | django_keycloak.services.realm.refresh_well_known_oidc(realm=realm) 19 | django_keycloak.services.realm.refresh_certs(realm=realm) 20 | logger.debug('Refreshed: {}'.format(realm)) 21 | -------------------------------------------------------------------------------- /src/django_keycloak/management/commands/keycloak_sync_resources.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import logging 4 | 5 | from django.core.management.base import BaseCommand 6 | 7 | from django_keycloak.models import Client 8 | 9 | import django_keycloak.services.uma 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def client(client_id): 15 | try: 16 | return Client.objects.get(client_id=client_id) 17 | except Client.DoesNotExist: 18 | raise TypeError('Client does not exist') 19 | 20 | 21 | class Command(BaseCommand): 22 | 23 | def add_arguments(self, parser): 24 | parser.add_argument('--client', type=client, required=False) 25 | 26 | def handle(self, *args, **options): 27 | client = options.get('client') 28 | 29 | if client: 30 | django_keycloak.services.uma.synchronize_client(client=client) 31 | else: 32 | for client in Client.objects.all(): 33 | django_keycloak.services.uma.synchronize_client(client=client) 34 | -------------------------------------------------------------------------------- /src/django_keycloak/middleware.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.conf import settings 4 | from django.contrib.auth import authenticate 5 | from django.contrib.auth.models import AnonymousUser 6 | from django.utils.deprecation import MiddlewareMixin 7 | from django.utils.functional import SimpleLazyObject 8 | 9 | from django_keycloak.models import Realm 10 | from django_keycloak.auth import get_remote_user 11 | from django_keycloak.response import HttpResponseNotAuthorized 12 | 13 | 14 | def get_realm(request): 15 | if not hasattr(request, '_cached_realm'): 16 | request._cached_realm = Realm.objects.first() 17 | return request._cached_realm 18 | 19 | 20 | def get_user(request, origin_user): 21 | # Check for the user as set by 22 | # django.contrib.auth.middleware.AuthenticationMiddleware 23 | if not isinstance(origin_user, AnonymousUser): 24 | return origin_user 25 | 26 | if not hasattr(request, '_cached_user'): 27 | request._cached_user = get_remote_user(request) 28 | return request._cached_user 29 | 30 | 31 | class BaseKeycloakMiddleware(MiddlewareMixin): 32 | 33 | set_session_state_cookie = True 34 | 35 | def process_request(self, request): 36 | """ 37 | Adds Realm to request. 38 | :param request: django request 39 | """ 40 | request.realm = SimpleLazyObject(lambda: get_realm(request)) 41 | 42 | def process_response(self, request, response): 43 | 44 | if self.set_session_state_cookie: 45 | return self.set_session_state_cookie_(request, response) 46 | 47 | return response 48 | 49 | def set_session_state_cookie_(self, request, response): 50 | 51 | if not request.user.is_authenticated \ 52 | or not hasattr(request.user, 'oidc_profile'): 53 | return response 54 | 55 | jwt = request.user.oidc_profile.jwt 56 | if not jwt: 57 | return response 58 | 59 | cookie_name = getattr(settings, 'KEYCLOAK_SESSION_STATE_COOKIE_NAME', 60 | 'session_state') 61 | 62 | # Set a browser readable cookie which expires when the refresh token 63 | # expires. 64 | response.set_cookie( 65 | cookie_name, value=jwt['session_state'], 66 | expires=request.user.oidc_profile.refresh_expires_before, 67 | httponly=False 68 | ) 69 | 70 | return response 71 | 72 | 73 | class KeycloakStatelessBearerAuthenticationMiddleware(BaseKeycloakMiddleware): 74 | 75 | set_session_state_cookie = False 76 | header_key = "HTTP_AUTHORIZATION" 77 | 78 | def process_request(self, request): 79 | """ 80 | Forces authentication on all requests except the URL's configured in 81 | the exempt setting. 82 | """ 83 | super(KeycloakStatelessBearerAuthenticationMiddleware, self)\ 84 | .process_request(request=request) 85 | 86 | if hasattr(settings, 'KEYCLOAK_BEARER_AUTHENTICATION_EXEMPT_PATHS'): 87 | path = request.path_info.lstrip('/') 88 | 89 | if any(re.match(m, path) for m in 90 | settings.KEYCLOAK_BEARER_AUTHENTICATION_EXEMPT_PATHS): 91 | return 92 | 93 | if self.header_key not in request.META: 94 | return HttpResponseNotAuthorized( 95 | attributes={'realm': request.realm.name}) 96 | 97 | user = authenticate( 98 | request=request, 99 | access_token=request.META[self.header_key].split(' ')[1] 100 | ) 101 | 102 | if user is None: 103 | return HttpResponseNotAuthorized( 104 | attributes={'realm': request.realm.name}) 105 | else: 106 | request.user = user 107 | 108 | 109 | class RemoteUserAuthenticationMiddleware(MiddlewareMixin): 110 | set_session_state_cookie = False 111 | 112 | def process_request(self, request): 113 | """ 114 | Adds user to the request when authorized user is found in the session 115 | :param django.http.request.HttpRequest request: django request 116 | """ 117 | origin_user = getattr(request, 'user', None) 118 | 119 | request.user = SimpleLazyObject(lambda: get_user( 120 | request, 121 | origin_user=origin_user 122 | )) 123 | -------------------------------------------------------------------------------- /src/django_keycloak/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-03-15 21:15 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import uuid 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Client', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, 22 | serialize=False, verbose_name='ID')), 23 | ('client_id', models.CharField(max_length=255)), 24 | ('secret', models.CharField(max_length=255)), 25 | ], 26 | ), 27 | migrations.CreateModel( 28 | name='Nonce', 29 | fields=[ 30 | ('id', models.AutoField(auto_created=True, primary_key=True, 31 | serialize=False, verbose_name='ID')), 32 | ('state', models.UUIDField(default=uuid.uuid4, unique=True)), 33 | ('redirect_uri', models.CharField(max_length=255)), 34 | ('next_path', models.CharField(max_length=255, null=True)), 35 | ], 36 | ), 37 | migrations.CreateModel( 38 | name='OpenIdConnectProfile', 39 | fields=[ 40 | ('id', models.AutoField(auto_created=True, primary_key=True, 41 | serialize=False, verbose_name='ID')), 42 | ('access_token', models.TextField(null=True)), 43 | ('expires_before', models.DateTimeField(null=True)), 44 | ('refresh_token', models.TextField(null=True)), 45 | ('refresh_expires_before', models.DateTimeField(null=True)), 46 | ('sub', models.CharField(max_length=255, unique=True)), 47 | ], 48 | options={ 49 | 'abstract': False, 50 | 'swappable': 'KEYCLOAK_OIDC_PROFILE_MODEL', 51 | }, 52 | ), 53 | migrations.CreateModel( 54 | name='Realm', 55 | fields=[ 56 | ('id', models.AutoField(auto_created=True, primary_key=True, 57 | serialize=False, verbose_name='ID')), 58 | ('name', models.CharField( 59 | help_text='Name as known on the Keycloak server. This ' 60 | 'name is used in the API paths of this Realm.', 61 | max_length=255, unique=True)), 62 | ('_certs', models.TextField()), 63 | ('_well_known_oidc', models.TextField(blank=True)), 64 | ], 65 | ), 66 | migrations.CreateModel( 67 | name='Role', 68 | fields=[ 69 | ('id', models.AutoField(auto_created=True, primary_key=True, 70 | serialize=False, verbose_name='ID')), 71 | ('reference', models.CharField(max_length=50)), 72 | ('client', models.ForeignKey( 73 | on_delete=django.db.models.deletion.CASCADE, 74 | related_name='roles', to='django_keycloak.Client')), 75 | ('permission', models.ForeignKey( 76 | on_delete=django.db.models.deletion.CASCADE, 77 | to='auth.Permission')), 78 | ], 79 | ), 80 | migrations.CreateModel( 81 | name='Server', 82 | fields=[ 83 | ('id', models.AutoField(auto_created=True, primary_key=True, 84 | serialize=False, verbose_name='ID')), 85 | ('url', models.CharField(max_length=255)), 86 | ('internal_url', models.CharField( 87 | blank=True, 88 | help_text='URL on internal netwerk calls. For example ' 89 | 'when used with Docker Compose. Only supply ' 90 | 'when internal calls should go to a different ' 91 | 'url as the end-user will communicate with.', 92 | max_length=255, null=True)), 93 | ], 94 | ), 95 | migrations.AddField( 96 | model_name='realm', 97 | name='server', 98 | field=models.ForeignKey( 99 | on_delete=django.db.models.deletion.CASCADE, 100 | related_name='realms', to='django_keycloak.Server'), 101 | ), 102 | migrations.AddField( 103 | model_name='openidconnectprofile', 104 | name='realm', 105 | field=models.ForeignKey( 106 | on_delete=django.db.models.deletion.CASCADE, 107 | related_name='openid_profiles', to='django_keycloak.Realm'), 108 | ), 109 | migrations.AddField( 110 | model_name='openidconnectprofile', 111 | name='user', 112 | field=models.OneToOneField( 113 | on_delete=django.db.models.deletion.CASCADE, 114 | related_name='oidc_profile', to=settings.AUTH_USER_MODEL), 115 | ), 116 | migrations.AddField( 117 | model_name='client', 118 | name='realm', 119 | field=models.OneToOneField( 120 | on_delete=django.db.models.deletion.CASCADE, 121 | related_name='client', to='django_keycloak.Realm'), 122 | ), 123 | migrations.AddField( 124 | model_name='client', 125 | name='service_account', 126 | field=models.OneToOneField( 127 | null=True, on_delete=django.db.models.deletion.CASCADE, 128 | related_name='keycloak_client', to=settings.AUTH_USER_MODEL), 129 | ), 130 | migrations.AlterUniqueTogether( 131 | name='role', 132 | unique_together={('client', 'permission')}, 133 | ), 134 | ] 135 | -------------------------------------------------------------------------------- /src/django_keycloak/migrations/0002_auto_20180322_2059.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.3 on 2018-03-22 20:59 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_keycloak', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ExchangedToken', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, 18 | serialize=False, verbose_name='ID')), 19 | ('access_token', models.TextField(null=True)), 20 | ('expires_before', models.DateTimeField(null=True)), 21 | ('refresh_token', models.TextField(null=True)), 22 | ('refresh_expires_before', models.DateTimeField(null=True)), 23 | ('oidc_profile', models.ForeignKey( 24 | on_delete=django.db.models.deletion.CASCADE, 25 | to='django_keycloak.OpenIdConnectProfile')), 26 | ], 27 | ), 28 | migrations.CreateModel( 29 | name='RemoteClient', 30 | fields=[ 31 | ('id', models.AutoField(auto_created=True, primary_key=True, 32 | serialize=False, verbose_name='ID')), 33 | ('name', models.CharField(max_length=255)), 34 | ('realm', models.ForeignKey( 35 | on_delete=django.db.models.deletion.CASCADE, 36 | related_name='remote_clients', 37 | to='django_keycloak.Realm' 38 | )), 39 | ], 40 | ), 41 | migrations.AddField( 42 | model_name='exchangedtoken', 43 | name='remote_client', 44 | field=models.ForeignKey( 45 | on_delete=django.db.models.deletion.CASCADE, 46 | related_name='exchanged_tokens', 47 | to='django_keycloak.RemoteClient' 48 | ), 49 | ), 50 | migrations.AlterUniqueTogether( 51 | name='exchangedtoken', 52 | unique_together={('oidc_profile', 'remote_client')}, 53 | ), 54 | ] 55 | -------------------------------------------------------------------------------- /src/django_keycloak/migrations/0003_auto_20190204_1949.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-02-04 19:49 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_keycloak', '0002_auto_20180322_2059'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='RemoteUserOpenIdConnectProfile', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, 19 | serialize=False, verbose_name='ID')), 20 | ('access_token', models.TextField(null=True)), 21 | ('expires_before', models.DateTimeField(null=True)), 22 | ('refresh_token', models.TextField(null=True)), 23 | ('refresh_expires_before', models.DateTimeField(null=True)), 24 | ('sub', models.CharField(max_length=255, unique=True)), 25 | ('realm', models.ForeignKey( 26 | on_delete=django.db.models.deletion.CASCADE, 27 | related_name='openid_profiles', 28 | to='django_keycloak.Realm' 29 | )), 30 | ], 31 | options={ 32 | 'abstract': False, 33 | 'swappable': 'KEYCLOAK_OIDC_PROFILE_MODEL', 34 | }, 35 | ), 36 | migrations.AlterField( 37 | model_name='exchangedtoken', 38 | name='oidc_profile', 39 | field=models.ForeignKey( 40 | on_delete=django.db.models.deletion.CASCADE, 41 | to=settings.KEYCLOAK_OIDC_PROFILE_MODEL 42 | ), 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /src/django_keycloak/migrations/0004_client_service_account_profile.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-02-19 13:23 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('django_keycloak', '0003_auto_20190204_1949'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='client', 17 | name='service_account_profile', 18 | field=models.OneToOneField( 19 | null=True, 20 | on_delete=django.db.models.deletion.CASCADE, 21 | to=settings.KEYCLOAK_OIDC_PROFILE_MODEL 22 | ), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /src/django_keycloak/migrations/0005_auto_20190219_2002.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-02-19 20:02 2 | 3 | from django.db import migrations 4 | 5 | 6 | def forward(apps, schema_editor): 7 | Client = apps.get_model('django_keycloak', 'Client') 8 | for client in Client.objects.filter(service_account__isnull=False): 9 | client.service_account_profile = client.service_account.oidc_profile 10 | client.save() 11 | 12 | 13 | def backward(apps, schema_editor): 14 | Client = apps.get_model('django_keycloak', 'Client') 15 | for client in Client.objects.filter(service_account_profile__isnull=False): 16 | client.service_account = client.service_account_profile.user 17 | client.save() 18 | 19 | 20 | class Migration(migrations.Migration): 21 | 22 | dependencies = [ 23 | ('django_keycloak', '0004_client_service_account_profile'), 24 | ] 25 | 26 | operations = [ 27 | migrations.RunPython(forward, backward), 28 | ] 29 | -------------------------------------------------------------------------------- /src/django_keycloak/migrations/0006_remove_client_service_account.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-02-19 20:22 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_keycloak', '0005_auto_20190219_2002'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='client', 15 | name='service_account', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/django_keycloak/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/migrations/__init__.py -------------------------------------------------------------------------------- /src/django_keycloak/models.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import uuid 4 | 5 | from django.contrib.auth.models import Permission 6 | from django.db import models 7 | from django.conf import settings 8 | from django.utils import timezone 9 | from django.utils.functional import cached_property 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class Server(models.Model): 15 | 16 | url = models.CharField(max_length=255) 17 | 18 | internal_url = models.CharField( 19 | max_length=255, 20 | blank=True, 21 | null=True, 22 | help_text='URL on internal netwerk calls. For example when used with ' 23 | 'Docker Compose. Only supply when internal calls should go ' 24 | 'to a different url as the end-user will communicate with.' 25 | ) 26 | 27 | def __str__(self): 28 | return self.url 29 | 30 | 31 | class Realm(models.Model): 32 | 33 | server = models.ForeignKey(Server, related_name='realms', 34 | on_delete=models.CASCADE) 35 | 36 | name = models.CharField(max_length=255, unique=True, 37 | help_text='Name as known on the Keycloak server. ' 38 | 'This name is used in the API paths ' 39 | 'of this Realm.') 40 | _certs = models.TextField() 41 | 42 | @property 43 | def certs(self): 44 | return json.loads(self._certs) 45 | 46 | @certs.setter 47 | def certs(self, content): 48 | self._certs = json.dumps(content) 49 | 50 | _well_known_oidc = models.TextField(blank=True) 51 | 52 | @property 53 | def well_known_oidc(self): 54 | return json.loads(self._well_known_oidc) 55 | 56 | @well_known_oidc.setter 57 | def well_known_oidc(self, content): 58 | self._well_known_oidc = json.dumps(content) 59 | 60 | _keycloak_realm = None 61 | 62 | @cached_property 63 | def realm_api_client(self): 64 | """ 65 | :rtype: keycloak.realm.Realm 66 | """ 67 | if self._keycloak_realm is None: 68 | import django_keycloak.services.realm 69 | self._keycloak_realm = django_keycloak.services.realm.\ 70 | get_realm_api_client(realm=self) 71 | return self._keycloak_realm 72 | 73 | def __str__(self): 74 | return self.name 75 | 76 | 77 | class Client(models.Model): 78 | 79 | realm = models.OneToOneField(Realm, related_name='client', 80 | on_delete=models.CASCADE) 81 | 82 | client_id = models.CharField(max_length=255) 83 | secret = models.CharField(max_length=255) 84 | 85 | service_account_profile = models.OneToOneField( 86 | settings.KEYCLOAK_OIDC_PROFILE_MODEL, 87 | on_delete=models.CASCADE, 88 | null=True 89 | ) 90 | 91 | @cached_property 92 | def admin_api_client(self): 93 | """ 94 | :rtype: keycloak.admin.KeycloakAdmin 95 | """ 96 | import django_keycloak.services.client 97 | return django_keycloak.services.client.get_admin_client(client=self) 98 | 99 | @cached_property 100 | def openid_api_client(self): 101 | """ 102 | :rtype: keycloak.openid_connect.KeycloakOpenidConnect 103 | """ 104 | import django_keycloak.services.client 105 | return django_keycloak.services.client.get_openid_client(client=self) 106 | 107 | @cached_property 108 | def authz_api_client(self): 109 | """ 110 | :rtype: keycloak.authz.KeycloakAuthz 111 | """ 112 | import django_keycloak.services.client 113 | return django_keycloak.services.client.get_authz_api_client( 114 | client=self) 115 | 116 | @cached_property 117 | def uma1_api_client(self): 118 | """ 119 | :rtype: keycloak.uma1.KeycloakUMA1 120 | """ 121 | import django_keycloak.services.client 122 | return django_keycloak.services.client.get_uma1_client(client=self) 123 | 124 | def __str__(self): 125 | return self.client_id 126 | 127 | 128 | class Role(models.Model): 129 | 130 | client = models.ForeignKey('django_keycloak.Client', 131 | related_name='roles', 132 | on_delete=models.CASCADE) 133 | permission = models.ForeignKey(Permission, 134 | on_delete=models.CASCADE) 135 | reference = models.CharField(max_length=50) 136 | 137 | class Meta(object): 138 | unique_together = ( 139 | ('client', 'permission') 140 | ) 141 | 142 | 143 | class TokenModelAbstract(models.Model): 144 | 145 | access_token = models.TextField(null=True) 146 | expires_before = models.DateTimeField(null=True) 147 | 148 | refresh_token = models.TextField(null=True) 149 | refresh_expires_before = models.DateTimeField(null=True) 150 | 151 | class Meta(object): 152 | abstract = True 153 | 154 | @property 155 | def is_active(self): 156 | if not self.access_token or not self.expires_before: 157 | return False 158 | 159 | return self.expires_before > timezone.now() 160 | 161 | 162 | class OpenIdConnectProfileAbstract(TokenModelAbstract): 163 | 164 | sub = models.CharField(max_length=255, unique=True) 165 | 166 | realm = models.ForeignKey('django_keycloak.Realm', 167 | related_name='openid_profiles', 168 | on_delete=models.CASCADE) 169 | 170 | class Meta(object): 171 | abstract = True 172 | 173 | @property 174 | def jwt(self): 175 | """ 176 | :rtype: dict 177 | """ 178 | if not self.is_active: 179 | return None 180 | client = self.realm.client 181 | return client.openid_api_client.decode_token( 182 | token=self.access_token, 183 | key=client.realm.certs, 184 | algorithms=client.openid_api_client.well_known[ 185 | 'id_token_signing_alg_values_supported'] 186 | ) 187 | 188 | 189 | class RemoteUserOpenIdConnectProfile(OpenIdConnectProfileAbstract): 190 | 191 | is_remote = True 192 | _user = None 193 | 194 | class Meta(OpenIdConnectProfileAbstract.Meta): 195 | swappable = 'KEYCLOAK_OIDC_PROFILE_MODEL' 196 | 197 | def get_user(self): 198 | if self._user is None: 199 | import django_keycloak.services.oidc_profile 200 | self._user = django_keycloak.services.oidc_profile. \ 201 | get_remote_user_from_profile( 202 | oidc_profile=self 203 | ) 204 | return self._user 205 | 206 | def set_user(self, user): 207 | import django_keycloak.services.oidc_profile 208 | RemoteUserModel = django_keycloak.services.oidc_profile\ 209 | .get_remote_user_model() 210 | if not isinstance(user, RemoteUserModel): 211 | raise RuntimeError('Can\'t set a non-remote user to the {}'.format( 212 | type(self))) 213 | 214 | self._user = user 215 | 216 | user = property(get_user, set_user) 217 | 218 | 219 | class OpenIdConnectProfile(OpenIdConnectProfileAbstract): 220 | 221 | is_remote = False 222 | 223 | user = models.OneToOneField(settings.AUTH_USER_MODEL, 224 | related_name='oidc_profile', 225 | on_delete=models.CASCADE) 226 | 227 | class Meta(RemoteUserOpenIdConnectProfile.Meta): 228 | swappable = 'KEYCLOAK_OIDC_PROFILE_MODEL' 229 | 230 | 231 | class Nonce(models.Model): 232 | 233 | state = models.UUIDField(default=uuid.uuid4, unique=True) 234 | redirect_uri = models.CharField(max_length=255) 235 | next_path = models.CharField(max_length=255, null=True) 236 | 237 | 238 | class ExchangedToken(TokenModelAbstract): 239 | 240 | oidc_profile = models.ForeignKey( 241 | settings.KEYCLOAK_OIDC_PROFILE_MODEL, 242 | on_delete=models.CASCADE 243 | ) 244 | remote_client = models.ForeignKey('django_keycloak.RemoteClient', 245 | related_name='exchanged_tokens', 246 | on_delete=models.CASCADE) 247 | 248 | class Meta(object): 249 | unique_together = ( 250 | ('oidc_profile', 'remote_client'), 251 | ) 252 | 253 | 254 | class RemoteClient(models.Model): 255 | 256 | name = models.CharField(max_length=255) 257 | realm = models.ForeignKey('django_keycloak.Realm', 258 | related_name='remote_clients', 259 | on_delete=models.CASCADE) 260 | -------------------------------------------------------------------------------- /src/django_keycloak/remote_user.py: -------------------------------------------------------------------------------- 1 | from django.contrib import auth 2 | from django.core.exceptions import PermissionDenied 3 | 4 | from django_keycloak.models import RemoteUserOpenIdConnectProfile 5 | 6 | 7 | class KeycloakRemoteUser(object): 8 | """ 9 | A class based on django.contrib.auth.models.User. 10 | See https://docs.djangoproject.com/en/2.0/ref/contrib/auth/ 11 | #django.contrib.auth.models.User 12 | """ 13 | 14 | username = '' 15 | first_name = '' 16 | last_name = '' 17 | email = '' 18 | password = '' 19 | groups = [] 20 | user_permissions = [] 21 | 22 | _last_login = None 23 | 24 | def __init__(self, userinfo): 25 | """ 26 | Create KeycloakRemoteUser from userinfo and oidc_profile. 27 | :param dict userinfo: the userinfo as retrieved from the OIDC provider 28 | """ 29 | self.username = userinfo.get('preferred_username') or userinfo['sub'] 30 | self.email = userinfo.get('email', '') 31 | self.first_name = userinfo.get('given_name', '') 32 | self.last_name = userinfo.get('family_name', '') 33 | self.sub = userinfo['sub'] 34 | 35 | def __str__(self): 36 | return self.username 37 | 38 | @property 39 | def pk(self): 40 | """ 41 | Since the BaseAbstractUser is a model, every instance needs a primary 42 | key. The Django authentication backend requires this. 43 | """ 44 | return 0 45 | 46 | @property 47 | def identifier(self): 48 | """ 49 | Identifier used for session storage. 50 | :rtype: str 51 | """ 52 | return self.sub 53 | 54 | @property 55 | def is_staff(self): 56 | """ 57 | :rtype: bool 58 | :return: whether the user is a staff member or not, defaults to False 59 | """ 60 | return False 61 | 62 | @property 63 | def is_active(self): 64 | """ 65 | :rtype: bool 66 | :return: whether the user is active or not, defaults to True 67 | """ 68 | return True 69 | 70 | @property 71 | def is_superuser(self): 72 | """ 73 | :rtype: bool 74 | :return: whether the user is a superuser or not, defaults to False 75 | """ 76 | return False 77 | 78 | @property 79 | def last_login(self): 80 | """ 81 | :rtype: Datetime 82 | :return: the date and time of the last login 83 | """ 84 | return self._last_login 85 | 86 | @last_login.setter 87 | def last_login(self, content): 88 | """ 89 | :param datetime content: 90 | """ 91 | self._last_login = content 92 | 93 | @property 94 | def is_authenticated(self): 95 | """ 96 | Read-only attribute which is always True. 97 | See https://docs.djangoproject.com/en/2.0/ref/contrib/auth/ 98 | #django.contrib.auth.models.User.is_authenticated 99 | :return: 100 | """ 101 | return True 102 | 103 | @property 104 | def is_anonymous(self): 105 | """ 106 | Read-only attribute which is always False. 107 | See https://docs.djangoproject.com/en/2.0/ref/contrib/auth/ 108 | #django.contrib.auth.models.User.is_anonymous 109 | :return: 110 | """ 111 | return False 112 | 113 | def get_username(self): 114 | """ 115 | Get the username 116 | :return: username 117 | """ 118 | return self.username 119 | 120 | def get_full_name(self): 121 | """ 122 | Get the full name (first name + last name) of the user. 123 | :return: the first name and last name of the user 124 | """ 125 | return "{first} {last}".format(first=self.first_name, 126 | last=self.last_name) 127 | 128 | def get_short_name(self): 129 | """ 130 | Get the first name of the user. 131 | :return: first name 132 | """ 133 | return self.first_name 134 | 135 | def get_group_permissions(self, obj=None): 136 | pass 137 | 138 | def get_all_permissions(self, obj=None): 139 | """ 140 | Logic from django.contrib.auth.models._user_get_all_permissions 141 | :param perm: 142 | :param obj: 143 | :return: 144 | """ 145 | permissions = set() 146 | for backend in auth.get_backends(): 147 | # Excluding Django.contrib.auth backends since they are not 148 | # compatible with non-db-backed permissions. 149 | if hasattr(backend, "get_all_permissions") \ 150 | and not backend.__module__.startswith('django.'): 151 | permissions.update(backend.get_all_permissions(self, obj)) 152 | return permissions 153 | 154 | @property 155 | def oidc_profile(self): 156 | """ 157 | Get the related OIDC Profile for this user. 158 | :rtype: django_keycloak.models.RemoteUserOpenIdConnectProfile 159 | :return: OpenID Connect Profile 160 | """ 161 | try: 162 | return RemoteUserOpenIdConnectProfile.objects.get(sub=self.sub) 163 | except RemoteUserOpenIdConnectProfile.DoesNotExist: 164 | return None 165 | 166 | def has_perm(self, perm, obj=None): 167 | """ 168 | Logic from django.contrib.auth.models._user_has_perm 169 | :param perm: 170 | :param obj: 171 | :return: 172 | """ 173 | for backend in auth.get_backends(): 174 | if not hasattr(backend, 'has_perm') \ 175 | or backend.__module__.startswith('django.contrib.auth'): 176 | continue 177 | try: 178 | if backend.has_perm(self, perm, obj): 179 | return True 180 | except PermissionDenied: 181 | return False 182 | return False 183 | 184 | def has_perms(self, perm_list, obj=None): 185 | return all(self.has_perm(perm, obj) for perm in perm_list) 186 | 187 | def has_module_perms(self, module): 188 | """ 189 | Logic from django.contrib.auth.models._user_has_module_perms 190 | :param module: 191 | :return: 192 | """ 193 | for backend in auth.get_backends(): 194 | if not hasattr(backend, 'has_module_perms'): 195 | continue 196 | try: 197 | if backend.has_module_perms(self, module): 198 | return True 199 | except PermissionDenied: 200 | return False 201 | return False 202 | 203 | def email_user(self, subject, message, from_email=None, **kwargs): 204 | raise NotImplementedError('This feature is not implemented by default,' 205 | ' extend this class to implement') 206 | 207 | def save(self): 208 | """ 209 | Normally implemented by django.db.models.Model 210 | :raises NotImplementedError: to remind that this is not a 211 | database-backed model and should not be used like one 212 | """ 213 | raise NotImplementedError('This is not a database model') 214 | -------------------------------------------------------------------------------- /src/django_keycloak/response.py: -------------------------------------------------------------------------------- 1 | from django.http.response import HttpResponse 2 | 3 | 4 | class HttpResponseNotAuthorized(HttpResponse): 5 | status_code = 401 6 | 7 | def __init__(self, content=b'', authorization_method='Bearer', 8 | attributes=None, *args, **kwargs): 9 | super(HttpResponseNotAuthorized, self).__init__(content, *args, 10 | **kwargs) 11 | 12 | attributes = attributes or {} 13 | attributes_str = ', '.join( 14 | [ 15 | '{}="{}"'.format(key, value) 16 | for key, value in attributes.items() 17 | ] 18 | ) 19 | 20 | self['WWW-Authenticate'] = '{} {}'.format(authorization_method, 21 | attributes_str) 22 | -------------------------------------------------------------------------------- /src/django_keycloak/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/services/__init__.py -------------------------------------------------------------------------------- /src/django_keycloak/services/client.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from functools import partial 4 | 5 | from django.utils import timezone 6 | 7 | from django_keycloak.services.exceptions import TokensExpired 8 | 9 | import django_keycloak.services.oidc_profile 10 | 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def get_keycloak_id(client): 16 | """ 17 | Get internal Keycloak id for client configured in Realm 18 | :param django_keycloak.models.Realm realm: 19 | :return: 20 | """ 21 | keycloak_clients = client.admin_api_client.realms.by_name( 22 | name=client.realm.name).clients.all() 23 | for keycloak_client in keycloak_clients: 24 | if keycloak_client['clientId'] == client.client_id: 25 | return keycloak_client['id'] 26 | 27 | return None 28 | 29 | 30 | def get_authz_api_client(client): 31 | """ 32 | :param django_keycloak.models.Client client: 33 | :rtype: keycloak.authz.KeycloakAuthz 34 | """ 35 | return client.realm.realm_api_client.authz(client_id=client.client_id) 36 | 37 | 38 | def get_openid_client(client): 39 | """ 40 | :param django_keycloak.models.Client client: 41 | :rtype: keycloak.openid_connect.KeycloakOpenidConnect 42 | """ 43 | openid = client.realm.realm_api_client.open_id_connect( 44 | client_id=client.client_id, 45 | client_secret=client.secret 46 | ) 47 | 48 | if client.realm._well_known_oidc: 49 | openid.well_known.contents = client.realm.well_known_oidc 50 | 51 | return openid 52 | 53 | 54 | def get_uma1_client(client): 55 | """ 56 | :type client: django_keycloak.models.Client 57 | :rtype: keycloak.uma1.KeycloakUMA1 58 | """ 59 | return client.realm.realm_api_client.uma1 60 | 61 | 62 | def get_admin_client(client): 63 | """ 64 | Get the Keycloak admin client configured for given realm. 65 | 66 | :param django_keycloak.models.Client client: 67 | :rtype: keycloak.admin.KeycloakAdmin 68 | """ 69 | token = partial(get_access_token, client) 70 | return client.realm.realm_api_client.admin.set_token(token=token) 71 | 72 | 73 | def get_service_account_profile(client): 74 | """ 75 | Get service account for given client. 76 | 77 | :param django_keycloak.models.Client client: 78 | :rtype: django_keycloak.models.OpenIdConnectProfile 79 | """ 80 | 81 | if client.service_account_profile: 82 | return client.service_account_profile 83 | 84 | token_response, initiate_time = get_new_access_token(client=client) 85 | 86 | oidc_profile = django_keycloak.services.oidc_profile._update_or_create( 87 | client=client, 88 | token_response=token_response, 89 | initiate_time=initiate_time) 90 | 91 | client.service_account_profile = oidc_profile 92 | client.save(update_fields=['service_account_profile']) 93 | 94 | return oidc_profile 95 | 96 | 97 | def get_new_access_token(client): 98 | """ 99 | Get client access_token 100 | 101 | :param django_keycloak.models.Client client: 102 | :rtype: str 103 | """ 104 | scope = 'realm-management openid' 105 | 106 | initiate_time = timezone.now() 107 | token_response = client.openid_api_client.client_credentials(scope=scope) 108 | 109 | return token_response, initiate_time 110 | 111 | 112 | def get_access_token(client): 113 | """ 114 | Get access token from client's service account. 115 | :param django_keycloak.models.Client client: 116 | :rtype: str 117 | """ 118 | 119 | oidc_profile = get_service_account_profile(client=client) 120 | 121 | try: 122 | return django_keycloak.services.oidc_profile.get_active_access_token( 123 | oidc_profile=oidc_profile) 124 | except TokensExpired: 125 | token_reponse, initiate_time = get_new_access_token(client=client) 126 | oidc_profile = django_keycloak.services.oidc_profile.update_tokens( 127 | token_model=oidc_profile, 128 | token_response=token_reponse, 129 | initiate_time=initiate_time 130 | ) 131 | return oidc_profile.access_token 132 | -------------------------------------------------------------------------------- /src/django_keycloak/services/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class KeycloakOpenIdProfileNotFound(Exception): 4 | pass 5 | 6 | 7 | class TokensExpired(Exception): 8 | pass 9 | -------------------------------------------------------------------------------- /src/django_keycloak/services/permissions.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.contrib.auth.models import Permission 4 | from requests.exceptions import HTTPError 5 | 6 | import django_keycloak.services.client 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def synchronize(client): 12 | """ 13 | :param django_keycloak.models.Client client: 14 | :return: 15 | """ 16 | keycloak_client_id = django_keycloak.services.client.get_keycloak_id( 17 | client=client) 18 | 19 | role_api = client.admin_api_client.realms.by_name(client.realm.name)\ 20 | .clients.by_id(keycloak_client_id).roles 21 | 22 | for permission in Permission.objects.all(): 23 | 24 | try: 25 | role_api.create(name=permission.codename, 26 | description=permission.name) 27 | except HTTPError as e: 28 | if e.response.status_code != 409: 29 | raise 30 | 31 | else: 32 | continue 33 | 34 | # Update role 35 | role_api.by_name(permission.codename) \ 36 | .update(name=permission.codename, description=permission.name) 37 | -------------------------------------------------------------------------------- /src/django_keycloak/services/realm.py: -------------------------------------------------------------------------------- 1 | from keycloak.realm import KeycloakRealm 2 | 3 | try: 4 | from urllib.parse import urlparse 5 | except ImportError: 6 | from urlparse import urlparse 7 | 8 | 9 | def get_realm_api_client(realm): 10 | """ 11 | :param django_keycloak.models.Realm realm: 12 | :return keycloak.realm.Realm: 13 | """ 14 | headers = {} 15 | server_url = realm.server.url 16 | if realm.server.internal_url: 17 | # An internal URL is configured. We add some additional settings to let 18 | # Keycloak think that we access it using the server_url. 19 | server_url = realm.server.internal_url 20 | parsed_url = urlparse(realm.server.url) 21 | headers['Host'] = parsed_url.netloc 22 | 23 | if parsed_url.scheme == 'https': 24 | headers['X-Forwarded-Proto'] = 'https' 25 | 26 | return KeycloakRealm(server_url=server_url, realm_name=realm.name, 27 | headers=headers) 28 | 29 | 30 | def refresh_certs(realm): 31 | """ 32 | :param django_keycloak.models.Realm realm: 33 | :rtype django_keycloak.models.Realm 34 | """ 35 | realm.certs = realm.client.openid_api_client.certs() 36 | realm.save(update_fields=['_certs']) 37 | return realm 38 | 39 | 40 | def refresh_well_known_oidc(realm): 41 | """ 42 | Refresh Open ID Connect .well-known 43 | 44 | :param django_keycloak.models.Realm realm: 45 | :rtype django_keycloak.models.Realm 46 | """ 47 | server_url = realm.server.internal_url or realm.server.url 48 | 49 | # While fetching the well_known we should not use the prepared URL 50 | openid_api_client = KeycloakRealm( 51 | server_url=server_url, 52 | realm_name=realm.name 53 | ).open_id_connect(client_id='', client_secret='') 54 | 55 | realm.well_known_oidc = openid_api_client.well_known.contents 56 | realm.save(update_fields=['_well_known_oidc']) 57 | return realm 58 | 59 | 60 | def get_issuer(realm): 61 | """ 62 | Get correct issuer to validate the JWT against. If an internal URL is 63 | configured for the server it will be replaced with the public one. 64 | 65 | :param django_keycloak.models.Realm realm: 66 | :return: issuer 67 | :rtype: str 68 | """ 69 | issuer = realm.well_known_oidc['issuer'] 70 | if realm.server.internal_url: 71 | return issuer.replace(realm.server.internal_url, realm.server.url, 1) 72 | return issuer 73 | -------------------------------------------------------------------------------- /src/django_keycloak/services/remote_client.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.utils import timezone 4 | 5 | from django_keycloak.models import ExchangedToken 6 | 7 | import django_keycloak.services.oidc_profile 8 | 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | def exchange_token(oidc_profile, remote_client): 14 | """ 15 | Exchange access token from OpenID Connect profile for a token of given 16 | remote client. 17 | 18 | :param django_keycloak.models.OpenIdConnectProfile oidc_profile: 19 | :param django_keycloak.models.RemoteClient remote_client: 20 | :rtype: dict 21 | """ 22 | active_access_token = django_keycloak.services.oidc_profile\ 23 | .get_active_access_token(oidc_profile=oidc_profile) 24 | 25 | # http://www.keycloak.org/docs/latest/securing_apps/index.html#_token-exchange 26 | return oidc_profile.realm.client.openid_api_client.token_exchange( 27 | audience=remote_client.name, 28 | subject_token=active_access_token, 29 | requested_token_type='urn:ietf:params:oauth:token-type:refresh_token' 30 | ) 31 | 32 | 33 | def get_active_remote_client_token(oidc_profile, remote_client): 34 | """ 35 | Get an active remote client token. Exchange when not available or expired. 36 | 37 | :param django_keycloak.models.OpenIdConnectProfile oidc_profile: 38 | :param django_keycloak.models.RemoteClient remote_client: 39 | :rtype: str 40 | """ 41 | exchanged_token, _ = ExchangedToken.objects.get_or_create( 42 | oidc_profile=oidc_profile, 43 | remote_client=remote_client 44 | ) 45 | 46 | initiate_time = timezone.now() 47 | 48 | if exchanged_token.refresh_expires_before is None \ 49 | or initiate_time > exchanged_token.refresh_expires_before \ 50 | or initiate_time > exchanged_token.expires_before: 51 | token_response = exchange_token(oidc_profile, remote_client) 52 | 53 | exchanged_token = django_keycloak.services.oidc_profile.update_tokens( 54 | token_model=exchanged_token, 55 | token_response=token_response, 56 | initiate_time=initiate_time 57 | ) 58 | 59 | return exchanged_token.access_token 60 | -------------------------------------------------------------------------------- /src/django_keycloak/services/uma.py: -------------------------------------------------------------------------------- 1 | from django.apps.registry import apps 2 | from django.utils.text import slugify 3 | 4 | from keycloak.exceptions import KeycloakClientError 5 | 6 | import django_keycloak.services.client 7 | 8 | 9 | def synchronize_client(client): 10 | """ 11 | Synchronize all models as resources for a client. 12 | 13 | :type client: django_keycloak.models.Client 14 | """ 15 | for app_config in apps.get_app_configs(): 16 | synchronize_resources( 17 | client=client, 18 | app_config=app_config 19 | ) 20 | 21 | 22 | def synchronize_resources(client, app_config): 23 | """ 24 | Synchronize all resources (models) to the Keycloak server for given client 25 | and Django App. 26 | 27 | :type client: django_keycloak.models.Client 28 | :type app_config: django.apps.config.AppConfig 29 | """ 30 | 31 | if not app_config.models_module: 32 | return 33 | 34 | uma1_client = client.uma1_api_client 35 | 36 | access_token = django_keycloak.services.client.get_access_token( 37 | client=client 38 | ) 39 | 40 | for klass in app_config.get_models(): 41 | scopes = _get_all_permissions(klass._meta) 42 | 43 | try: 44 | uma1_client.resource_set_create( 45 | token=access_token, 46 | name=klass._meta.label_lower, 47 | type='urn:{client}:resources:{model}'.format( 48 | client=slugify(client.client_id), 49 | model=klass._meta.label_lower 50 | ), 51 | scopes=scopes 52 | ) 53 | except KeycloakClientError as e: 54 | if e.original_exc.response.status_code != 409: 55 | raise 56 | 57 | 58 | def _get_all_permissions(meta): 59 | """ 60 | :type meta: django.db.models.options.Options 61 | :rtype: list 62 | """ 63 | return meta.default_permissions 64 | -------------------------------------------------------------------------------- /src/django_keycloak/services/users.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | 4 | def credential_representation_from_hash(hash_, temporary=False): 5 | algorithm, hashIterations, salt, hashedSaltedValue = hash_.split('$') 6 | 7 | return { 8 | 'type': 'password', 9 | 'hashedSaltedValue': hashedSaltedValue, 10 | 'algorithm': algorithm.replace('_', '-'), 11 | 'hashIterations': int(hashIterations), 12 | 'salt': base64.b64encode(salt.encode()).decode('ascii').strip(), 13 | 'temporary': temporary 14 | } 15 | 16 | 17 | def add_user(client, user): 18 | """ 19 | Create user in Keycloak based on a local user including password. 20 | 21 | :param django_keycloak.models.Client client: 22 | :param django.contrib.auth.models.User user: 23 | """ 24 | credentials = credential_representation_from_hash(hash_=user.password) 25 | 26 | client.admin_api_client.realms.by_name(client.realm.name).users.create( 27 | username=user.username, 28 | credentials=credentials, 29 | first_name=user.first_name, 30 | last_name=user.last_name, 31 | email=user.email, 32 | enabled=user.is_active 33 | ) 34 | -------------------------------------------------------------------------------- /src/django_keycloak/templates/django_keycloak/includes/session_iframe_support.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/django_keycloak/templates/django_keycloak/session_iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/django_keycloak/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/tests/__init__.py -------------------------------------------------------------------------------- /src/django_keycloak/tests/backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/tests/backends/__init__.py -------------------------------------------------------------------------------- /src/django_keycloak/tests/backends/keycloak_authorization_base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/tests/backends/keycloak_authorization_base/__init__.py -------------------------------------------------------------------------------- /src/django_keycloak/tests/backends/keycloak_authorization_base/test_get_keycloak_permissions.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, override_settings 2 | 3 | from django_keycloak.factories import OpenIdConnectProfileFactory 4 | from django_keycloak.tests.mixins import MockTestCaseMixin 5 | from django_keycloak.auth.backends import KeycloakAuthorizationBase 6 | 7 | 8 | @override_settings(KEYCLOAK_PERMISSIONS_METHOD='resource') 9 | class BackendsKeycloakAuthorizationBaseGetKeycloakPermissionsTestCase( 10 | MockTestCaseMixin, TestCase): 11 | 12 | def setUp(self): 13 | self.backend = KeycloakAuthorizationBase() 14 | 15 | self.profile = OpenIdConnectProfileFactory() 16 | 17 | self.setup_mock( 18 | 'django_keycloak.services.oidc_profile.get_entitlement', 19 | return_value={ 20 | 'authorization': { 21 | 'permissions': [ 22 | { 23 | 'resource_set_name': 'Resource', 24 | 'scopes': [ 25 | 'Read', 26 | 'Update' 27 | ] 28 | }, 29 | { 30 | 'resource_set_name': 'Resource2' 31 | } 32 | ] 33 | } 34 | }) 35 | 36 | def test_get_keycloak_permissions(self): 37 | """ 38 | Case: The permissions are requested from Keycloak, which are returned 39 | by get_entitlement as a decoded RPT. 40 | Expect: The permissions are extracted from the RPT and are returned 41 | in a list. 42 | """ 43 | permissions = self.backend.get_keycloak_permissions( 44 | user_obj=self.profile.user) 45 | 46 | self.assertListEqual( 47 | ['Read_Resource', 'Update_Resource', 'Resource2'], 48 | permissions 49 | ) 50 | -------------------------------------------------------------------------------- /src/django_keycloak/tests/backends/keycloak_authorization_base/test_has_perm.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, override_settings 2 | 3 | from django_keycloak.factories import OpenIdConnectProfileFactory 4 | from django_keycloak.tests.mixins import MockTestCaseMixin 5 | from django_keycloak.auth.backends import KeycloakAuthorizationBase 6 | 7 | 8 | @override_settings(KEYCLOAK_PERMISSIONS_METHOD='resource') 9 | class BackendsKeycloakAuthorizationBaseHasPermTestCase( 10 | MockTestCaseMixin, TestCase): 11 | 12 | def setUp(self): 13 | self.backend = KeycloakAuthorizationBase() 14 | 15 | self.profile = OpenIdConnectProfileFactory(user__is_active=True) 16 | 17 | self.setup_mock( 18 | 'django_keycloak.services.oidc_profile.get_entitlement', 19 | return_value={ 20 | 'authorization': { 21 | 'permissions': [ 22 | { 23 | 'resource_set_name': 'Resource', 24 | 'scopes': [ 25 | 'Read', 26 | 'Update' 27 | ] 28 | }, 29 | { 30 | 'resource_set_name': 'Resource2' 31 | } 32 | ] 33 | } 34 | } 35 | ) 36 | 37 | def test_resource_scope_should_have_permission(self): 38 | """ 39 | Case: Permission is expected that is available to the user. 40 | Expected: Permission granted. 41 | """ 42 | permission = self.backend.has_perm( 43 | user_obj=self.profile.user, perm='Read_Resource') 44 | 45 | self.assertTrue(permission) 46 | 47 | def test_resource_no_scope_should_not_have_permission(self): 48 | """" 49 | Case: Permission is formatted as resource only which does not exist as 50 | such in the RPT. 51 | Expected: Permission denied. 52 | """ 53 | permission = self.backend.has_perm( 54 | user_obj=self.profile.user, perm='Resource') 55 | 56 | self.assertFalse(permission) 57 | 58 | def test_resource_other_scope_should_not_have_permission(self): 59 | """" 60 | Case: Permission is expected with a scope that is not available to 61 | the user according to the RPT. 62 | Expected: Permission denied. 63 | """ 64 | permission = self.backend.has_perm( 65 | user_obj=self.profile.user, perm='Create_Resource') 66 | 67 | self.assertFalse(permission) 68 | 69 | def test_other_resource_other_scope_should_not_have_permission(self): 70 | """" 71 | Case: Permission is expected that is not available to the user 72 | according to the RPT. 73 | Expected: Permission denied. 74 | """ 75 | permission = self.backend.has_perm( 76 | user_obj=self.profile.user, perm='OtherScope_OtherResource') 77 | 78 | self.assertFalse(permission) 79 | 80 | def test_resource_no_scope_should_have_permission(self): 81 | """" 82 | Case: Permission is expected with no scope provided, but scope is 83 | also not provided in the RPT. 84 | Expected: Permission granted. 85 | """ 86 | permission = self.backend.has_perm( 87 | user_obj=self.profile.user, perm='Resource2') 88 | 89 | self.assertTrue(permission) 90 | -------------------------------------------------------------------------------- /src/django_keycloak/tests/mixins.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | 4 | class MockTestCaseMixin(object): 5 | 6 | def __init__(self, *args, **kwargs): 7 | self._mocks = {} 8 | 9 | super(MockTestCaseMixin, self).__init__(*args, **kwargs) 10 | 11 | def setup_mock(self, target, autospec=True, **kwargs): 12 | if target in self._mocks: 13 | raise Exception('Target %s already patched', target) 14 | 15 | self._mocks[target] = mock.patch(target, autospec=autospec, **kwargs) 16 | 17 | return self._mocks[target].start() 18 | 19 | def tearDown(self): 20 | for mock_ in self._mocks.values(): 21 | mock_.stop() 22 | 23 | return super(MockTestCaseMixin, self).tearDown() 24 | -------------------------------------------------------------------------------- /src/django_keycloak/tests/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/tests/services/__init__.py -------------------------------------------------------------------------------- /src/django_keycloak/tests/services/oidc_profile/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/tests/services/oidc_profile/__init__.py -------------------------------------------------------------------------------- /src/django_keycloak/tests/services/oidc_profile/test_get_active_access_token.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | from datetime import datetime 4 | 5 | from django.test import TestCase 6 | from freezegun import freeze_time 7 | from keycloak.openid_connect import KeycloakOpenidConnect 8 | 9 | from django_keycloak.factories import OpenIdConnectProfileFactory 10 | from django_keycloak.tests.mixins import MockTestCaseMixin 11 | 12 | import django_keycloak.services.oidc_profile 13 | 14 | 15 | class ServicesKeycloakOpenIDProfileGetActiveAccessTokenTestCase( 16 | MockTestCaseMixin, TestCase): 17 | 18 | def setUp(self): 19 | self.oidc_profile = OpenIdConnectProfileFactory( 20 | access_token='access-token', 21 | expires_before=datetime(2018, 3, 5, 1, 0, 0), 22 | refresh_token='refresh-token', 23 | refresh_expires_before=datetime(2018, 3, 5, 2, 0, 0) 24 | ) 25 | self.oidc_profile.realm.client.openid_api_client = mock.MagicMock( 26 | spec_set=KeycloakOpenidConnect) 27 | self.oidc_profile.realm.client.openid_api_client.refresh_token\ 28 | .return_value = { 29 | 'access_token': 'new-access-token', 30 | 'expires_in': 600, 31 | 'refresh_token': 'new-refresh-token', 32 | 'refresh_expires_in': 3600 33 | } 34 | 35 | @freeze_time('2018-03-05 00:59:00') 36 | def test_not_expired(self): 37 | """ 38 | Case: access token get fetched and is not yet expired 39 | Expected: current token is returned 40 | """ 41 | access_token = django_keycloak.services.oidc_profile\ 42 | .get_active_access_token(oidc_profile=self.oidc_profile) 43 | 44 | self.assertEqual(access_token, 'access-token') 45 | self.assertFalse( 46 | self.oidc_profile.realm.client.openid_api_client.refresh_token 47 | .called 48 | ) 49 | 50 | @freeze_time('2018-03-05 01:01:00') 51 | def test_expired(self): 52 | """ 53 | Case: access token get requested but current one is expired 54 | Expected: A new one get requested 55 | """ 56 | access_token = django_keycloak.services.oidc_profile \ 57 | .get_active_access_token(oidc_profile=self.oidc_profile) 58 | 59 | self.assertEqual(access_token, 'new-access-token') 60 | self.oidc_profile.realm.client.openid_api_client.refresh_token\ 61 | .assert_called_once_with(refresh_token='refresh-token') 62 | 63 | self.oidc_profile.refresh_from_db() 64 | self.assertEqual(self.oidc_profile.access_token, 'new-access-token') 65 | self.assertEqual(self.oidc_profile.expires_before, 66 | datetime(2018, 3, 5, 1, 11, 0)) 67 | self.assertEqual(self.oidc_profile.refresh_token, 'new-refresh-token') 68 | self.assertEqual(self.oidc_profile.refresh_expires_before, 69 | datetime(2018, 3, 5, 2, 1, 0)) 70 | -------------------------------------------------------------------------------- /src/django_keycloak/tests/services/oidc_profile/test_get_entitlement.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | from datetime import datetime 4 | 5 | from django.test import TestCase 6 | from keycloak.openid_connect import KeycloakOpenidConnect 7 | from keycloak.authz import KeycloakAuthz 8 | 9 | from django_keycloak.factories import OpenIdConnectProfileFactory 10 | from django_keycloak.tests.mixins import MockTestCaseMixin 11 | 12 | import django_keycloak.services.oidc_profile 13 | 14 | 15 | class ServicesKeycloakOpenIDProfileGetActiveAccessTokenTestCase( 16 | MockTestCaseMixin, TestCase): 17 | 18 | def setUp(self): 19 | self.mocked_get_active_access_token = self.setup_mock( 20 | 'django_keycloak.services.oidc_profile' 21 | '.get_active_access_token' 22 | ) 23 | 24 | self.oidc_profile = OpenIdConnectProfileFactory( 25 | access_token='access-token', 26 | expires_before=datetime(2018, 3, 5, 1, 0, 0), 27 | refresh_token='refresh-token' 28 | ) 29 | self.oidc_profile.realm.client.openid_api_client = mock.MagicMock( 30 | spec_set=KeycloakOpenidConnect) 31 | self.oidc_profile.realm.client.authz_api_client = mock.MagicMock( 32 | spec_set=KeycloakAuthz) 33 | self.oidc_profile.realm.client.authz_api_client.entitlement\ 34 | .return_value = { 35 | 'rpt': 'RPT_VALUE' 36 | } 37 | self.oidc_profile.realm.certs = {'cert': 'cert-value'} 38 | 39 | def test(self): 40 | django_keycloak.services.oidc_profile.get_entitlement( 41 | oidc_profile=self.oidc_profile 42 | ) 43 | self.oidc_profile.realm.client.authz_api_client.entitlement\ 44 | .assert_called_once_with( 45 | token=self.mocked_get_active_access_token.return_value 46 | ) 47 | self.oidc_profile.realm.client.openid_api_client.decode_token\ 48 | .assert_called_once_with( 49 | token='RPT_VALUE', 50 | key=self.oidc_profile.realm.certs, 51 | options={ 52 | 'verify_signature': True, 53 | 'exp': True, 54 | 'iat': True, 55 | 'aud': True 56 | } 57 | ) 58 | -------------------------------------------------------------------------------- /src/django_keycloak/tests/services/oidc_profile/test_get_or_create_from_id_token.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | from datetime import datetime 4 | 5 | from django.test import TestCase 6 | from keycloak.openid_connect import KeycloakOpenidConnect 7 | 8 | from django_keycloak.factories import ClientFactory, \ 9 | OpenIdConnectProfileFactory, UserFactory 10 | from django_keycloak.tests.mixins import MockTestCaseMixin 11 | 12 | import django_keycloak.services.oidc_profile 13 | 14 | 15 | class ServicesOpenIDProfileGetOrCreateFromIdTokenTestCase( 16 | MockTestCaseMixin, TestCase): 17 | 18 | def setUp(self): 19 | self.client = ClientFactory( 20 | realm___certs='{}', 21 | realm___well_known_oidc='{"issuer": "https://issuer"}' 22 | ) 23 | self.client.openid_api_client = mock.MagicMock( 24 | spec_set=KeycloakOpenidConnect) 25 | self.client.openid_api_client.well_known = { 26 | 'id_token_signing_alg_values_supported': ['signing-alg'] 27 | } 28 | self.client.openid_api_client.decode_token.return_value = { 29 | 'sub': 'some-sub', 30 | 'email': 'test@example.com', 31 | 'given_name': 'Some given name', 32 | 'family_name': 'Some family name' 33 | } 34 | 35 | def test_create_with_new_user_new_profile(self): 36 | """ 37 | Case: oidc profile is requested based on a provided id token. 38 | The user and profile do not exist yet. 39 | Expected: oidc profile and user are created with information from 40 | the id token. 41 | """ 42 | profile = django_keycloak.services.oidc_profile. \ 43 | get_or_create_from_id_token( 44 | client=self.client, id_token='some-id-token' 45 | ) 46 | 47 | self.client.openid_api_client.decode_token.assert_called_with( 48 | token='some-id-token', 49 | key=dict(), 50 | algorithms=['signing-alg'], 51 | issuer='https://issuer' 52 | ) 53 | 54 | self.assertEqual(profile.sub, 'some-sub') 55 | self.assertEqual(profile.user.username, 'some-sub') 56 | self.assertEqual(profile.user.email, 'test@example.com') 57 | self.assertEqual(profile.user.first_name, 'Some given name') 58 | self.assertEqual(profile.user.last_name, 'Some family name') 59 | 60 | def test_update_with_existing_profile_new_user(self): 61 | """ 62 | Case: oidc profile is requested based on a provided id token. 63 | The profile exists, but the user doesn't. 64 | Expected: oidc user is created with information from the id token 65 | and linked to the profile. 66 | """ 67 | existing_profile = OpenIdConnectProfileFactory( 68 | access_token='access-token', 69 | expires_before=datetime(2018, 3, 5, 1, 0, 0), 70 | refresh_token='refresh-token', 71 | sub='some-sub' 72 | ) 73 | 74 | profile = django_keycloak.services.oidc_profile. \ 75 | get_or_create_from_id_token( 76 | client=self.client, id_token='some-id-token' 77 | ) 78 | 79 | self.client.openid_api_client.decode_token.assert_called_with( 80 | token='some-id-token', 81 | key=dict(), 82 | algorithms=['signing-alg'], 83 | issuer='https://issuer' 84 | ) 85 | 86 | self.assertEqual(profile.sub, 'some-sub') 87 | self.assertEqual(profile.pk, existing_profile.pk) 88 | self.assertEqual(profile.user.username, 'some-sub') 89 | self.assertEqual(profile.user.email, 'test@example.com') 90 | self.assertEqual(profile.user.first_name, 'Some given name') 91 | self.assertEqual(profile.user.last_name, 'Some family name') 92 | 93 | def test_create_with_existing_user_new_profile(self): 94 | """ 95 | Case: oidc profile is requested based on a provided id token. 96 | The user exists, but the profile doesn't. 97 | Expected: oidc profile is created and user is linked to the profile. 98 | """ 99 | existing_user = UserFactory( 100 | username='some-sub' 101 | ) 102 | 103 | profile = django_keycloak.services.oidc_profile.\ 104 | get_or_create_from_id_token( 105 | client=self.client, id_token='some-id-token' 106 | ) 107 | 108 | self.client.openid_api_client.decode_token.assert_called_with( 109 | token='some-id-token', 110 | key=dict(), 111 | algorithms=['signing-alg'], 112 | issuer='https://issuer' 113 | ) 114 | 115 | self.assertEqual(profile.sub, 'some-sub') 116 | self.assertEqual(profile.user.pk, existing_user.pk) 117 | self.assertEqual(profile.user.username, 'some-sub') 118 | self.assertEqual(profile.user.email, 'test@example.com') 119 | self.assertEqual(profile.user.first_name, 'Some given name') 120 | self.assertEqual(profile.user.last_name, 'Some family name') 121 | 122 | def test_create_with_existing_user_existing_profile(self): 123 | """ 124 | Case: oidc profile is requested based on a provided id token. 125 | The user and profile already exist. 126 | Expected: existing oidc profile is returned with existing user linked 127 | to it. 128 | """ 129 | existing_user = UserFactory( 130 | username='some-sub' 131 | ) 132 | 133 | existing_profile = OpenIdConnectProfileFactory( 134 | access_token='access-token', 135 | expires_before=datetime(2018, 3, 5, 1, 0, 0), 136 | refresh_token='refresh-token', 137 | sub='some-sub' 138 | ) 139 | 140 | profile = django_keycloak.services.oidc_profile.\ 141 | get_or_create_from_id_token( 142 | client=self.client, id_token='some-id-token' 143 | ) 144 | 145 | self.client.openid_api_client.decode_token.assert_called_with( 146 | token='some-id-token', 147 | key=dict(), 148 | algorithms=['signing-alg'], 149 | issuer='https://issuer' 150 | ) 151 | 152 | self.assertEqual(profile.pk, existing_profile.pk) 153 | self.assertEqual(profile.sub, 'some-sub') 154 | self.assertEqual(profile.user.pk, existing_user.pk) 155 | self.assertEqual(profile.user.username, 'some-sub') 156 | self.assertEqual(profile.user.email, 'test@example.com') 157 | self.assertEqual(profile.user.first_name, 'Some given name') 158 | self.assertEqual(profile.user.last_name, 'Some family name') 159 | -------------------------------------------------------------------------------- /src/django_keycloak/tests/services/oidc_profile/test_update_or_create.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | from datetime import datetime 4 | 5 | from django.contrib.auth import get_user_model 6 | from django.test import TestCase 7 | from freezegun import freeze_time 8 | from keycloak.openid_connect import KeycloakOpenidConnect 9 | 10 | from django_keycloak.factories import ClientFactory 11 | from django_keycloak.models import OpenIdConnectProfile 12 | from django_keycloak.tests.mixins import MockTestCaseMixin 13 | 14 | import django_keycloak.services.oidc_profile 15 | 16 | 17 | class ServicesKeycloakOpenIDProfileUpdateOrCreateTestCase(MockTestCaseMixin, 18 | TestCase): 19 | 20 | def setUp(self): 21 | self.client = ClientFactory( 22 | realm___certs='{}', 23 | realm___well_known_oidc='{"issuer": "https://issuer"}' 24 | ) 25 | self.client.openid_api_client = mock.MagicMock( 26 | spec_set=KeycloakOpenidConnect) 27 | self.client.openid_api_client.authorization_code.return_value = { 28 | 'id_token': 'id-token', 29 | 'expires_in': 600, 30 | 'refresh_expires_in': 3600, 31 | 'access_token': 'access-token', 32 | 'refresh_token': 'refresh-token' 33 | } 34 | self.client.openid_api_client.well_known = { 35 | 'id_token_signing_alg_values_supported': ['signing-alg'] 36 | } 37 | self.client.openid_api_client.decode_token.return_value = { 38 | 'sub': 'some-sub', 39 | 'email': 'test@example.com', 40 | 'given_name': 'Some given name', 41 | 'family_name': 'Some family name' 42 | } 43 | 44 | @freeze_time('2018-03-01 00:00:00') 45 | def test_create(self): 46 | django_keycloak.services.oidc_profile.update_or_create_from_code( 47 | client=self.client, 48 | code='some-code', 49 | redirect_uri='https://redirect' 50 | ) 51 | self.client.openid_api_client.authorization_code\ 52 | .assert_called_once_with(code='some-code', 53 | redirect_uri='https://redirect') 54 | self.client.openid_api_client.decode_token.assert_called_once_with( 55 | token='id-token', 56 | key=dict(), 57 | algorithms=['signing-alg'], 58 | issuer='https://issuer' 59 | ) 60 | 61 | profile = OpenIdConnectProfile.objects.get(sub='some-sub') 62 | self.assertEqual(profile.sub, 'some-sub'), 63 | self.assertEqual(profile.access_token, 'access-token') 64 | self.assertEqual(profile.refresh_token, 'refresh-token') 65 | self.assertEqual(profile.expires_before, datetime( 66 | year=2018, month=3, day=1, hour=0, minute=10, second=0 67 | )) 68 | self.assertEqual(profile.refresh_expires_before, datetime( 69 | year=2018, month=3, day=1, hour=1, minute=0, second=0 70 | )) 71 | 72 | user = profile.user 73 | self.assertEqual(user.username, 'some-sub') 74 | self.assertEqual(user.first_name, 'Some given name') 75 | self.assertEqual(user.last_name, 'Some family name') 76 | 77 | @freeze_time('2018-03-01 00:00:00') 78 | def test_update(self): 79 | UserModel = get_user_model() 80 | user = UserModel.objects.create( 81 | username='some-sub', 82 | email='', 83 | first_name='', 84 | last_name='' 85 | ) 86 | profile = OpenIdConnectProfile.objects.create( 87 | realm=self.client.realm, 88 | sub='some-sub', 89 | user=user, 90 | access_token='another-access-token', 91 | expires_before=datetime.now(), 92 | refresh_token='another-refresh-token', 93 | refresh_expires_before=datetime.now() 94 | ) 95 | 96 | django_keycloak.services.oidc_profile.update_or_create_from_code( 97 | client=self.client, 98 | code='some-code', 99 | redirect_uri='https://redirect' 100 | ) 101 | self.client.openid_api_client.authorization_code\ 102 | .assert_called_once_with(code='some-code', 103 | redirect_uri='https://redirect') 104 | self.client.openid_api_client.decode_token.assert_called_once_with( 105 | token='id-token', 106 | key=dict(), 107 | algorithms=['signing-alg'], 108 | issuer='https://issuer' 109 | ) 110 | 111 | profile.refresh_from_db() 112 | self.assertEqual(profile.sub, 'some-sub') 113 | self.assertEqual(profile.access_token, 'access-token') 114 | self.assertEqual(profile.refresh_token, 'refresh-token') 115 | self.assertEqual(profile.expires_before, datetime( 116 | year=2018, month=3, day=1, hour=0, minute=10, second=0 117 | )) 118 | self.assertEqual(profile.refresh_expires_before, datetime( 119 | year=2018, month=3, day=1, hour=1, minute=0, second=0 120 | )) 121 | 122 | user = profile.user 123 | user.refresh_from_db() 124 | self.assertEqual(user.username, 'some-sub') 125 | self.assertEqual(user.first_name, 'Some given name') 126 | self.assertEqual(user.last_name, 'Some family name') 127 | -------------------------------------------------------------------------------- /src/django_keycloak/tests/services/realm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peter-Slump/django-keycloak/5211fba26ae4216b4344309eb4a67da9745ada05/src/django_keycloak/tests/services/realm/__init__.py -------------------------------------------------------------------------------- /src/django_keycloak/tests/services/realm/test_get_realm_api_client.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from django_keycloak.factories import ServerFactory, RealmFactory 4 | from django_keycloak.tests.mixins import MockTestCaseMixin 5 | 6 | import django_keycloak.services.realm 7 | 8 | 9 | class ServicesRealmGetRealmApiClientTestCase( 10 | MockTestCaseMixin, TestCase): 11 | 12 | def setUp(self): 13 | self.server = ServerFactory( 14 | url='https://some-url', 15 | internal_url='' 16 | ) 17 | 18 | self.realm = RealmFactory( 19 | server=self.server, 20 | name='test-realm' 21 | ) 22 | 23 | def test_get_realm_api_client(self): 24 | """ 25 | Case: a realm api client is requested for a realm on a server without 26 | internal_url. 27 | Expected: a KeycloakRealm client is returned with settings based on the 28 | provided realm. The server_url in the client is the provided url. 29 | """ 30 | client = django_keycloak.services.realm.\ 31 | get_realm_api_client(realm=self.realm) 32 | 33 | self.assertEqual(client.server_url, self.server.url) 34 | self.assertEqual(client.realm_name, self.realm.name) 35 | 36 | def test_get_realm_api_client_with_internal_url(self): 37 | """ 38 | Case: a realm api client is requested for a realm on a server with 39 | internal_url. 40 | Expected: a KeycloakRealm client is returned with settings based on the 41 | provided realm. The server_url in the client is the provided url. 42 | """ 43 | self.server.internal_url = 'https://some-internal-url' 44 | 45 | client = django_keycloak.services.realm.\ 46 | get_realm_api_client(realm=self.realm) 47 | 48 | self.assertEqual(client.server_url, self.server.internal_url) 49 | self.assertEqual(client.realm_name, self.realm.name) 50 | -------------------------------------------------------------------------------- /src/django_keycloak/tests/services/realm/test_refresh_well_known_oidc.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | from django.test import TestCase 4 | 5 | from django_keycloak.factories import RealmFactory 6 | from django_keycloak.tests.mixins import MockTestCaseMixin 7 | 8 | import django_keycloak.services.realm 9 | 10 | 11 | class ServicesRealmRefreshWellKnownOIDCTestCase( 12 | MockTestCaseMixin, TestCase): 13 | 14 | def setUp(self): 15 | self.realm = RealmFactory( 16 | name='test-realm', 17 | _well_known_oidc='empty' 18 | ) 19 | 20 | keycloak_oidc_mock = mock.MagicMock() 21 | keycloak_oidc_mock.well_known.contents = {'key': 'value'} 22 | self.setup_mock('keycloak.realm.KeycloakRealm.open_id_connect', 23 | return_value=keycloak_oidc_mock) 24 | 25 | def test_refresh_well_known_oidc(self): 26 | """ 27 | Case: An update is requested for the .well-known for a specified realm. 28 | Expected: The .well-known is updated. 29 | """ 30 | self.assertEqual(self.realm._well_known_oidc, 'empty') 31 | 32 | django_keycloak.services.realm.refresh_well_known_oidc( 33 | realm=self.realm 34 | ) 35 | 36 | self.assertEqual(self.realm._well_known_oidc, '{"key": "value"}') 37 | -------------------------------------------------------------------------------- /src/django_keycloak/tests/settings.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django_keycloak.app_settings import * # noqa: F403,F401 4 | 5 | PASSWORD_HASHERS = ( 6 | 'django.contrib.auth.hashers.MD5PasswordHasher', 7 | ) 8 | 9 | # SECURITY WARNING: keep the secret key used in production secret! 10 | SECRET_KEY = 'secret-key' 11 | 12 | # SECURITY WARNING: don't run with debug turned on in production! 13 | DEBUG = False 14 | 15 | LOGIN_URL = 'keycloak_login' 16 | LOGOUT_REDIRECT_URL = 'index' 17 | 18 | # Application definition 19 | INSTALLED_APPS = [ 20 | 'django.contrib.auth', 21 | 'django.contrib.contenttypes', 22 | 23 | 'django_keycloak.apps.KeycloakAppConfig', 24 | ] 25 | 26 | MIDDLEWARE = [ 27 | 'django_keycloak.middleware.BaseKeycloakMiddleware', 28 | ] 29 | 30 | AUTHENTICATION_BACKENDS = [ 31 | 'django.contrib.auth.backends.ModelBackend', 32 | 'django_keycloak.auth.backends.KeycloakAuthorizationCodeBackend', 33 | ] 34 | 35 | DATABASES = { 36 | 'default': { 37 | 'ENGINE': 'django.db.backends.sqlite3', 38 | 'NAME': ':memory:', 39 | } 40 | } 41 | 42 | logging.disable(logging.CRITICAL) 43 | -------------------------------------------------------------------------------- /src/django_keycloak/urls.py: -------------------------------------------------------------------------------- 1 | """resource_provider URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | 18 | from django_keycloak import views 19 | 20 | urlpatterns = [ 21 | url(r'^login$', views.Login.as_view(), name='keycloak_login'), 22 | url(r'^login-complete$', views.LoginComplete.as_view(), 23 | name='keycloak_login_complete'), 24 | url(r'^logout$', views.Logout.as_view(), name='keycloak_logout'), 25 | url(r'^session-iframe', views.SessionIframe.as_view(), 26 | name='keycloak_session_iframe') 27 | ] 28 | -------------------------------------------------------------------------------- /src/django_keycloak/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import logging 5 | 6 | from django.shortcuts import resolve_url 7 | 8 | from django_keycloak.services.oidc_profile import get_remote_user_model 9 | 10 | try: 11 | from urllib.parse import urljoin # noqa: F401 12 | except ImportError: 13 | from urlparse import urljoin # noqa: F401 14 | 15 | from django.conf import settings 16 | from django.contrib.auth import authenticate, login, logout 17 | from django.http.response import ( 18 | HttpResponseBadRequest, 19 | HttpResponseServerError, 20 | HttpResponseRedirect 21 | ) 22 | from django.urls.base import reverse 23 | from django.views.generic.base import ( 24 | RedirectView, 25 | TemplateView 26 | ) 27 | 28 | from django_keycloak.models import Nonce 29 | from django_keycloak.auth import remote_user_login 30 | 31 | 32 | logger = logging.getLogger(__name__) 33 | 34 | 35 | class Login(RedirectView): 36 | 37 | def get_redirect_url(self, *args, **kwargs): 38 | 39 | nonce = Nonce.objects.create( 40 | redirect_uri=self.request.build_absolute_uri( 41 | location=reverse('keycloak_login_complete')), 42 | next_path=self.request.GET.get('next')) 43 | 44 | self.request.session['oidc_state'] = str(nonce.state) 45 | 46 | authorization_url = self.request.realm.client.openid_api_client\ 47 | .authorization_url( 48 | redirect_uri=nonce.redirect_uri, 49 | scope='openid given_name family_name email', 50 | state=str(nonce.state) 51 | ) 52 | 53 | if self.request.realm.server.internal_url: 54 | authorization_url = authorization_url.replace( 55 | self.request.realm.server.internal_url, 56 | self.request.realm.server.url, 57 | 1 58 | ) 59 | 60 | logger.debug(authorization_url) 61 | 62 | return authorization_url 63 | 64 | 65 | class LoginComplete(RedirectView): 66 | 67 | def get(self, *args, **kwargs): 68 | request = self.request 69 | 70 | if 'error' in request.GET: 71 | return HttpResponseServerError(request.GET['error']) 72 | 73 | if 'code' not in request.GET and 'state' not in request.GET: 74 | return HttpResponseBadRequest() 75 | 76 | if 'oidc_state' not in request.session \ 77 | or request.GET['state'] != request.session['oidc_state']: 78 | # Missing or incorrect state; login again. 79 | return HttpResponseRedirect(reverse('keycloak_login')) 80 | 81 | nonce = Nonce.objects.get(state=request.GET['state']) 82 | 83 | user = authenticate(request=request, 84 | code=request.GET['code'], 85 | redirect_uri=nonce.redirect_uri) 86 | 87 | RemoteUserModel = get_remote_user_model() 88 | if isinstance(user, RemoteUserModel): 89 | remote_user_login(request, user) 90 | else: 91 | login(request, user) 92 | 93 | nonce.delete() 94 | 95 | return HttpResponseRedirect(nonce.next_path or '/') 96 | 97 | 98 | class Logout(RedirectView): 99 | 100 | def get_redirect_url(self, *args, **kwargs): 101 | if hasattr(self.request.user, 'oidc_profile'): 102 | self.request.realm.client.openid_api_client.logout( 103 | self.request.user.oidc_profile.refresh_token 104 | ) 105 | self.request.user.oidc_profile.access_token = None 106 | self.request.user.oidc_profile.expires_before = None 107 | self.request.user.oidc_profile.refresh_token = None 108 | self.request.user.oidc_profile.refresh_expires_before = None 109 | self.request.user.oidc_profile.save(update_fields=[ 110 | 'access_token', 111 | 'expires_before', 112 | 'refresh_token', 113 | 'refresh_expires_before' 114 | ]) 115 | 116 | logout(self.request) 117 | 118 | if settings.LOGOUT_REDIRECT_URL: 119 | return resolve_url(settings.LOGOUT_REDIRECT_URL) 120 | 121 | return reverse('keycloak_login') 122 | 123 | 124 | class SessionIframe(TemplateView): 125 | template_name = 'django_keycloak/session_iframe.html' 126 | 127 | @property 128 | def op_location(self): 129 | realm = self.request.realm 130 | if realm.server.internal_url: 131 | return realm.well_known_oidc['check_session_iframe'].replace( 132 | realm.server.internal_url, 133 | realm.server.url, 134 | 1 135 | ) 136 | return realm.server.url 137 | 138 | @property 139 | def client_id(self): 140 | if not hasattr(self.request, 'realm'): 141 | return None 142 | 143 | realm = self.request.realm 144 | return realm.client.client_id 145 | 146 | def get_context_data(self, **kwargs): 147 | return super(SessionIframe, self).get_context_data( 148 | client_id=self.client_id, 149 | identity_server=self.request.realm.server.url, 150 | op_location=self.op_location, 151 | cookie_name=getattr(settings, 'KEYCLOAK_SESSION_STATE_COOKIE_NAME', 152 | 'session_state') 153 | ) 154 | --------------------------------------------------------------------------------