From 9b8cf8f34a9b02cd8799d73f3b72965f3ded1ac9 Mon Sep 17 00:00:00 2001 From: Mustafa Gezen Date: Sat, 1 Jul 2023 21:42:36 +0200 Subject: [PATCH] Initial commit --- .gitignore | 4 +++ LICENSE | 28 +++++++++++++++ README.md | 3 ++ pdot_common/oidc/__init__.py | 70 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 23 ++++++++++++ 5 files changed, 128 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pdot_common/oidc/__init__.py create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aeb61ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +.venv +*.egg-info +/build \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e000438 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2023 Ctrl IQ, Inc. All rights reserved. +Copyright (c) 2023 Rocky Enterprise Software Foundation, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2820df8 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# pdot_common +Common Python library for Peridot projects + diff --git a/pdot_common/oidc/__init__.py b/pdot_common/oidc/__init__.py new file mode 100644 index 0000000..0458b2c --- /dev/null +++ b/pdot_common/oidc/__init__.py @@ -0,0 +1,70 @@ +from dataclasses import dataclass + +import httpx + +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse + + +@dataclass +class OIDCConfig: + userinfo_endpoint: str + + +def add_oidc_middleware(app: FastAPI, config: OIDCConfig): + @app.middleware("http") + async def verify_oidc_auth(request: Request, call_next): + # First verify that there is an Authorization header + auth_header = request.headers.get("Authorization") + if not auth_header: + return JSONResponse( + status_code=401, + content={ + "detail": "No Authorization header", + }, + ) + + # Then verify that it is a Bearer token + auth_split = auth_header.split(" ") + if len(auth_split) != 2: + return JSONResponse( + status_code=401, + content={ + "detail": "Invalid Authorization value", + }, + ) + + auth_type = auth_split[0] + auth_token = auth_split[1] + if not auth_type or auth_type.lower() != "bearer": + return JSONResponse( + status_code=401, + content={ + "detail": "Not a bearer token", + }, + ) + + # Then verify that the token is valid + async with httpx.AsyncClient() as client: + res = await client.get( + config.userinfo_endpoint, + headers={ + "Authorization": f"Bearer {auth_token}", + }, + ) + if res.status_code != 200: + return JSONResponse( + status_code=401, + content={"detail": "Invalid token"}, + ) + + userinfo = res.json() + if not userinfo: + return JSONResponse( + status_code=401, + content={"detail": "Invalid token"}, + ) + + request.state.userinfo = userinfo + + return await call_next(request) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7a2156c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "pdot_common" +version = "0.0.1" +description = "Common Python library for Peridot projects" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "fastapi >= 0.99.0", + "authlib >= 1.2.1", + "httpx >= 0.24.1", +] +authors = [ + { name = "Mustafa Gezen", email = "mustafa@rockylinux.org" } +] +maintainers = [ + { name = "Mustafa Gezen", email = "mustafa@rockylinux.org" } +] + +[project.license] +file = "LICENSE" + +[tool.setuptools] +package-dir = { "pdot_common" = "pdot_common" }