diff --git a/.github/workflows/docs-publish.yml b/.github/workflows/docs-publish.yml index b462c408..7fab9b86 100644 --- a/.github/workflows/docs-publish.yml +++ b/.github/workflows/docs-publish.yml @@ -4,68 +4,34 @@ on: push: branches: - master - - documentation # @todo Remove this before merging PR. jobs: docs: runs-on: ubuntu-latest steps: - - name: Checks out repo - uses: actions/checkout@v2 - - - name: Generates HTML documentation - uses: synchronizing/sphinx-action@master - with: - docs-folder: "docs/" - pre-build-command: "apt-get update -y && apt-get install -y build-essential && make docs-install" - - - name: Builds the PDF documentation - uses: synchronizing/sphinx-action@master - with: - pre-build-command: "apt-get update -y && apt-get install -y build-essential latexmk texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended && make docs-install" - build-command: "make latexpdf" - docs-folder: "docs/" - - - name: Saves the HTML build documentation - uses: actions/upload-artifact@v4 - with: - path: docs/build/html/ - - - name: Saves the PDF build documentation - uses: actions/upload-artifact@v4 - with: - path: docs/build/latex/aioauth.pdf - - - name: Commits docs changes to gh-pages branch - run: | - - # Copies documentation outside of git folder. - mkdir -p ../docs/html ../docs/pdf - cp -r docs/build/html ../docs/ - cp docs/build/latex/aioauth.pdf ../docs/pdf/ - - # Removes all of the content of the current folder. - sudo rm -rf * - - # Checks out to gh-pages branch. - git checkout -b gh-pages - - # Copies files to branch. - cp -r ../docs/html/* . - cp ../docs/pdf/aioauth.pdf . - - # Sets up no Jekyll config. - touch .nojekyll - - # Commits the changes. - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git add . - git commit -m "Documentation update." -a || true - - - name: Push changes to gh-pages branch - uses: ad-m/github-push-action@master - with: - branch: gh-pages - github_token: ${{ secrets.GITHUB_TOKEN }} - force: True + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + pip install -e ."[docs]" + + - name: Build documentation + run: mkdocs build + + - name: Upload site artifact + uses: actions/upload-artifact@v4 + with: + path: site + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./site + force_orphan: true diff --git a/.gitignore b/.gitignore index 749f0d19..320ac0cd 100644 --- a/.gitignore +++ b/.gitignore @@ -68,8 +68,8 @@ instance/ # Scrapy stuff: .scrapy -# Sphinx documentation -docs/_build/ +# MkDocs build +/site # PyBuilder target/ @@ -105,8 +105,6 @@ venv.bak/ # Rope project settings .ropeproject -# mkdocs documentation -/site # mypy .mypy_cache/ diff --git a/Makefile b/Makefile index 2a2f8fb3..0ca84a50 100644 --- a/Makefile +++ b/Makefile @@ -74,10 +74,10 @@ docs-install: ## install packages for local documentation. pip install -e ."[docs]" docs: ## builds the documentation. - $(MAKE) -C docs html + mkdocs build docs-serve: ## serves the documentation on 127.0.0.1:8000. - $(MAKE) -C docs serve + mkdocs serve new-env: ## creates environment for testing and documentation. python -m venv env diff --git a/aioauth/collections.py b/aioauth/collections.py index 299e0fd8..993d5a09 100644 --- a/aioauth/collections.py +++ b/aioauth/collections.py @@ -1,11 +1,8 @@ """ -.. code-block:: python - - from aioauth import collections - Collections that are used throughout the project. - ----- +```python +from aioauth import collections +``` """ from collections import UserDict @@ -14,43 +11,53 @@ class HTTPHeaderDict(UserDict): """ - :param headers: - An iterable of field-value pairs. Must not contain multiple field names - when compared case-insensitively. - - :param kwargs: - Additional field-value pairs to pass in to ``dict.update``. + A dict-like container for storing HTTP headers with case-insensitive keys. - A ``dict`` like container for storing HTTP Headers. + Args: + headers (Optional[Mapping[str, str]]): + An iterable of field-value pairs. Must not contain duplicate field + names (case-insensitively). + **kwargs: + Additional key-value pairs passed to `dict.update`. Example: - - .. code-block:: python - + ```python from aioauth.collections import HTTPHeaderDict d = HTTPHeaderDict({"hello": "world"}) - d['hello'] == 'world' # >>> True - d['Hello'] == 'world' # >>> True - d['hElLo'] == 'world' # >>> True + print(d['hello']) # >>> 'world' + print(d['Hello']) # >>> 'world' + print(d['hElLo']) # >>> 'world' + ``` """ - def __init__(self, dict=None, **kwargs): - """Object initialization.""" - super().__init__(dict, **kwargs) + def __init__(self, headers=None, **kwargs): + """Initialize the case-insensitive dictionary.""" + super().__init__(headers, **kwargs) self.data = {k.lower(): v for k, v in self.data.items()} def __setitem__(self, key: str, value: str): + """Set a key-value pair with case-insensitive key.""" super().__setitem__(key.lower(), value) def __getitem__(self, key: str): + """Retrieve a value by case-insensitive key.""" return super().__getitem__(key.lower()) def __delitem__(self, key: str): - """Item deletion.""" + """Delete a key-value pair using a case-insensitive key.""" return super().__delitem__(key.lower()) def get(self, key: str, default: Any = None): - """Case-insentive get.""" + """ + Return the value for key if key is in the dictionary, else default. + + Args: + key (str): The key to search for (case-insensitive). + default (Any, optional): The value to return if key is not found. + + Returns: + Any: The value associated with the key, or default if not found. + """ try: return self[key] except KeyError: diff --git a/aioauth/config.py b/aioauth/config.py index 6ab853d8..9226bd11 100644 --- a/aioauth/config.py +++ b/aioauth/config.py @@ -1,11 +1,9 @@ """ -.. code-block:: python - - from aioauth import config - Configuration settings for aioauth server instance. ----- +```python +from aioauth import config +``` """ from dataclasses import dataclass @@ -44,8 +42,8 @@ class Settings: Reference links: - - https://datatracker.ietf.org/doc/html/rfc6749#section-4.2 - - https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2 + - [https://datatracker.ietf.org/doc/html/rfc6749#section-4.2](https://datatracker.ietf.org/doc/html/rfc6749#section-4.2) + - [https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2](https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2) """ AUTHORIZATION_CODE_EXPIRES_IN: int = 5 * 60 diff --git a/aioauth/constances.py b/aioauth/constances.py index 0d403175..1e297ef7 100644 --- a/aioauth/constances.py +++ b/aioauth/constances.py @@ -1,11 +1,8 @@ """ -.. code-block:: python - - from aioauth import constances - Constants that are used throughout the project. - ----- +```python +from aioauth import constances +``` """ from .collections import HTTPHeaderDict diff --git a/aioauth/errors.py b/aioauth/errors.py index 15ec9820..e4242aa2 100644 --- a/aioauth/errors.py +++ b/aioauth/errors.py @@ -1,11 +1,8 @@ """ -.. code-block:: python - - from aioauth import errors - Errors used throughout the project. - ----- +```python +from aioauth import errors +``` """ from http import HTTPStatus @@ -77,11 +74,11 @@ class InvalidClientError(OAuth2Error): """ Client authentication failed (e.g. unknown client, no client authentication included, or unsupported authentication method). - The authorization server **may** return an ``HTTP 401`` (Unauthorized) status + The authorization server **may** return an `HTTP 401` (Unauthorized) status code to indicate which HTTP authentication schemes are supported. If the client attempted to authenticate via the ``Authorization`` request header field, the authorization server **must** respond with an - ``HTTP 401`` (Unauthorized) status code, and include the ``WWW-Authenticate`` + `HTTP 401` (Unauthorized) status code, and include the `WWW-Authenticate` response header field matching the authentication scheme used by the client. """ @@ -140,7 +137,7 @@ class InvalidGrantError(OAuth2Error): not match the redirection URI used in the authorization request, or was issued to another client. - See `RFC6749 section 5.2 `_. + See [RFC6749 section 5.2](https://tools.ietf.org/html/rfc6749#section-5.2). """ error: ErrorType = "invalid_grant" @@ -167,7 +164,7 @@ class InvalidScopeError(OAuth2Error): The requested scope is invalid, unknown, or malformed, or exceeds the scope granted by the resource owner. - See `RFC6749 section 5.2 `_. + See [RFC6749 section 5.2](https://tools.ietf.org/html/rfc6749#section-5.2). """ error: ErrorType = "invalid_scope" diff --git a/aioauth/grant_type.py b/aioauth/grant_type.py index 49fe43eb..dc75254c 100644 --- a/aioauth/grant_type.py +++ b/aioauth/grant_type.py @@ -1,11 +1,8 @@ """ -.. code-block:: python - - from aioauth import grant_type - Different OAuth 2.0 grant types. - ----- +```python +from aioauth import grant_type +``` """ from typing import Optional @@ -92,13 +89,13 @@ class AuthorizationCodeGrantType(GrantTypeBase): the user returns to the client via the redirect URL, the application will get the authorization code from the URL and use it to request an access token. - It is recommended that all clients use `RFC 7636 `_ + It is recommended that all clients use [RFC 7636](https://tools.ietf.org/html/rfc7636) Proof Key for Code Exchange extension with this flow as well to provide better security. Note: - Note that ``aioauth`` implements RFC 7636 out-of-the-box. - See `RFC 6749 section 1.3.1 `_. + Note that `aioauth` implements RFC 7636 out-of-the-box. + See [RFC 6749 section 1.3.1](https://tools.ietf.org/html/rfc6749#section-1.3.1). """ async def validate_request(self, request: Request) -> Client: @@ -170,8 +167,8 @@ class PasswordGrantType(GrantTypeBase): for an access token. Because the client application has to collect the user's password and send it to the authorization server, it is not recommended that this grant be used at all anymore. - See `RFC 6749 section 1.3.3 `_. - The latest `OAuth 2.0 Security Best Current Practice `_ + See [RFC 6749 section 1.3.3](https://tools.ietf.org/html/rfc6749#section-1.3.3). + The latest [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-3.4) disallows the password grant entirely. """ @@ -199,7 +196,7 @@ class RefreshTokenGrantType(GrantTypeBase): refresh token for an access token when the access token has expired. This allows clients to continue to have a valid access token without further interaction with the user. - See `RFC 6749 section 1.5 `_. + See [RFC 6749 section 1.5](https://tools.ietf.org/html/rfc6749#section-1.5). """ async def create_token_response( @@ -272,7 +269,7 @@ class ClientCredentialsGrantType(GrantTypeBase): access token outside of the context of a user. This is typically used by clients to access resources about themselves rather than to access a user's resources. - See `RFC 6749 section 4.4 `_. + See [RFC 6749 section 4.4](https://tools.ietf.org/html/rfc6749#section-4.4). """ async def validate_request(self, request: Request) -> Client: diff --git a/aioauth/models.py b/aioauth/models.py index 20ac30eb..2f29eb6e 100644 --- a/aioauth/models.py +++ b/aioauth/models.py @@ -1,11 +1,8 @@ """ -.. code-block:: python - - from aioauth import models - Memory objects used throughout the project. - ----- +```python +from aioauth import models +``` """ from dataclasses import dataclass @@ -65,15 +62,15 @@ class Client: def check_redirect_uri(self, redirect_uri) -> bool: """ - Verifies passed ``redirect_uri`` is part of the Clients's - ``redirect_uris`` list. + Verifies passed `redirect_uri` is part of the Clients's + `redirect_uris` list. """ return redirect_uri in self.redirect_uris def check_grant_type(self, grant_type: Optional[GrantType]) -> bool: """ - Verifies passed ``grant_type`` is part of the client's - ``grant_types`` list. + Verifies passed `grant_type` is part of the client's + `grant_types` list. """ return grant_type in self.grant_types if grant_type else False @@ -81,17 +78,17 @@ def check_response_type( self, response_type: Optional[Union[ResponseType, str]] ) -> bool: """ - Verifies passed ``response_type`` is part of the client's - ``response_types`` list. + Verifies passed `response_type` is part of the client's + `response_types` list. """ return not (set(enforce_list(response_type)) - set(self.response_types)) def get_allowed_scope(self, scope: str) -> str: """ - Returns the allowed ``scope`` given the passed ``scope``. + Returns the allowed `scope` given the passed `scope`. Note: - Note that the passed ``scope`` may contain multiple scopes + Note that the passed `scope` may contain multiple scopes seperated by a space character. """ if not scope: @@ -154,7 +151,7 @@ class AuthorizationCode: code_challenge: Optional[str] = None """ - Only used when `RFC 7636 `_, + Only used when [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636), Proof Key for Code Exchange, is used. PKCE works by having the app generate a random value at the beginning of the flow called a Code Verifier. The app hashes the @@ -166,14 +163,14 @@ class AuthorizationCode: code_challenge_method: Optional[CodeChallengeMethod] = None """ - Only used when `RFC 7636 `_, + Only used when [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636>), Proof Key for Code Exchange, is used. Method used to transform the code verifier into the code challenge. """ nonce: Optional[str] = None """ - Only used when `RFC 7636 `_, + Only used when [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636), Proof Key for Code Exchange, is used. Random piece of data. """ diff --git a/aioauth/oidc/core/grant_type.py b/aioauth/oidc/core/grant_type.py index 1a5b0817..1fddeaa0 100644 --- a/aioauth/oidc/core/grant_type.py +++ b/aioauth/oidc/core/grant_type.py @@ -1,11 +1,9 @@ """ -.. code-block:: python - - from aioauth.oidc.core import grant_type - Different OAuth 2.0 grant types with OpenID Connect extensions. ----- +```python +from aioauth.oidc.core import grant_type +``` """ from typing import TYPE_CHECKING @@ -26,13 +24,13 @@ class AuthorizationCodeGrantType(OAuth2AuthorizationCodeGrantType): the user returns to the client via the redirect URL, the application will get the authorization code from the URL and use it to request an access token. - It is recommended that all clients use `RFC 7636 `_ + It is recommended that all clients use [RFC 7636](https://tools.ietf.org/html/rfc7636). Proof Key for Code Exchange extension with this flow as well to provide better security. Note: - Note that ``aioauth`` implements RFC 7636 out-of-the-box. - See `RFC 6749 section 1.3.1 `_. + Note that `aioauth` implements RFC 7636 out-of-the-box. + See [RFC 6749 section 1.3.1](https://tools.ietf.org/html/rfc6749#section-1.3.1). """ async def create_token_response( @@ -43,7 +41,8 @@ async def create_token_response( Extends the OAuth2 authorization_code grant type such that an id_token is always included with the access_token. - https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse + + See: [https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse](https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse) """ if self.scope is None: raise RuntimeError("validate_request() must be called first") diff --git a/aioauth/oidc/core/requests.py b/aioauth/oidc/core/requests.py index 7d67e785..0538a40f 100644 --- a/aioauth/oidc/core/requests.py +++ b/aioauth/oidc/core/requests.py @@ -1,3 +1,11 @@ +""" +Different OAuth 2.0 request with OpenID Connect extensions. + +```python +from aioauth.oidc.core import requests +``` +""" + from dataclasses import dataclass, field from typing import Optional @@ -10,6 +18,8 @@ @dataclass class Query(BaseQuery): + """Query extended with OpenID `prompt`""" + # Space delimited, case sensitive list of ASCII string values that # specifies whether the Authorization Server prompts the End-User for # reauthentication and consent. The defined values are: none, login, diff --git a/aioauth/oidc/core/responses.py b/aioauth/oidc/core/responses.py index 53c3d471..8832a52f 100644 --- a/aioauth/oidc/core/responses.py +++ b/aioauth/oidc/core/responses.py @@ -1,3 +1,11 @@ +""" +Different OAuth 2.0 responses with OpenID Connect extensions. + +```python +from aioauth.oidc.core import responses +``` +""" + from dataclasses import dataclass from typing import Optional @@ -6,4 +14,6 @@ @dataclass class TokenResponse(OAuthTokenResponse): + """Token response extended with OpenID `id_token`""" + id_token: Optional[str] = None diff --git a/aioauth/requests.py b/aioauth/requests.py index d7ede89d..24f6396a 100644 --- a/aioauth/requests.py +++ b/aioauth/requests.py @@ -1,11 +1,8 @@ """ -.. code-block:: python - - from aioauth import requests - Request objects used throughout the project. - ----- +```python +from aioauth import requests +``` """ from dataclasses import dataclass, field @@ -26,7 +23,7 @@ class Query: """ Object that contains a client's query string portion of a request. - Read more on query strings `here `__. + Read more on query strings [here](https://en.wikipedia.org/wiki/Query_string). """ client_id: Optional[str] = None @@ -44,7 +41,7 @@ class Query: class Post: """ Object that contains a client's post request portion of a request. - Read more on post requests `here `__. + Read more on post requests [here](https://en.wikipedia.org/wiki/POST_(HTTP)). """ grant_type: Optional[GrantType] = None diff --git a/aioauth/response_type.py b/aioauth/response_type.py index e67ea234..87426492 100644 --- a/aioauth/response_type.py +++ b/aioauth/response_type.py @@ -1,11 +1,8 @@ """ -.. code-block:: python - - from aioauth import responses - Response objects used throughout the project. - ----- +```python +from aioauth import responses +``` """ from typing import Tuple, get_args diff --git a/aioauth/responses.py b/aioauth/responses.py index 95ee6dd6..cba9ce64 100644 --- a/aioauth/responses.py +++ b/aioauth/responses.py @@ -1,11 +1,8 @@ """ -.. code-block:: python - - from aioauth import responses - Response objects used throughout the project. - ----- +```python +from aioauth import responses +``` """ from dataclasses import dataclass, field @@ -28,9 +25,9 @@ class ErrorResponse: @dataclass class AuthorizationCodeResponse: - """Response for ``authorization_code``. + """Response for `authorization_code`. - Used by :py:class:`aioauth.response_type.ResponseTypeAuthorizationCode`. + Used by `aioauth.response_type.ResponseTypeAuthorizationCode`. """ code: str @@ -39,9 +36,9 @@ class AuthorizationCodeResponse: @dataclass class NoneResponse: - """Response for :py:class:`aioauth.response_type.ResponseTypeNone`. + """Response for `aioauth.response_type.ResponseTypeNone`. - See: `OAuth v2 multiple response types `_, + See: [OAuth v2 multiple response types](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#none), """ @@ -49,7 +46,7 @@ class NoneResponse: class TokenResponse: """Response for valid token. - Used by :py:class:`aioauth.response_type.ResponseTypeToken`. + Used by `aioauth.response_type.ResponseTypeToken`. """ expires_in: int @@ -64,7 +61,7 @@ class TokenResponse: class IdTokenResponse: """Response for OpenID id_token. - Used by :py:class:`aioauth.response_type.ResponseResponseTypeIdTokenTypeToken`. + Used by `aioauth.response_type.ResponseResponseTypeIdTokenTypeToken`. """ id_token: str @@ -74,7 +71,7 @@ class IdTokenResponse: class TokenActiveIntrospectionResponse: """Response for a valid access token. - Used by :py:meth:`aioauth.server.AuthorizationServer.create_token_introspection_response`. + Used by `aioauth.server.AuthorizationServer.create_token_introspection_response`. """ scope: str @@ -88,7 +85,7 @@ class TokenActiveIntrospectionResponse: class TokenInactiveIntrospectionResponse: """For an invalid, revoked or expired token. - Used by :py:meth:`aioauth.server.AuthorizationServer.create_token_introspection_response`. + Used by `aioauth.server.AuthorizationServer.create_token_introspection_response`. """ active: bool = False @@ -98,7 +95,7 @@ class TokenInactiveIntrospectionResponse: class Response: """General response class. - Used by :py:class:`aioauth.server.AuthorizationServer`. + Used by `aioauth.server.AuthorizationServer`. """ content: Dict = field(default_factory=dict) diff --git a/aioauth/server.py b/aioauth/server.py index c92de672..4f6a2447 100644 --- a/aioauth/server.py +++ b/aioauth/server.py @@ -1,20 +1,18 @@ """ -.. code-block:: python - - from aioauth import server - Memory object and interface used to initialize an OAuth2.0 server instance. Warning: - Note that :py:class:`aioauth.server.AuthorizationServer` is not + Note that `aioauth.server.AuthorizationServer` is not depedent on any server framework, nor serves at any specific endpoint. Instead, it is used to create an interface that can be - used in conjunction with a server framework like ``FastAPI`` or - ``aiohttp`` to create a fully functional OAuth 2.0 server. + used in conjunction with a server framework like `FastAPI` or + `aiohttp` to create a fully functional OAuth 2.0 server. Check out the *Examples* portion of the documentation to understand how it can be leveraged in your own project. ----- +```python +from aioauth import server +``` """ from dataclasses import asdict, dataclass @@ -151,8 +149,10 @@ def is_secure_transport(self, request: Request) -> bool: This method simply checks if the request URL contains ``https://`` at the start of it. It does **not** ensure if the SSL certificate is valid. + Args: - request: :py:class:`aioauth.requests.Request` object. + request: `aioauth.requests.Request` object. + Returns: Flag representing whether or not the transport is secure. """ @@ -177,33 +177,34 @@ def validate_request(self, request: Request, allowed_methods: List[RequestMethod async def create_token_introspection_response(self, request: Request) -> Response: """ Returns a response object with introspection of the passed token. - For more information see `RFC7662 section 2.1 `_. + For more information see [RFC7662 section 2.1](https://tools.ietf.org/html/rfc7662#section-2.1). Note: The API endpoint that leverages this function is usually - ``/introspect``. + `/introspect`. Example: Below is an example utilizing FastAPI as the server framework. - .. code-block:: python - from aioauth_fastapi.utils import to_oauth2_request, to_fastapi_response + ```python + from aioauth_fastapi.utils import to_oauth2_request, to_fastapi_response - @app.get("/token/introspect") - async def introspect(request: fastapi.Request) -> fastapi.Response: - # Converts a fastapi.Request to an aioauth.Request. - oauth2_request: aioauth.Request = await to_oauth2_request(request) - # Creates the response via this function call. - oauth2_response: aioauth.Response = await server.create_token_introspection_response(oauth2_request) - # Converts an aioauth.Response to a fastapi.Response. - response: fastapi.Response = await to_fastapi_response(oauth2_response) - return response + @app.get("/token/introspect") + async def introspect(request: fastapi.Request) -> fastapi.Response: + # Converts a fastapi.Request to an aioauth.Request. + oauth2_request: aioauth.Request = await to_oauth2_request(request) + # Creates the response via this function call. + oauth2_response: aioauth.Response = await server.create_token_introspection_response(oauth2_request) + # Converts an aioauth.Response to a fastapi.Response. + response: fastapi.Response = await to_fastapi_response(oauth2_response) + return response + ``` Args: - request: An :py:class:`aioauth.requests.Request` object. + request: An `aioauth.requests.Request` object. Returns: - response: An :py:class:`aioauth.responses.Response` object. + response: An `aioauth.responses.Response` object. """ self.validate_request(request, ["POST"]) client_id, client_secret = self.get_client_credentials( @@ -291,33 +292,34 @@ async def create_token_response(self, request: Request) -> Response: """Endpoint to obtain an access and/or ID token by presenting an authorization grant or refresh token. Validates a token request and creates a token response. - For more information see - `RFC6749 section 4.1.3 `_. + For more information see: [RFC6749 section 4.1.3](https://tools.ietf.org/html/rfc6749#section-4.1.3). Note: The API endpoint that leverages this function is usually - ``/token``. + `/token`. + Example: Below is an example utilizing FastAPI as the server framework. - .. code-block:: python - from aioauth_fastapi.utils import to_oauth2_request, to_fastapi_response + ```python + from aioauth_fastapi.utils import to_oauth2_request, to_fastapi_response - @app.post("/token") - async def token(request: fastapi.Request) -> fastapi.Response: - # Converts a fastapi.Request to an aioauth.Request. - oauth2_request: aioauth.Request = await to_oauth2_request(request) - # Creates the response via this function call. - oauth2_response: aioauth.Response = await server.create_token_response(oauth2_request) - # Converts an aioauth.Response to a fastapi.Response. - response: fastapi.Response = await to_fastapi_response(oauth2_response) - return response + @app.post("/token") + async def token(request: fastapi.Request) -> fastapi.Response: + # Converts a fastapi.Request to an aioauth.Request. + oauth2_request: aioauth.Request = await to_oauth2_request(request) + # Creates the response via this function call. + oauth2_response: aioauth.Response = await server.create_token_response(oauth2_request) + # Converts an aioauth.Response to a fastapi.Response. + response: fastapi.Response = await to_fastapi_response(oauth2_response) + return response + ``` Args: - request: An :py:class:`aioauth.requests.Request` object. + request: An `aioauth.requests.Request` object. Returns: - response: An :py:class:`aioauth.responses.Response` object. + response: An `aioauth.responses.Response` object. """ self.validate_request(request, ["POST"]) @@ -380,36 +382,36 @@ async def validate_authorization_request( authoriation grant. Validate authorization request and return valid authorization state for later response generation. - For more information see - `RFC6749 section 4.1.1 `_. + For more information see: [RFC6749 section 4.1.1](https://tools.ietf.org/html/rfc6749#section-4.1.1). Note: The API endpoint that leverages this function is usually - ``/authorize``. + `/authorize`. Example: Below is an example utilizing FastAPI as the server framework. - .. code-block:: python - - from aioauth.fastapi.utils import to_oauth2_request, to_fastapi_response - - @app.post("/authorize") - async def authorize(request: fastapi.Request) -> fastapi.Response: - # Converts a fastapi.Request to an aioauth.Request. - oauth2_request: aioauth.Request = await to_oauth2_request(request) - # Validate the oauth request - auth_state: aioauth.AuthState = await server.validate_authorization_request(oauth2_request) - # Creates the response via this function call. - oauth2_response: aioauth.Response = await server.create_authorization_response(auth_state) - # Converts an aioauth.Response to a fastapi.Response. - response: fastapi.Response = await to_fastapi_response(oauth2_response) - return response + + ```python + from aioauth.fastapi.utils import to_oauth2_request, to_fastapi_response + + @app.post("/authorize") + async def authorize(request: fastapi.Request) -> fastapi.Response: + # Converts a fastapi.Request to an aioauth.Request. + oauth2_request: aioauth.Request = await to_oauth2_request(request) + # Validate the oauth request + auth_state: aioauth.AuthState = await server.validate_authorization_request(oauth2_request) + # Creates the response via this function call. + oauth2_response: aioauth.Response = await server.create_authorization_response(auth_state) + # Converts an aioauth.Response to a fastapi.Response. + response: fastapi.Response = await to_fastapi_response(oauth2_response) + return response + ``` Args: - request: An :py:class:`aioauth.requests.Request` object. + request: An `aioauth.requests.Request` object. Returns: - state: An :py:class:`aioauth.server.AuthState` object. + state: An `aioauth.server.AuthState` object. """ self.validate_request(request, ["GET", "POST"]) @@ -551,32 +553,32 @@ async def create_authorization_response(self, request: Request) -> Response: Endpoint to interact with the resource owner and obtain an authorization grant. Create an authorization response after validation. - For more information see - `RFC6749 section 4.1.1 `_. + For more information see: [RFC6749 section 4.1.1](https://tools.ietf.org/html/rfc6749#section-4.1.1). Example: Below is an example utilizing FastAPI as the server framework. - .. code-block:: python - - from aioauth.fastapi.utils import to_oauth2_request, to_fastapi_response - - @app.post("/authorize") - async def authorize(request: fastapi.Request) -> fastapi.Response: - # Converts a fastapi.Request to an aioauth.Request. - oauth2_request: aioauth.Request = await to_oauth2_request(request) - # Validate the oauth request - auth_state: aioauth.AuthState = await server.validate_authorization_request(oauth2_request) - # Creates the response via this function call. - oauth2_response: aioauth.Response = await server.create_authorization_response(auth_state) - # Converts an aioauth.Response to a fastapi.Response. - response: fastapi.Response = await to_fastapi_response(oauth2_response) - return response + + ```python + from aioauth.fastapi.utils import to_oauth2_request, to_fastapi_response + + @app.post("/authorize") + async def authorize(request: fastapi.Request) -> fastapi.Response: + # Converts a fastapi.Request to an aioauth.Request. + oauth2_request: aioauth.Request = await to_oauth2_request(request) + # Validate the oauth request + auth_state: aioauth.AuthState = await server.validate_authorization_request(oauth2_request) + # Creates the response via this function call. + oauth2_response: aioauth.Response = await server.create_authorization_response(auth_state) + # Converts an aioauth.Response to a fastapi.Response. + response: fastapi.Response = await to_fastapi_response(oauth2_response) + return response + ``` Args: - auth_state: An :py:class:`aioauth.server.AuthState` object. + auth_state: An `aioauth.server.AuthState` object. Returns: - response: An :py:class:`aioauth.responses.Response` object. + response: An `aioauth.responses.Response` object. """ auth_state = await self.validate_authorization_request(request) return await self.finalize_authorization_response(auth_state) @@ -584,33 +586,33 @@ async def authorize(request: fastapi.Request) -> fastapi.Response: @catch_errors_and_unavailability() async def revoke_token(self, request: Request) -> Response: """Endpoint to revoke an access token or refresh token. - For more information see - `RFC7009 `_. + For more information see: [RFC7009](https://tools.ietf.org/html/rfc7009). Note: The API endpoint that leverages this function is usually - ``/revoke``. + `/revoke`. Example: Below is an example utilizing FastAPI as the server framework. - .. code-block:: python - from aioauth_fastapi.utils import to_oauth2_request, to_fastapi_response + ```python + from aioauth_fastapi.utils import to_oauth2_request, to_fastapi_response - @app.post("/revoke") - async def revoke(request: fastapi.Request) -> fastapi.Response: - # Converts a fastapi.Request to an aioauth.Request. - oauth2_request: aioauth.Request = await to_oauth2_request(request) - # Creates the response via this function call. - oauth2_response: aioauth.Response = await server.revoke_token(oauth2_request) - # Converts an aioauth.Response to a fastapi.Response. - response: fastapi.Response = await to_fastapi_response(oauth2_response) - return response + @app.post("/revoke") + async def revoke(request: fastapi.Request) -> fastapi.Response: + # Converts a fastapi.Request to an aioauth.Request. + oauth2_request: aioauth.Request = await to_oauth2_request(request) + # Creates the response via this function call. + oauth2_response: aioauth.Response = await server.revoke_token(oauth2_request) + # Converts an aioauth.Response to a fastapi.Response. + response: fastapi.Response = await to_fastapi_response(oauth2_response) + return response + ``` Args: - request: An :py:class:`aioauth.requests.Request` object. + request: An `aioauth.requests.Request` object. Returns: - response: An :py:class:`aioauth.responses.Response` object. + response: An `aioauth.responses.Response` object. """ self.validate_request(request, ["POST"]) client_id, client_secret = self.get_client_credentials( diff --git a/aioauth/storage.py b/aioauth/storage.py index 61bd10e0..5960c8a1 100644 --- a/aioauth/storage.py +++ b/aioauth/storage.py @@ -1,7 +1,8 @@ """ -.. code-block:: python +```python +from aioauth import storage +``` - from aioauth import storage Storage helper class for storing and retrieving client and resource owner information. See the examples on the sidebar to view this in @@ -31,23 +32,24 @@ async def create_token( """Generates a user token and stores it in the database. Used by: - - `ResponseTypeToken` - - `AuthorizationCodeGrantType` - - `PasswordGrantType` - - `ClientCredentialsGrantType` - - `RefreshTokenGrantType` + + * `ResponseTypeToken` + * `AuthorizationCodeGrantType` + * `PasswordGrantType` + * `ClientCredentialsGrantType` + * `RefreshTokenGrantType` Warning: Generated token *must* be stored in the database. Note: Method is used by all core grant types, but only used for - :py:class:`aioauth.response_type.ResponseTypeToken`. + `aioauth.response_type.ResponseTypeToken`. Args: - request: An :py:class:`aioauth.requests.Request`. + request: An `aioauth.requests.Request`. client_id: A user client ID. scope: The scopes for the token. Returns: - The new generated :py:class:`aioauth.models.Token`. + The new generated `aioauth.models.Token`. """ raise NotImplementedError("Method create_token must be implemented") @@ -64,15 +66,15 @@ async def get_token( Note: Method is used by - :py:class:`aioauth.server.AuthorizationServer`, and by the - grant type :py:class:`aioauth.grant_types.RefreshTokenGrantType`. + `aioauth.server.AuthorizationServer`, and by the + grant type `aioauth.grant_types.RefreshTokenGrantType`. Args: - request: An :py:class:`aioauth.requests.Request`. + request: An `aioauth.requests.Request`. client_id: A user client ID. access_token: The user access token. refresh_token: The user refresh token. Returns: - An optional :py:class:`aioauth.models.Token` object. + An optional `aioauth.models.Token` object. """ raise NotImplementedError("Method get_token must be implemented") @@ -107,19 +109,22 @@ async def create_authorization_code( Warning: Generated authorization token *must* be stored in the database. + Note: This must is used by the response type - :py:class:`aioauth.respose_type.ResponseTypeAuthorizationCode`. + `aioauth.respose_type.ResponseTypeAuthorizationCode`. + Args: - request: An :py:class:`aioauth.requests.Request`. + request: An `aioauth.requests.Request`. client_id: A user client ID. scope: The scopes for the token. - response_type: An :py:class:`aioauth.types.ResponseType`. + response_type: An `aioauth.types.ResponseType`. redirect_uri: The redirect URI. - code_challenge_method: An :py:class:`aioauth.types.CodeChallengeMethod`. + code_challenge_method: An `aioauth.types.CodeChallengeMethod`. code_challenge: Code challenge string. + Returns: - An :py:class:`aioauth.models.AuthorizationCode` object. + An `aioauth.models.AuthorizationCode` object. """ raise NotImplementedError( "Method create_authorization_code must be implemented" @@ -138,15 +143,18 @@ async def get_authorization_code( If authorization code does not exists this function *must* return ``None`` to indicate to the validator that the requested authorization code does not exist or is invalid. + Note: This method is used by the grant type - :py:class:`aioauth.grant_type.AuthorizationCodeGrantType`. + `aioauth.grant_type.AuthorizationCodeGrantType`. + Args: - request: An :py:class:`aioauth.requests.Request`. + request: An `aioauth.requests.Request`. client_id: A user client ID. code: An authorization code. + Returns: - An optional :py:class:`aioauth.models.AuthorizationCode`. + An optional `aioauth.models.AuthorizationCode`. """ raise NotImplementedError( "Method get_authorization_code must be implemented for AuthorizationCodeGrantType" @@ -163,9 +171,10 @@ async def delete_authorization_code( Note: This method is used by the grant type - :py:class:`aioauth.grant_type.AuthorizationCodeGrantType`. + `aioauth.grant_type.AuthorizationCodeGrantType`. + Args: - request: An :py:class:`aioauth.requests.Request`. + request: An `aioauth.requests.Request`. client_id: A user client ID. code: An authorization code. """ @@ -186,17 +195,20 @@ async def get_client( Warning: If client does not exists in database this method *must* - return ``None`` to indicate to the validator that the - requested ``client_id`` does not exist or is invalid. + return `None` to indicate to the validator that the + requested `client_id` does not exist or is invalid. + Note: This method is used by all core grant types, as well as all core response types. + Args: - request: An :py:class:`aioauth.requests.Request`. + request: An `aioauth.requests.Request`. client_id: A user client ID. client_secret: An optional user client secret. + Returns: - An optional :py:class:`aioauth.models.Client` object. + An optional `aioauth.models.Client` object. """ raise NotImplementedError("Method get_client must be implemented") @@ -207,9 +219,11 @@ async def get_user(self, request: Request) -> Optional[Any]: Note: This method is used by the grant type - :py:class:`aioauth.grant_type.PasswordGrantType`. + `aioauth.grant_type.PasswordGrantType`. + Args: - request: An :py:class:`aioauth.requests.Request`. + request: An `aioauth.requests.Request`. + Returns: Boolean indicating whether or not the user was authenticated successfully. @@ -229,11 +243,11 @@ async def get_id_token( nonce: Optional[str] = None, ) -> str: """Returns an id_token. - For more information see `OpenID Connect Core 1.0 incorporating errata set 1 section 2 `_. + For more information see [OpenID Connect Core 1.0 incorporating errata set 1 section 2](https://openid.net/specs/openid-connect-core-1_0.html#IDToken). Note: - Method is used by response type :py:class:`aioauth.response_type.ResponseTypeIdToken` - and :py:class:`aioauth.oidc.core.grant_type.AuthorizationCodeGrantType`. + Method is used by response type `aioauth.response_type.ResponseTypeIdToken` + and `aioauth.oidc.core.grant_type.AuthorizationCodeGrantType`. """ raise NotImplementedError("get_id_token must be implemented.") diff --git a/aioauth/types.py b/aioauth/types.py index d2eadbca..de9beb0b 100644 --- a/aioauth/types.py +++ b/aioauth/types.py @@ -1,11 +1,8 @@ """ -.. code-block:: python - - from aioauth import types - Containers that contain constants used throughout the project. - ----- +```python +from aioauth import types +``` """ import sys diff --git a/aioauth/utils.py b/aioauth/utils.py index 2a943b9e..c6a432ec 100644 --- a/aioauth/utils.py +++ b/aioauth/utils.py @@ -1,12 +1,9 @@ """ -.. code-block:: python - - from aioauth import utils - Contains helper functions that is used throughout the project that doesn't pertain to a specific file or module. - ----- +```python +from aioauth import utils +``` """ import base64 @@ -59,8 +56,9 @@ def get_authorization_scheme_param( Args: authorization_header_value: Value of the authorization header. + Returns: - Tuple of the format ``(scheme, param)``. + Tuple of the format `(scheme, param)`. """ if not authorization_header_value: return "", "" @@ -74,14 +72,17 @@ def enforce_str(scope: List) -> str: Note: If a string is passed to this method it will simply return an - empty string back. Use :py:func:`enforce_list` to convert + empty string back. Use `enforce_list` to convert strings to scope lists. + Args: scope: An iterable or string that contains a list of scope. + Returns: A string of scopes seperated by spaces. + Raises: - TypeError: The ``scope`` value passed is not of the proper type. + TypeError: The `scope` value passed is not of the proper type. """ if isinstance(scope, (set, tuple, list)): return " ".join([str(s) for s in scope]) @@ -95,10 +96,12 @@ def enforce_list(scope: Optional[Union[str, List, Set, Tuple]]) -> List: Note: If an iterable is passed to this method it will return a list - representation of the iterable. Use :py:func:`enforce_str` to + representation of the iterable. Use `enforce_str` to convert iterables to a scope string. + Args: scope: An iterable or string that contains scopes. + Returns: A list of scopes. """ @@ -121,8 +124,9 @@ def generate_token(length: int = 30, chars: str = UNICODE_ASCII_CHARACTER_SET) - Args: length: Length of the generated token. chars: The characters to use to generate the string. + Returns: - Random string of length ``length`` and characters in ``chars``. + Random string of length `length` and characters in `chars`. """ rand = random.SystemRandom() return "".join(rand.choice(chars) for _ in range(length)) @@ -132,16 +136,17 @@ def build_uri( url: str, query_params: Optional[Dict] = None, fragment: Optional[Dict] = None ) -> str: """ - Builds an URI string from passed ``url``, ``query_params``, and + Builds an URI string from passed `url`, `query_params`, and ``fragment``. Args: url: URL string. query_params: Paramaters that contain the query. fragment: Fragment of the page. + Returns: - URL containing the original ``url``, and the added - ``query_params`` and ``fragment``. + URL containing the original `url`, and the added + `query_params` and `fragment`. """ if query_params is None: query_params = {} @@ -169,9 +174,10 @@ def encode_auth_headers(client_id: str, client_secret: str) -> HTTPHeaderDict: Args: client_id: The client's id. client_secret: The client's secret. + Returns: A case insensitive dictionary that contains the - ``Authorization`` header set to ``basic`` and the authorization + `Authorization` header set to `basic` and the authorization header. """ authorization = b64encode(f"{client_id}:{client_secret}".encode("ascii")) @@ -182,13 +188,15 @@ def decode_auth_headers(authorization: str) -> Tuple[str, str]: """ Decodes an encoded HTTP basic authentication string. Returns a tuple of the form ``(client_id, client_secret)``, and - raises a :py:class:`aioauth.errors.InvalidClientError` exception if nothing + raises a `aioauth.errors.InvalidClientError` exception if nothing could be decoded. Args: authorization: Authorization header string. + Returns: - Tuple of the form ``(client_id, client_secret)``. + Tuple of the form `(client_id, client_secret)`. + Raises: ValueError: Invalid `authorization` header string. """ @@ -211,16 +219,16 @@ def decode_auth_headers(authorization: str) -> Tuple[str, str]: def create_s256_code_challenge(code_verifier: str) -> str: """ - Create S256 code challenge with the passed ``code_verifier``. + Create S256 code challenge with the passed `code_verifier`. Note: - This function implements - ``base64url(sha256(ascii(code_verifier)))``. + This function implements: `base64url(sha256(ascii(code_verifier)))`. Args: code_verifier: Code verifier string. + Returns: Representation of the S256 code challenge with the passed - ``code_verifier``. + `code_verifier`. """ code_verifier_bytes = code_verifier.encode("utf-8") data = hashlib.sha256(code_verifier_bytes).digest() @@ -239,6 +247,7 @@ def build_error_response( exc: Exception used to generate HTTP response request: oauth request object skip_redirect_on_exc: Exception types to skip redirect on + Returns: OAuth HTTP response """ @@ -286,6 +295,7 @@ def catch_errors_and_unavailability( Args: f: A callable. + Returns: A callable with error catching capabilities. """ diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 0e40c48d..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -# Adds serve ability with auto-reload. -serve: - @sphinx-autobuild -b html source build/html - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..a7451ad6 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,33 @@ +# 🔐 aioauth + +aioauth is a spec-compliant OAuth 2.0 asynchronous Python module. aioauth works out-of-the-box with asynchronous server frameworks like FastAPI, Starlette, aiohttp, and others, as well as asynchronous database modules like Motor (MongoDB), aiopg (PostgreSQL), aiomysql (MySQL), or ORMs like Gino, sqlalchemy, or Tortoise. + +The magic of aioauth is its plug-and-play methods that allow the use of virtually any server or database framework. + +## Installing + +```bash +pip install aioauth +``` + +To install pre-releases: + +```bash +pip install git+https://github.com/aliev/aioauth +``` + +## Supported RFC + +aioauth supports the following RFCs: + +- [RFC 6749 - The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749) +- [RFC 7662 - OAuth 2.0 Token Introspection](https://tools.ietf.org/html/rfc7662) +- [RFC 7636 - Proof Key for Code Exchange by OAuth Public Clients](https://tools.ietf.org/html/rfc7636) + +## Pages + +- [Github Project](https://github.com/aliev/aioauth) +- [Issues](https://github.com/aliev/aioauth/issues) +- [Discussion](https://github.com/aliev/aioauth/discussions) + +---- diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 2119f510..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/sections/api/collections.md b/docs/sections/api/collections.md new file mode 100644 index 00000000..6ffb2655 --- /dev/null +++ b/docs/sections/api/collections.md @@ -0,0 +1,3 @@ +# Collections + +::: aioauth.collections diff --git a/docs/sections/api/config.md b/docs/sections/api/config.md new file mode 100644 index 00000000..80ee7b22 --- /dev/null +++ b/docs/sections/api/config.md @@ -0,0 +1,3 @@ +# Config + +::: aioauth.config diff --git a/docs/sections/api/constances.md b/docs/sections/api/constances.md new file mode 100644 index 00000000..6b909a40 --- /dev/null +++ b/docs/sections/api/constances.md @@ -0,0 +1,3 @@ +# Constances + +::: aioauth.constances diff --git a/docs/sections/api/errors.md b/docs/sections/api/errors.md new file mode 100644 index 00000000..eee28535 --- /dev/null +++ b/docs/sections/api/errors.md @@ -0,0 +1,3 @@ +# Errors + +::: aioauth.errors diff --git a/docs/sections/api/grant_type.md b/docs/sections/api/grant_type.md new file mode 100644 index 00000000..ea6bfc0e --- /dev/null +++ b/docs/sections/api/grant_type.md @@ -0,0 +1,3 @@ +# Grant Type + +::: aioauth.grant_type diff --git a/docs/sections/api/models.md b/docs/sections/api/models.md new file mode 100644 index 00000000..2c794812 --- /dev/null +++ b/docs/sections/api/models.md @@ -0,0 +1,3 @@ +# Models + +::: aioauth.models diff --git a/docs/sections/api/oidc/core/grant_type.md b/docs/sections/api/oidc/core/grant_type.md new file mode 100644 index 00000000..251f61ee --- /dev/null +++ b/docs/sections/api/oidc/core/grant_type.md @@ -0,0 +1,3 @@ +# Grant Type + +::: aioauth.oidc.core.grant_type diff --git a/docs/sections/api/oidc/core/requests.md b/docs/sections/api/oidc/core/requests.md new file mode 100644 index 00000000..591f8b1a --- /dev/null +++ b/docs/sections/api/oidc/core/requests.md @@ -0,0 +1,3 @@ +# Requests + +::: aioauth.oidc.core.requests diff --git a/docs/sections/api/oidc/core/responses.md b/docs/sections/api/oidc/core/responses.md new file mode 100644 index 00000000..d1a519c3 --- /dev/null +++ b/docs/sections/api/oidc/core/responses.md @@ -0,0 +1,3 @@ +# Responses + +::: aioauth.oidc.core.responses diff --git a/docs/sections/api/requests.md b/docs/sections/api/requests.md new file mode 100644 index 00000000..fbd04adf --- /dev/null +++ b/docs/sections/api/requests.md @@ -0,0 +1,3 @@ +# Requests + +::: aioauth.requests diff --git a/docs/sections/api/response_type.md b/docs/sections/api/response_type.md new file mode 100644 index 00000000..dda3cdc5 --- /dev/null +++ b/docs/sections/api/response_type.md @@ -0,0 +1,3 @@ +# Response Type + +::: aioauth.response_type diff --git a/docs/sections/api/responses.md b/docs/sections/api/responses.md new file mode 100644 index 00000000..ba0422e7 --- /dev/null +++ b/docs/sections/api/responses.md @@ -0,0 +1,3 @@ +# Responses + +::: aioauth.responses diff --git a/docs/sections/api/server.md b/docs/sections/api/server.md new file mode 100644 index 00000000..271cc9d5 --- /dev/null +++ b/docs/sections/api/server.md @@ -0,0 +1,3 @@ +# Server + +::: aioauth.server diff --git a/docs/sections/api/storage.md b/docs/sections/api/storage.md new file mode 100644 index 00000000..71441ff8 --- /dev/null +++ b/docs/sections/api/storage.md @@ -0,0 +1,3 @@ +# Storage + +::: aioauth.storage diff --git a/docs/sections/api/types.md b/docs/sections/api/types.md new file mode 100644 index 00000000..926159d8 --- /dev/null +++ b/docs/sections/api/types.md @@ -0,0 +1,3 @@ +# Types + +::: aioauth.types diff --git a/docs/sections/api/utils.md b/docs/sections/api/utils.md new file mode 100644 index 00000000..6867d510 --- /dev/null +++ b/docs/sections/api/utils.md @@ -0,0 +1,3 @@ +# Utils + +::: aioauth.utils diff --git a/docs/sections/examples/example.md b/docs/sections/examples/example.md new file mode 100644 index 00000000..2b92de9a --- /dev/null +++ b/docs/sections/examples/example.md @@ -0,0 +1,81 @@ +To help you get started, we've provided a working example of aioauth integrated into a minimal FastAPI server using an in-memory database. While the example is intentionally simple, it demonstrates a fully functional flow and can serve as a solid foundation. You can later adapt and extend it into a full production-ready solution tailored to your needs. + +### Installation + +```bash +$ git clone git@github.com:aliev/aioauth.git +$ cd aioauth/examples +$ pip install -r requirements.txt +$ python3 fastapi_example.py +``` + +### Testing + +Initialize an `authorization_code` request with the example server. + +``` +http://localhost:8000/oauth/authorize?client_id=test_client&redirect_uri=https%3A%2F%2Fwww.example.com%2Fredirect&response_type=code&state=somestate&scope=email +``` + +The oauth server authenticates the resource owner (via a login form). + +![login-form](./screenshots/login-form.png) + +The oauth server then checks whether the resource owner approves or +denies the client's access request. + +![login-form](./screenshots/approve-form.png) + +The oauth server will then generate a response as a redirect to the +specified `redirect_uri` in the initial request. If there is an error +with the initial client request, or the resource owner denies the +request the server will respond with an error, otherwise it will +return a success with a generated "authorization-code" + +An error response would look something like this: + +``` +https://www.example.com/redirect?error=access_denied&state=somestate +``` + +Whilst a success looks like this: + +``` +https://www.example.com/redirect?state=somestate&code=EJKOGQhY7KcWjNGI2UbCnOrqAGtRiCEJnAYNwYJ8M5&scope=email +``` + +The client can then request an access-token in exchange for the +authorization-code using the servers token endpoint. + +```bash +curl localhost:8000/oauth/tokenize \ + -u 'test_client:password' \ + -d 'grant_type=authorization_code' \ + -d 'code=EJKOGQhY7KcWjNGI2UbCnOrqAGtRiCEJnAYNwYJ8M5'\ + -d 'redirect_uri=https://www.example.com/redirect' +``` + +The server then responds with the associated `access_token`, `refresh_token`, +and its relevant data: + +```json +{ + "expires_in": 300, + "refresh_token_expires_in": 900, + "access_token": "TIQdQv5FCyBoFtoeGt1tAJ37EJdggl8xgSvCVbdjqD", + "refresh_token": "iJD7Yf4SFuSljmXOhyfjfZelc5J0uIe2P4hwGm4wORCDJyrT", + "scope": "email", + "token_type": "Bearer" +} +``` + +The access-token may be replaced/renewed using the specified `refresh_token` +using the `refresh_token` grant type, which returns the same set of data +before with new tokens. + +```bash +curl localhost:8000/oauth/tokenize \ + -u 'test_client:password' \ + -d 'grant_type=refresh_token' \ + -d 'refresh_token=iJD7Yf4SFuSljmXOhyfjfZelc5J0uIe2P4hwGm4wORCDJyrT' +``` diff --git a/docs/sections/examples/screenshots/approve-form.png b/docs/sections/examples/screenshots/approve-form.png new file mode 100644 index 00000000..966649fd Binary files /dev/null and b/docs/sections/examples/screenshots/approve-form.png differ diff --git a/docs/sections/examples/screenshots/login-form.png b/docs/sections/examples/screenshots/login-form.png new file mode 100644 index 00000000..de936351 Binary files /dev/null and b/docs/sections/examples/screenshots/login-form.png differ diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css deleted file mode 100644 index 8cf1b517..00000000 --- a/docs/source/_static/custom.css +++ /dev/null @@ -1,34 +0,0 @@ -/* Sidebar top (and topbar for mobile) */ -.wy-side-nav-search, -.wy-nav-top { - background: #264653; -} - -/* Sidebar top link */ -.wy-side-nav-search>a, -.wy-side-nav-search .wy-dropdown>a { - color: #FFF9F8 !important; -} - -/* Sidebar bottom */ -.wy-nav-side { - background: #2D5362; -} - -/* Sidebar bottom subsection */ -.wy-menu-vertical header, -.wy-menu-vertical p.caption { - color: #FFF9F8 !important; -} - -/* Sidebar bottom text */ -.wy-menu-vertical a { - color: #FFF9F8 !important; -} - -/* Sidebar bottom text selected */ -.wy-menu-vertical li.on a, -.wy-menu-vertical li.current>a, -.wy-menu-vertical li ul li a { - color: black !important; -} diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index 8a2c9006..00000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,58 +0,0 @@ -# -- Path setup -------------------------------------------------------------- - -from pathlib import Path -import tomllib -from aioauth import __version__ - -# Project root folder. -root = Path(__file__).parent.parent.parent - -# Loads __version__ file. -about = {} -with open(root / "pyproject.toml", "rb") as f: - about = tomllib.load(f) - -# -- Project information ----------------------------------------------------- - -project = about["project"]["description"] -author = about["project"]["authors"][0]["name"] -release = __version__ - -# -- General configuration --------------------------------------------------- - -extensions = [ - "sphinx.ext.viewcode", - "sphinx.ext.autodoc", - "sphinx.ext.napoleon", - "m2r2", - "sphinx_copybutton", -] - -templates_path = ["_templates"] -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] - - -# -- Options for HTML output ------------------------------------------------- - -html_theme = "sphinx_rtd_theme" -html_static_path = ["_static"] - - -# Adds custom css file. -def setup(app): - app.add_css_file("custom.css") - - -# -- Extra Configuration ----------------------------------------------------- - -# Order of docs. -autodoc_member_order = "bysource" - -# Turn off typehints. -autodoc_typehints = "signature" - -# Remove module names from class docs. -add_module_names = False - -# Show only class docs. -autoclass_content = "both" diff --git a/docs/source/contents.rst b/docs/source/contents.rst deleted file mode 100644 index 92b125e5..00000000 --- a/docs/source/contents.rst +++ /dev/null @@ -1,23 +0,0 @@ -.. toctree:: - :hidden: - - self - -.. toctree:: - :caption: Understanding - :glob: - :maxdepth: 2 - - sections/understanding/* - -.. toctree:: - :caption: Using - :glob: - :maxdepth: 2 - - sections/documentation/* - -.. toctree:: - :caption: FastAPI - :glob: - :maxdepth: 3 diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index aa734316..00000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,52 +0,0 @@ -🔐 aioauth -========== - -aioauth is a spec-compliant OAuth 2.0 asynchronous Python module. aioauth works out-of-the-box with asynchronous server frameworks like FastAPI, Starlette, aiohttp, and others, as well as asynchronous database modules like Motor (MongoDB), aiopg (PostgreSQL), aiomysql (MySQL), or ORMs like Gino, sqlalchemy, or Tortoise. - -The magic of aioauth is its plug-and-play methods that allow the use of virtually any server or database framework. - -Installing ----------- - -To install aioauth at the command line: - -.. code-block:: - - $ pip install aioauth - -To install pre-releases: - -.. code-block:: - - $ pip install git+https://github.com/aliev/aioauth - - -Supported RFC -------------- - -aioauth supports the following RFCs: - -* `RFC 6749 - The OAuth 2.0 Authorization Framework `_ -* `RFC 7662 - OAuth 2.0 Token Introspection `_ -* `RFC 7636 - Proof Key for Code Exchange by OAuth Public Clients `_ - -Pages ------ - -* `Github Project `_ -* `Issues `_ -* `Discussion `_ - ----- - -Sections -======== - -.. include:: contents.rst - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/source/sections/documentation/collections.rst b/docs/source/sections/documentation/collections.rst deleted file mode 100644 index 0e01dfae..00000000 --- a/docs/source/sections/documentation/collections.rst +++ /dev/null @@ -1,6 +0,0 @@ -Collections -=========== - -.. automodule:: aioauth.collections - :members: - :undoc-members: diff --git a/docs/source/sections/documentation/config.rst b/docs/source/sections/documentation/config.rst deleted file mode 100644 index ae32eaac..00000000 --- a/docs/source/sections/documentation/config.rst +++ /dev/null @@ -1,6 +0,0 @@ -Config -====== - -.. automodule:: aioauth.config - :members: - :undoc-members: diff --git a/docs/source/sections/documentation/constances.rst b/docs/source/sections/documentation/constances.rst deleted file mode 100644 index 8489fddb..00000000 --- a/docs/source/sections/documentation/constances.rst +++ /dev/null @@ -1,6 +0,0 @@ -Constances -========== - -.. automodule:: aioauth.constances - :members: - :undoc-members: diff --git a/docs/source/sections/documentation/errors.rst b/docs/source/sections/documentation/errors.rst deleted file mode 100644 index e77c1a33..00000000 --- a/docs/source/sections/documentation/errors.rst +++ /dev/null @@ -1,7 +0,0 @@ -Errors -====== - -.. automodule:: aioauth.errors - :members: - :undoc-members: - :exclude-members: OAuth2Error diff --git a/docs/source/sections/documentation/grant_type.rst b/docs/source/sections/documentation/grant_type.rst deleted file mode 100644 index 20f1188f..00000000 --- a/docs/source/sections/documentation/grant_type.rst +++ /dev/null @@ -1,6 +0,0 @@ -Grant Type -========== - -.. automodule:: aioauth.grant_type - :members: - :undoc-members: diff --git a/docs/source/sections/documentation/models.rst b/docs/source/sections/documentation/models.rst deleted file mode 100644 index 31a0fb62..00000000 --- a/docs/source/sections/documentation/models.rst +++ /dev/null @@ -1,6 +0,0 @@ -Models -====== - -.. automodule:: aioauth.models - :members: - :undoc-members: diff --git a/docs/source/sections/documentation/requests.rst b/docs/source/sections/documentation/requests.rst deleted file mode 100644 index 6bbc4439..00000000 --- a/docs/source/sections/documentation/requests.rst +++ /dev/null @@ -1,6 +0,0 @@ -Requests -======== - -.. automodule:: aioauth.requests - :members: - :undoc-members: diff --git a/docs/source/sections/documentation/response_type.rst b/docs/source/sections/documentation/response_type.rst deleted file mode 100644 index a55b76a0..00000000 --- a/docs/source/sections/documentation/response_type.rst +++ /dev/null @@ -1,6 +0,0 @@ -Response Type -============= - -.. automodule:: aioauth.response_type - :members: - :undoc-members: diff --git a/docs/source/sections/documentation/responses.rst b/docs/source/sections/documentation/responses.rst deleted file mode 100644 index 4a187a42..00000000 --- a/docs/source/sections/documentation/responses.rst +++ /dev/null @@ -1,6 +0,0 @@ -Responses -========= - -.. automodule:: aioauth.responses - :members: - :undoc-members: diff --git a/docs/source/sections/documentation/server.rst b/docs/source/sections/documentation/server.rst deleted file mode 100644 index b079eb7b..00000000 --- a/docs/source/sections/documentation/server.rst +++ /dev/null @@ -1,6 +0,0 @@ -Server -====== - -.. automodule:: aioauth.server - :members: - :undoc-members: diff --git a/docs/source/sections/documentation/storage.rst b/docs/source/sections/documentation/storage.rst deleted file mode 100644 index abecb155..00000000 --- a/docs/source/sections/documentation/storage.rst +++ /dev/null @@ -1,6 +0,0 @@ -Storage -======= - -.. automodule:: aioauth.storage - :members: - :undoc-members: diff --git a/docs/source/sections/documentation/types.rst b/docs/source/sections/documentation/types.rst deleted file mode 100644 index d8936d23..00000000 --- a/docs/source/sections/documentation/types.rst +++ /dev/null @@ -1,6 +0,0 @@ -Types -===== - -.. automodule:: aioauth.types - :members: - :undoc-members: diff --git a/docs/source/sections/documentation/utils.rst b/docs/source/sections/documentation/utils.rst deleted file mode 100644 index ea3c8c0e..00000000 --- a/docs/source/sections/documentation/utils.rst +++ /dev/null @@ -1,6 +0,0 @@ -Utils -===== - -.. automodule:: aioauth.utils - :members: - :undoc-members: diff --git a/docs/source/sections/understanding/plug_and_play.rst b/docs/source/sections/understanding/plug_and_play.rst deleted file mode 100644 index 7a0eacf8..00000000 --- a/docs/source/sections/understanding/plug_and_play.rst +++ /dev/null @@ -1,4 +0,0 @@ -Plug-and-Play -============= - -aioauth was designed to be as flexible as possible to allow developers to choose their own server framework, as well as database provider. Since aioauth is written as an asynchronous module it would be to the advantage of the developer to write their applications asynchronously. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..39fa35a0 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,51 @@ +site_name: aioauth +repo_url: https://github.com/aliev/aioauth +docs_dir: docs +theme: + name: material + palette: + - scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to light mode +plugins: + - search + - mkdocstrings: + default_handler: python + handlers: + python: + paths: ["."] +nav: + - Home: index.md + - Quick Start: sections/examples/example.md + - API: + - Collections: sections/api/collections.md + - Config: sections/api/config.md + - Constances: sections/api/constances.md + - Errors: sections/api/errors.md + - Grant Type: sections/api/grant_type.md + - Models: sections/api/models.md + - Requests: sections/api/requests.md + - Response Type: sections/api/response_type.md + - Responses: sections/api/responses.md + - Server: sections/api/server.md + - Storage: sections/api/storage.md + - Types: sections/api/types.md + - Utils: sections/api/utils.md + - OIDC: + - Core: + - Grant Type: sections/api/oidc/core/grant_type.md + - Requests: sections/api/oidc/core/requests.md + - Responses: sections/api/oidc/core/responses.md +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences diff --git a/pyproject.toml b/pyproject.toml index c1a81e8e..9005d362 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,11 +54,9 @@ dev = [ ] docs = [ - "sphinx", - "sphinx-copybutton", - "sphinx-autobuild", - "m2r2", - "sphinx-rtd-theme", + "mkdocs", + "mkdocs-material", + "mkdocstrings[python]", ] fastapi = [