Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/github_actions/actions/download-a…
Browse files Browse the repository at this point in the history
…rtifact-4.1.2
  • Loading branch information
francbartoli authored Feb 18, 2024
2 parents 024e584 + 53fd0d7 commit f651f30
Show file tree
Hide file tree
Showing 21 changed files with 1,378 additions and 957 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.venv
14 changes: 12 additions & 2 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,20 @@ DEV_OPA_URL=http://localhost:8383
# api-keys
DEV_API_KEY_ENABLED=false
DEV_PYGEOAPI_KEY_GLOBAL=pygeoapi
# oidc
# oidc-full
DEV_APP_URI=http://localhost:5000
DEV_OIDC_WELL_KNOWN_ENDPOINT=http://localhost:8282/realms/pygeoapi/.well-known/openid-configuration
DEV_OIDC_CLIENT_ID=pygeoapi-client
DEV_OIDC_CLIENT_SECRET=2yholx8r3mqyUJaOoJiZhcqvQDQwmgyD
# oidc-jwks-only
DEV_JWKS_ENABLED=true
DEV_OAUTH2_JWKS_ENDPOINT=https://uat.interop.pagopa.it/.well-known/jwks.json
DEV_OAUTH2_TOKEN_ENDPOINT=https://auth.uat.interop.pagopa.it/token.oauth2
# pygeoapi
DEV_PYGEOAPI_BASEURL=http://localhost:5000
DEV_PYGEOAPI_CONFIG=pygeoapi-config.yml
DEV_PYGEOAPI_OPENAPI=pygeoapi-openapi.yml
DEV_PYGEOAPI_SECURITY_SCHEME=http
# fastgeoapi
DEV_FASTGEOAPI_CONTEXT=/geoapi

Expand All @@ -68,14 +73,19 @@ PROD_OPA_URL=http://localhost:8383
# api-keys
PROD_API_KEY_ENABLED=true
PROD_PYGEOAPI_KEY_GLOBAL=pygeoapi
# oidc
# oidc-full
PROD_APP_URI=http://localhost:5000
PROD_OIDC_WELL_KNOWN_ENDPOINT=http://localhost:8282/realms/pygeoapi/.well-known/openid-configuration
PROD_OIDC_CLIENT_ID=pygeoapi-client
PROD_OIDC_CLIENT_SECRET=2yholx8r3mqyUJaOoJiZhcqvQDQwmgyD
# oidc-jwks-only
PROD_JWKS_ENABLED=false
PROD_OAUTH2_JWKS_ENDPOINT=https://interop.pagopa.it/.well-known/jwks.json
PROD_OAUTH2_TOKEN_ENDPOINT=https://auth.interop.pagopa.it/token.oauth2
# pygeoapi
PROD_PYGEOAPI_BASEURL=http://localhost:5000
PROD_PYGEOAPI_CONFIG=pygeoapi-config.yml
PROD_PYGEOAPI_OPENAPI=pygeoapi-openapi.yml
PROD_PYGEOAPI_SECURITY_SCHEME=http
# fastgeoapi
PROD_FASTGEOAPI_CONTEXT=/geoapi
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
fetch-depth: 2

- name: Set up Python
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v5.0.0
with:
python-version: "3.10"

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
uses: actions/[email protected]

- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v5.0.0
with:
python-version: ${{ matrix.python }}

Expand Down Expand Up @@ -120,7 +120,7 @@ jobs:
uses: actions/[email protected]

- name: Set up Python
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v5.0.0
with:
python-version: "3.10"

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,6 @@ dmypy.json

# OS
.DS_Store
.aider*

pygeoapi-openapi.json
1 change: 1 addition & 0 deletions app/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Auth package."""
26 changes: 26 additions & 0 deletions app/auth/auth_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Auth interface module."""
from abc import ABC
from abc import abstractmethod
from typing import Dict
from typing import Union

from starlette.requests import Request
from starlette.responses import RedirectResponse


class AuthInterface(ABC):
"""Define the interface for the authentication instances.
The interface provides necessary methods for the OPAMiddleware
authentication flow. This allows to easily integrate various auth methods.
"""

