├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app └── main.py ├── docker-compose.yaml ├── images └── app_urls.png └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig 2 | 3 | # Created by https://www.gitignore.io/api/linux,visualstudiocode,python 4 | # Edit at https://www.gitignore.io/?templates=linux,visualstudiocode,python 5 | 6 | ### Linux ### 7 | *~ 8 | 9 | # temporary files which can be created if a process still has a handle open of a deleted file 10 | .fuse_hidden* 11 | 12 | # KDE directory preferences 13 | .directory 14 | 15 | # Linux trash folder which might appear on any partition or disk 16 | .Trash-* 17 | 18 | # .nfs files are created when an open file is removed but is still being accessed 19 | .nfs* 20 | 21 | ### Python ### 22 | # Byte-compiled / optimized / DLL files 23 | __pycache__/ 24 | *.py[cod] 25 | *$py.class 26 | 27 | # C extensions 28 | *.so 29 | 30 | # Distribution / packaging 31 | .Python 32 | build/ 33 | develop-eggs/ 34 | dist/ 35 | downloads/ 36 | eggs/ 37 | .eggs/ 38 | lib/ 39 | lib64/ 40 | parts/ 41 | sdist/ 42 | var/ 43 | wheels/ 44 | pip-wheel-metadata/ 45 | share/python-wheels/ 46 | *.egg-info/ 47 | .installed.cfg 48 | *.egg 49 | MANIFEST 50 | 51 | # PyInstaller 52 | # Usually these files are written by a python script from a template 53 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 54 | *.manifest 55 | *.spec 56 | 57 | # Installer logs 58 | pip-log.txt 59 | pip-delete-this-directory.txt 60 | 61 | # Unit test / coverage reports 62 | htmlcov/ 63 | .tox/ 64 | .nox/ 65 | .coverage 66 | .coverage.* 67 | .cache 68 | nosetests.xml 69 | coverage.xml 70 | *.cover 71 | .hypothesis/ 72 | .pytest_cache/ 73 | 74 | # Translations 75 | *.mo 76 | *.pot 77 | 78 | # Django stuff: 79 | *.log 80 | local_settings.py 81 | db.sqlite3 82 | db.sqlite3-journal 83 | 84 | # Flask stuff: 85 | instance/ 86 | .webassets-cache 87 | 88 | # Scrapy stuff: 89 | .scrapy 90 | 91 | # Sphinx documentation 92 | docs/_build/ 93 | 94 | # PyBuilder 95 | target/ 96 | 97 | # Jupyter Notebook 98 | .ipynb_checkpoints 99 | 100 | # IPython 101 | profile_default/ 102 | ipython_config.py 103 | 104 | # pyenv 105 | .python-version 106 | 107 | # pipenv 108 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 109 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 110 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 111 | # install all needed dependencies. 112 | #Pipfile.lock 113 | 114 | # celery beat schedule file 115 | celerybeat-schedule 116 | 117 | # SageMath parsed files 118 | *.sage.py 119 | 120 | # Environments 121 | .env 122 | .venv 123 | env/ 124 | venv/ 125 | ENV/ 126 | env.bak/ 127 | venv.bak/ 128 | 129 | # Spyder project settings 130 | .spyderproject 131 | .spyproject 132 | 133 | # Rope project settings 134 | .ropeproject 135 | 136 | # mkdocs documentation 137 | /site 138 | 139 | # mypy 140 | .mypy_cache/ 141 | .dmypy.json 142 | dmypy.json 143 | 144 | # Pyre type checker 145 | .pyre/ 146 | 147 | ### VisualStudioCode ### 148 | .vscode/* 149 | # !.vscode/settings.json 150 | !.vscode/tasks.json 151 | !.vscode/launch.json 152 | !.vscode/extensions.json 153 | 154 | ### VisualStudioCode Patch ### 155 | # Ignore all local history of files 156 | .history 157 | 158 | # End of https://www.gitignore.io/api/linux,visualstudiocode,python 159 | 160 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) 161 | 162 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-buster 2 | 3 | WORKDIR /appuser/ 4 | 5 | COPY requirements.txt requirements.txt 6 | RUN pip install -r requirements.txt 7 | 8 | COPY app/ app/ 9 | 10 | RUN groupadd -g 999 appuser && \ 11 | useradd -r -u 999 -g appuser appuser 12 | RUN chown -R appuser:appuser /appuser 13 | 14 | USER appuser 15 | 16 | CMD ["python", "app/main.py"]] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Harrison Kiang 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Using Kubernetes? 2 | 3 | If you're using Kuberentes, I would strongly encourage you to read this [blog post](https://hkiang01.github.io/kubernetes/keycloak/) that describes how to use Keycloak in a manner that secures APIs independently of the application. 4 | For code samples see https://github.com/hkiang01/keycloak-demo 5 | 6 | 7 | ## Quickstart 8 | 9 | 1. Log into keycloak admin console (see docker compose) 10 | 2. Create "Clients" realm (upper left) 11 | 3. Within "Clients" realm, create a client called "app" with the following URLs 12 | 13 | ![alt text](images/app_urls.png "app URLs") 14 | 4. Create a user with username and password set to "test" 15 | 5. `docker-compose down && docker-compose build && docker-compose up` 16 | 6. Navigate to `localhost:8000/login` 17 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from typing import Dict 4 | 5 | import jwt 6 | import requests 7 | import uvicorn 8 | from fastapi import FastAPI 9 | from fastapi.security.utils import get_authorization_scheme_param 10 | from starlette.requests import Request 11 | from starlette.responses import RedirectResponse 12 | 13 | APP_BASE_URL = "http://localhost:8000/" 14 | KEYCLOAK_BASE_URL = "http://localhost:8080" 15 | AUTH_URL = ( 16 | f"{KEYCLOAK_BASE_URL}/auth/realms/Clients" 17 | "/protocol/openid-connect/auth?client_id=app&response_type=code" 18 | ) 19 | TOKEN_URL = ( 20 | f"{KEYCLOAK_BASE_URL}/auth/realms/Clients/protocol/openid-connect/token" 21 | ) 22 | 23 | 24 | logger = logging.getLogger(__name__) 25 | logger.setLevel("DEBUG") 26 | 27 | app = FastAPI() 28 | 29 | 30 | @app.get("/login") 31 | async def login() -> RedirectResponse: 32 | return RedirectResponse(AUTH_URL) 33 | 34 | 35 | @app.get("/auth") 36 | async def auth(code: str) -> RedirectResponse: 37 | payload = ( 38 | f"grant_type=authorization_code&code={code}" 39 | f"&redirect_uri={APP_BASE_URL}&client_id=app" 40 | ) 41 | headers = {"Content-Type": "application/x-www-form-urlencoded"} 42 | token_response = requests.request( 43 | "POST", TOKEN_URL, data=payload, headers=headers 44 | ) 45 | 46 | token_body = json.loads(token_response.content) 47 | access_token = token_body["access_token"] 48 | 49 | response = RedirectResponse(url="/") 50 | response.set_cookie("Authorization", value=f"Bearer {access_token}") 51 | return response 52 | 53 | 54 | @app.get("/") 55 | async def root(request: Request,) -> Dict: 56 | authorization: str = request.cookies.get("Authorization") 57 | scheme, credentials = get_authorization_scheme_param(authorization) 58 | 59 | decoded = jwt.decode( 60 | credentials, verify=False 61 | ) # TODO input keycloak public key as key, disable option to verify aud 62 | logger.debug(decoded) 63 | 64 | return {"message": "You're logged in!"} 65 | 66 | 67 | if __name__ == "__main__": 68 | uvicorn.run(app, port=8000, loop="asyncio") 69 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | 2 | version: '3' 3 | services: 4 | postgres: 5 | image: postgres 6 | volumes: 7 | - postgres_data:/var/lib/postgresql/data 8 | environment: 9 | POSTGRES_DB: keycloak 10 | POSTGRES_USER: keycloak 11 | POSTGRES_PASSWORD: password 12 | auth: 13 | image: jboss/keycloak 14 | ports: 15 | - "8080:8080" 16 | depends_on: 17 | - postgres 18 | environment: 19 | DB_VENDOR: POSTGRES 20 | DB_ADDR: postgres 21 | DB_DATABASE: keycloak 22 | DB_USER: keycloak 23 | DB_SCHEMA: public 24 | DB_PASSWORD: password 25 | KEYCLOAK_USER: admin 26 | KEYCLOAK_PASSWORD: Pa55w0rd 27 | # Uncomment the line below if you want to specify JDBC parameters. The parameter below is just an example, and it shouldn't be used in production without knowledge. It is highly recommended that you read the PostgreSQL JDBC driver documentation in order to use it. 28 | #JDBC_PARAMS: "ssl=true" 29 | app: 30 | build: 31 | context: . 32 | dockerfile: Dockerfile 33 | command: ["python", "app/main.py"] 34 | ports: 35 | - "8000:8000" 36 | network_mode: host 37 | 38 | volumes: 39 | postgres_data: 40 | driver: local -------------------------------------------------------------------------------- /images/app_urls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hkiang01/fastapi-keycloak-oidc-auth/da29fcf1e1ab03371e2e140a382e548768d8517b/images/app_urls.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | pyjwt[crypto] 3 | requests 4 | uvicorn --------------------------------------------------------------------------------