Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r test-requirements.txt
python -m pip install --upgrade pip wheel setuptools
pip install -q ruff
pip install -q ruff mypy
- name: Lint
run: |
ruff check .
ruff format --check .
mypy .
- name: Test
run: |
pip install wheel
Expand Down
26 changes: 15 additions & 11 deletions atlassian_jwt_auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
from atlassian_jwt_auth.algorithms import get_permitted_algorithm_names # noqa

from atlassian_jwt_auth.signer import ( # noqa
from atlassian_jwt_auth.algorithms import get_permitted_algorithm_names
from atlassian_jwt_auth.key import (
HTTPSPublicKeyRetriever,
KeyIdentifier,
)
from atlassian_jwt_auth.signer import (
create_signer,
create_signer_from_file_private_key_repository,
)
from atlassian_jwt_auth.verifier import JWTAuthVerifier

from atlassian_jwt_auth.key import ( # noqa
KeyIdentifier,
HTTPSPublicKeyRetriever,
)

from atlassian_jwt_auth.verifier import ( # noqa
JWTAuthVerifier,
)
__all__ = [
"get_permitted_algorithm_names",
"HTTPSPublicKeyRetriever",
"KeyIdentifier",
"create_signer",
"create_signer_from_file_private_key_repository",
"JWTAuthVerifier",
]
5 changes: 4 additions & 1 deletion atlassian_jwt_auth/algorithms.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
def get_permitted_algorithm_names():
from typing import List


def get_permitted_algorithm_names() -> List[str]:
"""returns permitted algorithm names."""
return [
"RS256",
Expand Down
23 changes: 20 additions & 3 deletions atlassian_jwt_auth/auth.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
from __future__ import absolute_import

from typing import Any, Iterable, Union

import atlassian_jwt_auth
from atlassian_jwt_auth import KeyIdentifier
from atlassian_jwt_auth.signer import JWTAuthSigner


class BaseJWTAuth(object):
"""Adds a JWT bearer token to the request per the ASAP specification"""

def __init__(self, signer, audience, *args, **kwargs):
def __init__(
self,
signer: JWTAuthSigner,
audience: Union[str, Iterable[str]],
*args: Any,
**kwargs: Any,
) -> None:
self._audience = audience
self._signer = signer
self._additional_claims = kwargs.get("additional_claims", {})

@classmethod
def create(cls, issuer, key_identifier, private_key_pem, audience, **kwargs):
def create(
cls,
issuer: str,
key_identifier: Union[KeyIdentifier, str],
private_key_pem: Union[str, bytes],
audience: Union[str, Iterable[str]],
**kwargs: Any,
) -> "BaseJWTAuth":
"""Instantiate a JWTAuth while creating the signer inline"""
signer = atlassian_jwt_auth.create_signer(
issuer, key_identifier, private_key_pem, **kwargs
)
return cls(signer, audience)

def _get_header_value(self):
def _get_header_value(self) -> bytes:
return b"Bearer " + self._signer.generate_jwt(
self._audience, additional_claims=self._additional_claims
)
24 changes: 9 additions & 15 deletions atlassian_jwt_auth/contrib/aiohttp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
"""Provide asyncio support"""

import sys

if sys.version_info >= (3, 5):
try:
import aiohttp # noqa
from .auth import JWTAuth # noqa
from .key import HTTPSPublicKeyRetriever # noqa
from .verifier import JWTAuthVerifier # noqa
except ImportError as e:
import warnings

warnings.warn(str(e))


del sys
from .auth import JWTAuth
from .key import HTTPSPublicKeyRetriever
from .verifier import JWTAuthVerifier

__all__ = [
"JWTAuth",
"HTTPSPublicKeyRetriever",
"JWTAuthVerifier",
]
13 changes: 11 additions & 2 deletions atlassian_jwt_auth/contrib/aiohttp/auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from typing import Any, Iterable, Union

from aiohttp import BasicAuth

from atlassian_jwt_auth import KeyIdentifier
from atlassian_jwt_auth.auth import BaseJWTAuth


Expand All @@ -12,10 +15,16 @@ class JWTAuth(BaseJWTAuth, BasicAuth):
def __new__(cls, *args, **kwargs):
return super().__new__(cls, "")

def encode(self):
def encode(self) -> str:
return self._get_header_value().decode(self.encoding)


def create_jwt_auth(issuer, key_identifier, private_key_pem, audience, **kwargs):
def create_jwt_auth(
issuer: str,
key_identifier: Union[KeyIdentifier, str],
private_key_pem: str,
audience: Union[str, Iterable[str]],
**kwargs: Any,
) -> BaseJWTAuth:
"""Instantiate a JWTAuth while creating the signer inline"""
return JWTAuth.create(issuer, key_identifier, private_key_pem, audience, **kwargs)
31 changes: 20 additions & 11 deletions atlassian_jwt_auth/contrib/aiohttp/key.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,38 @@
import asyncio
import urllib.parse
from asyncio import AbstractEventLoop
from typing import Any, Awaitable, Dict, Optional

import aiohttp

from atlassian_jwt_auth.exceptions import PublicKeyRetrieverException
from atlassian_jwt_auth.key import (
PEM_FILE_TYPE,
)
from atlassian_jwt_auth.key import (
HTTPSPublicKeyRetriever as _HTTPSPublicKeyRetriever,
)
from atlassian_jwt_auth.key import PEM_FILE_TYPE
from atlassian_jwt_auth.key import HTTPSPublicKeyRetriever as _HTTPSPublicKeyRetriever


class HTTPSPublicKeyRetriever(_HTTPSPublicKeyRetriever):
"""A class for retrieving JWT public keys with aiohttp"""

_class_session = None

def __init__(self, base_url, *, loop=None):
def __init__(
self, base_url: str, *, loop: Optional[AbstractEventLoop] = None
) -> None:
if loop is None:
loop = asyncio.get_event_loop()
self.loop = loop
super().__init__(base_url)

def _get_session(self):
def _get_session(self) -> aiohttp.ClientSession: # type: ignore[override]
if HTTPSPublicKeyRetriever._class_session is None:
HTTPSPublicKeyRetriever._class_session = aiohttp.ClientSession(
loop=self.loop
)
return HTTPSPublicKeyRetriever._class_session

def _convert_proxies_to_proxy_arg(self, url, requests_kwargs):
def _convert_proxies_to_proxy_arg(
self, url: str, requests_kwargs: Dict[Any, Any]
) -> Dict[str, Any]:
"""returns a modified requests_kwargs dict that contains proxy
information in a form that aiohttp accepts
(it wants proxy information instead of a dict of proxies).
Expand All @@ -43,11 +45,18 @@ def _convert_proxies_to_proxy_arg(self, url, requests_kwargs):
requests_kwargs["proxy"] = proxy
return requests_kwargs

async def _retrieve(self, url, requests_kwargs):
async def _retrieve(
self, url: str, requests_kwargs: Dict[Any, Any]
) -> Awaitable[str]:
requests_kwargs = self._convert_proxies_to_proxy_arg(url, requests_kwargs)
try:
resp = await self._session.get(
url, headers={"accept": PEM_FILE_TYPE}, **requests_kwargs
url,
headers={
"accept": # type: ignore[misc]
PEM_FILE_TYPE
},
**requests_kwargs,
)
resp.raise_for_status()
self._check_content_type(url, resp.headers["content-type"])
Expand Down
11 changes: 9 additions & 2 deletions atlassian_jwt_auth/contrib/aiohttp/verifier.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import asyncio
from typing import Any, Dict, Iterable, Union

import jwt

from atlassian_jwt_auth import key
from atlassian_jwt_auth.verifier import JWTAuthVerifier as _JWTAuthVerifier


class JWTAuthVerifier(_JWTAuthVerifier):
async def verify_jwt(self, a_jwt, audience, leeway=0, **requests_kwargs):
class JWTAuthVerifier(_JWTAuthVerifier): # type: ignore[override]
async def verify_jwt( # type: ignore[override]
self,
a_jwt: str,
audience: Union[str, Iterable[str]],
leeway: int = 0,
**requests_kwargs: Any,
) -> Dict[Any, Any]:
"""Verify if the token is correct

Returns:
Expand Down
14 changes: 12 additions & 2 deletions atlassian_jwt_auth/contrib/django/decorators.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
from collections.abc import Callable
from functools import wraps
from typing import Iterable, Optional

from django.http.response import HttpResponse

from atlassian_jwt_auth.frameworks.django.decorators import with_asap


def validate_asap(issuers=None, subjects=None, required=True):
def validate_asap(
issuers: Optional[Iterable[str]] = None,
subjects: Optional[Iterable[str]] = None,
required: bool = True,
) -> Callable:
"""Decorator to allow endpoint-specific ASAP authorization, assuming ASAP
authentication has already occurred.

Expand Down Expand Up @@ -45,7 +51,11 @@ def validate_asap_wrapper(request, *args, **kwargs):
return validate_asap_decorator


def requires_asap(issuers=None, subject_should_match_issuer=None, func=None):
def requires_asap(
issuers: Optional[Iterable[str]] = None,
subject_should_match_issuer: Optional[bool] = None,
func: Optional[Callable] = None,
) -> Callable:
"""Decorator for Django endpoints to require ASAP

:param list issuers: *required The 'iss' claims that this endpoint is from.
Expand Down
15 changes: 10 additions & 5 deletions atlassian_jwt_auth/contrib/django/middleware.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any, Callable, Optional

from django.conf import settings
from django.utils.deprecation import MiddlewareMixin

Expand All @@ -10,7 +12,7 @@ class ProxiedAsapMiddleware(OldStyleASAPMiddleware, MiddlewareMixin):

This must come before any authentication middleware."""

def __init__(self, get_response=None):
def __init__(self, get_response: Optional[Any] = None) -> None:
super(ProxiedAsapMiddleware, self).__init__()
self.get_response = get_response

Expand All @@ -25,15 +27,15 @@ def __init__(self, get_response=None):
settings, "ASAP_PROXIED_AUTHORIZATION_HEADER", "HTTP_X_ASAP_AUTHORIZATION"
)

def process_request(self, request):
def process_request(self, request) -> Optional[str]:
error_response = super(ProxiedAsapMiddleware, self).process_request(request)

if error_response:
return error_response

forwarded_for = request.META.pop(self.xfwd, None)
if forwarded_for is None:
return
return None

request.asap_forwarded = True
request.META["HTTP_X_FORWARDED_FOR"] = forwarded_for
Expand All @@ -46,10 +48,13 @@ def process_request(self, request):
request.META["HTTP_AUTHORIZATION"] = orig_auth
if asap_auth is not None:
request.META[self.xauth] = asap_auth
return None

def process_view(self, request, view_func, view_args, view_kwargs):
def process_view(
self, request: Any, view_func: Callable, view_args: Any, view_kwargs: Any
) -> None:
if not hasattr(request, "asap_forwarded"):
return
return None

# swap headers back into place
asap_auth = request.META.pop(self.xauth, None)
Expand Down
6 changes: 5 additions & 1 deletion atlassian_jwt_auth/contrib/flask_app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import warnings

from .decorators import requires_asap # noqa
from .decorators import requires_asap

__all__ = [
"requires_asap",
]

warnings.warn(
"The atlassian_jwt_auth.contrib.flask_app package is deprecated in 4.0.0 "
Expand Down
8 changes: 7 additions & 1 deletion atlassian_jwt_auth/contrib/flask_app/decorators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from typing import Callable, Iterable, Optional

from atlassian_jwt_auth.frameworks.flask.decorators import with_asap


def requires_asap(f, issuers=None, subject_should_match_issuer=None):
def requires_asap(
f: Callable,
issuers: Optional[Iterable[str]] = None,
subject_should_match_issuer: Optional[bool] = None,
) -> Callable:
"""
Wrapper for Flask endpoints to make them require asap authentication to
access.
Expand Down
18 changes: 15 additions & 3 deletions atlassian_jwt_auth/contrib/requests.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
from __future__ import absolute_import

from typing import Any, Iterable, Union

import requests
from requests.auth import AuthBase

from atlassian_jwt_auth import KeyIdentifier
from atlassian_jwt_auth.auth import BaseJWTAuth


class JWTAuth(AuthBase, BaseJWTAuth):
"""Adds a JWT bearer token to the request per the ASAP specification"""

def __call__(self, r):
r.headers["Authorization"] = self._get_header_value()
def __call__(
self, r: requests.models.PreparedRequest
) -> requests.models.PreparedRequest:
r.headers["Authorization"] = self._get_header_value() # type: ignore[assignment]
return r


def create_jwt_auth(issuer, key_identifier, private_key_pem, audience, **kwargs):
def create_jwt_auth(
issuer: str,
key_identifier: Union[KeyIdentifier, str],
private_key_pem: str,
audience: Union[str, Iterable[str]],
**kwargs: Any,
) -> BaseJWTAuth:
"""Instantiate a JWTAuth while creating the signer inline"""
return JWTAuth.create(issuer, key_identifier, private_key_pem, audience, **kwargs)
Loading