@abstractmethod
async def authenticate(self, request: Request) -> Union[RedirectResponse, Dict]:
"""Authenticate the incoming request.
The method returns a dictionary containing the valid and authorized
users information or a redirect since some flows require calling a
identity broker beforehand.
"""
pass
93 changes: 93 additions & 0 deletions app/auth/auth_jwks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Auth JWKS module."""
import typing
from dataclasses import dataclass
from dataclasses import field

import httpx
from app.auth.auth_interface import AuthInterface
from app.auth.exceptions import Oauth2Error
from app.config.logging import create_logger
from authlib.jose import errors
from authlib.jose import JsonWebKey
from authlib.jose import JsonWebToken
from authlib.jose import JWTClaims
from authlib.jose import KeySet
from starlette.requests import Request
from starlette.responses import RedirectResponse

# from cachetools import cached
# from cachetools import TTLCache


logger = create_logger("app.auth.auth_jwks")


@dataclass
class JWKSConfig:
"""JWKS configuration instance."""

jwks_uri: str = field(default="")


class JWKSAuthentication(AuthInterface):
"""JWKS authentication instance."""

def __init__(self, config: JWKSConfig) -> None:
"""Initialize the authentication."""
self.config = config

# @cached(TTLCache(maxsize=1, ttl=3600))
async def get_jwks(self) -> KeySet:
"""Get cached or new JWKS."""
url = self.config.jwks_uri
logger.info(f"Fetching JSON Web Key Set from {url}")
async with httpx.AsyncClient() as client:
response = await client.get(url)
return JsonWebKey.import_key_set(response.json())

async def decode_token(
self,
token: str,
) -> JWTClaims:
"""Validate and decode JWT."""
try:
jwks = await self.get_jwks()
claims = JsonWebToken(["RS256"]).decode(
s=token,
key=jwks,
# claim_options={
# # Example of validating audience to match expected value
# # "aud": {"essential": True, "values": [APP_CLIENT_ID]}
# }
)
if "client_id" in claims:
# Insert Cognito's `client_id` into `aud` claim if `aud` claim is unset
claims.setdefault("aud", claims["client_id"])
claims.validate()
except errors.ExpiredTokenError:
logger.error("Unable to validate an expired token")
raise Oauth2Error("Unable to validate an expired token") # noqa
except errors.JoseError:
logger.error("Unable to decode token")
raise Oauth2Error("Unable to decode token") # noqa

return claims

async def authenticate(
self,
request: Request,
accepted_methods: typing.Optional[typing.List[str]] = ["access_token"], # noqa
) -> typing.Union[RedirectResponse, typing.Dict]:
"""Authenticate the caller with the incoming request."""
bearer = request.headers.get("Authorization")
if not bearer:
logger.exception("Unable to get a token")
raise Oauth2Error("Auth token not found")
access_token = bearer.replace("Bearer ", "")
try:
claims = await self.decode_token(access_token)
if not claims:
pass
return claims
except Exception:
raise Oauth2Error("Authentication error") # noqa
13 changes: 13 additions & 0 deletions app/auth/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Authentication exceptions module."""


class AuthenticationError(Exception):
"""This is being raised for exceptions within the auth flow."""

pass


class Oauth2Error(AuthenticationError):
"""Oauth2 authentication flow exception."""

pass
59 changes: 59 additions & 0 deletions app/auth/oauth2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""OAuth2 provider module."""
import re
from abc import ABC
from abc import abstractmethod
from typing import List
from typing import Optional

from app.auth.auth_interface import AuthInterface
from starlette.requests import Request


class Injectable(ABC):
"""Define interface for injectables."""

def __init__(
self, key: str, skip_endpoints: Optional[List[str]] = [] # noqa B006
) -> None:
"""Set properties initialization for injectables."""
self.key = key
self.skip_endpoints = [
re.compile(skip) for skip in skip_endpoints # type:ignore
]

@abstractmethod
async def extract(self, request: Request) -> List:
"""Extract the token from the request."""
pass


class Oauth2Provider:
"""OAuth2 middleware."""

def __init__(
self,
authentication: [AuthInterface, List[AuthInterface]], # type:ignore
injectables: Optional[List[Injectable]] = None,
accepted_methods: Optional[List[str]] = [ # noqa B006
"id_token",
"access_token",
],
) -> None:
"""Handle configuration container for the OAuth2 middleware. # noqa D405
PARAMETERS
----------
authentication: [AuthInterface, List[AuthInterface]]
Authentication Implementations to be used for the
request authentication.
injectables: List[Injectable], default=None
List of injectables to be used to add information to the
request payload.
accepted_methods: List[str], default=["id_token", "access_token"]
List of accepted authentication methods.
"""
if not isinstance(authentication, list):
authentication = [authentication]
self.authentication = authentication
self.injectables = injectables
self.accepted_methods = accepted_methods
Loading

0 comments on commit f651f30

Please sign in to comment.