diff options
Diffstat (limited to 'python-flask-azure-oauth.spec')
| -rw-r--r-- | python-flask-azure-oauth.spec | 1794 |
1 files changed, 1794 insertions, 0 deletions
diff --git a/python-flask-azure-oauth.spec b/python-flask-azure-oauth.spec new file mode 100644 index 0000000..ce6f76c --- /dev/null +++ b/python-flask-azure-oauth.spec @@ -0,0 +1,1794 @@ +%global _empty_manifest_terminate_build 0 +Name: python-flask-azure-oauth +Version: 0.7.0 +Release: 1 +Summary: Python Flask extension for using Azure Active Directory with OAuth to protect applications +License: MIT +URL: https://github.com/antarctica/flask-azure-oauth +Source0: https://mirrors.nju.edu.cn/pypi/web/packages/79/d1/9b28401655516bbee8aaa13b7622b0983a03549280f6d755c86b379a5243/flask-azure-oauth-0.7.0.tar.gz +BuildArch: noarch + +Requires: python3-authlib +Requires: python3-Flask +Requires: python3-requests + +%description +# Flask Azure AD OAuth Provider + +Python Flask extension for securing apps with Azure Active Directory OAuth + +## Purpose + +Provide an [AuthLib](https://authlib.org) +[Resource Protector/Server](https://docs.authlib.org/en/latest/flask/2/resource-server.html) to authenticate and +authorise users and applications using a Flask application with OAuth functionality offered by +[Azure Active Directory](https://azure.microsoft.com/en-us/services/active-directory/), as part of the +[Microsoft identity platform](https://docs.microsoft.com/en-us/azure/active-directory/develop/about-microsoft-identity-platform). + +Azure Active Directory, acting as an identity provider, issues +[OAuth access tokens](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens), the claims of +which are validated by this provider. These claims include the identity of the user and client application (used for +authentication), and any permissions/scopes assigned or delegated to the user or application (used for authorisation). + +This provider supports these scenarios: + +1. *application to application* + * supports authentication and authorisation + * used to allow a client application access to some functionality or resources provided by another application + * can be used for non-interactive, machine-to-machine, processes (using the OAuth Client Credentials Grant) + * optionally, uses the identity of the client application for authentication + * optionally, uses permissions assigned directly to the client application for authorisation +2. *user to application* + * supports authentication and authorisation + * used to allow users access to some functionality or resources provided by another application + * can be used for interactive console (using the Device Authorization Grant) or web application (using the OAuth + Authorization Code Grant) processes + * uses the identity of the user, and optionally, the client application they are using, for authentication + * optionally, uses permissions assigned to the user, permissions delegated by the user to the client application, + and/or permissions assigned directly to the client application for authorisation + +Other scenarios may work but are not officially supported, this may change in the future. + +**Note:** This provider does not support client applications requesting tokens from Azure. See the +[Microsoft Authentication Library (MSAL) for Python](https://github.com/AzureAD/microsoft-authentication-library-for-python) +package if you need to do this. + +**Note:** This provider has been written to solve an internal need within applications used by the British Antarctic +Survey. It is offered to others in the hope that's useful for your needs as well, however it does not (and cannot) +cover every option available. + +## Installation + +This package can be installed using Pip from [PyPi](https://pypi.org/project/flask-azure-oauth): + +``` +$ pip install flask-azure-oauth +``` + +**Note:** Since version 0.6.0, this package requires Flask 2.0 or greater. + +## Usage + +This provider provides an [AuthLib](https://authlib.org) +[Resource Protector](https://docs.authlib.org/en/latest/flask/2/resource-server.html) which can be used as a decorator +on Flask routes. + +A minimal application would look like this: + +```python +from flask import Flask + +from flask_azure_oauth import FlaskAzureOauth + +app = Flask(__name__) + +app.config['AZURE_OAUTH_TENANCY'] = 'xxx' +app.config['AZURE_OAUTH_APPLICATION_ID'] = 'xxx' + +auth = FlaskAzureOauth() +auth.init_app(app) + +@app.route('/unprotected') +def unprotected(): + return 'hello world' + +@app.route('/protected') +@auth() +def protected(): + return 'hello authenticated entity' + +@app.route('/protected-with-single-scope') +@auth('required-scope') +def protected_with_scope(): + return 'hello authenticated and authorised entity' + +@app.route('/protected-with-multiple-scopes') +@auth('required-scope1 required-scope2') +def protected_with_multiple_scopes(): + return 'hello authenticated and authorised entity' +``` + +To restrict a route to any valid user or client application (authentication): + +* add the resource protector as a decorator (`auth` in this example) - for example the `/protected` route + +To restrict a route to specific users (authorisation): + +* add any required [Scopes](#permissions-roles-and-scopes) to the decorator - for example the `/projected-with-*` routes + +Independently of these options, it's possible to require specific, trusted, client applications, regardless of the user +using them. This is useful in circumstances where a user may be authorised but the client can't be trusted: + +* set the `AZURE_OAUTH_CLIENT_APPLICATION_IDS` config option to a list of Azure application identifiers + +For example: + +``` +app.config['AZURE_OAUTH_CLIENT_APPLICATION_IDS'] = ['xxx']` +``` + +### Configuration options + +The resource protector requires two configuration options to validate tokens correctly. These are read from the Flask +[config object](http://flask.pocoo.org/docs/1.0/config/) through the `init_app()` method. + +| Configuration Option | Data Type | Required | Description | +| ------------------------------------ | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | +| `AZURE_OAUTH_TENANCY` | Str | Yes | ID of the Azure AD tenancy all applications and users are registered within | +| `AZURE_OAUTH_APPLICATION_ID` | Str | Yes | ID of the Azure AD application registration for the application being protected | +| `AZURE_OAUTH_CLIENT_APPLICATION_IDS` | List[Str] | No | ID(s) of the Azure AD application registration(s) for the application(s) granted access to the application being protected | + +**Note:** If the `AZURE_OAUTH_CLIENT_APPLICATION_IDS` option is not set, all client applications will be trusted and the +`azp` claim, if present, is ignored. + +Before these options can be set you will need to: + +1. [register the application to be protected](#registering-an-application-in-azure) +2. [define the permissions and roles this application supports](#defining-permissions-and-roles-within-an-application) +3. [register the application(s) that will use the protected application](#registering-an-application-in-azure) +4. [assign permissions to users and/or client application(s)](#assigning-permissions-and-roles-to-users-and-applications) + +### Flask session support + +This provider extends the AuthLib ResourceProtector to support detecting access tokens stored in the Flask session. + +This is intended for browser based applications where the `Authorization` header cannot be easily set to include the +access token. This support will be enabled automatically if an `access_token` session key is set. + +### Access token versions + +Since version 0.5.0, this provider is compatible with Azure access token versions 1.0 and 2.0. Prior to version 0.5.0 +only version 2.0 tokens could be used. See +[Microsoft's documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens) for the +differences between token versions. + +**Note:** If you use version 1.0 tokens, this provider expects at least one of the `identifierUris` property values to +be `api://{protected_application_id}`, where `{protected_application_id}` is the application ID of the app registration +representing the application being protected by this provider. Without this, you will receive errors for an invalid +audience. + +### Applications, users, groups and tenancies + +Azure Active Directory has a number of different concepts for agents that represent things being protected and things +that want to interact with protected things: + +* [applications](https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-scenarios#application-model) - + represent services that offer, or wish to use, functionality that should be restricted: + * services offering functionality are *protected applications*, e.g. an API + * services wishing to use functionality interactively or non-interactively, are *client applications*: + * interactive client applications include self-service portals for example + * non-interactive client applications include nightly synchronisation tasks for example +* [users](https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/directory-overview-user-model) - + represent individuals that wish to use functionality offered by protected applications, through one or more + client applications (e.g. a user may use a self-service portal to access information) +* [groups](https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/directory-overview-user-model) - + represent multiple users, for ease of managing permissions to similar users (e.g. administrative users) + +For management purposes, all agents are scoped to an Azure tenancy (with the exception of users that can be used across +tenancies). + +In the Azure management portal: + +* applications are represented by *Application registrations* +* users are represented by *users*, or optionally *groups* of users + +### Permissions, roles and scopes + +Azure Active Directory has a number of mechanisms for controlling how agents can interact with each other: + +* [roles](https://docs.microsoft.com/en-us/azure/architecture/multitenant-identity/app-roles) - functions, designations + or labels conferred on users and/or groups (e.g. `admins`, `staff`) +* [direct permissions](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent) - + capabilities of a protected application client applications can use themselves or without the consent of the current + user (e.g. machine-to-machine access to, or modification of, data from all users) +* [delegated permissions](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent) - + capabilities of a protected application the current user allows a client application to use (e.g. interactive access + to, or modification of, their data) + +Generally, and in terms of the OAuth ecosystem, all of these can be considered as +[scopes](https://tools.ietf.org/html/rfc6749#section-3.3). As discussed in the [Usage](#usage) section, scopes can be +used to control who and/or what can use features within protected applications. + +Scopes are included the access token generated by a client application (possibly interactively by a user) and presented +to the projected application as a bearer token. Azure encodes different mechanisms in different claims: + +* `roles` - for roles assigned to users and permissions directly assigned to client applications +* `scp` - for permissions delegated by the user to a client application + +For ease of use, this extension abstracts these two claims into a single set of `scopes` that can be required for a +given route. Multiple scopes can be required (as a logical AND) to allow scopes to be used more flexibly. + +#### Defining permissions and roles within an application + +Permissions and roles are defined in the +[application manifest](https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-app-manifest) of each +application being protected. They can then be [assigned](#assigning-permissions-and-roles-to-users-and-applications) to +users, groups and client applications. + +1. [register](#registering-an-application-in-azure) the application to be protected +2. [add permissions to application manifest](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps) + +For example: + +```json +"appRoles": [ + { + "allowedMemberTypes": [ + "Application" + ], + "displayName": "List all Foo resources", + "id": "112b3a76-2dd0-4d09-9976-9f94b2ed965d", + "isEnabled": true, + "description": "Allows access to basic information for all Foo resources", + "value": "Foo.List.All" + } +], +``` + +#### Assigning permissions and roles to users and applications + +Permissions and roles (collectively, application roles) are assigned through the Azure portal: + +1. [define roles and permissions in the protected application](#defining-permissions-and-roles-within-an-application) +2. [register](#registering-an-application-in-azure) the client application(s) +3. assign: + * [roles to users/groups](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps) + * [permissions to client applications](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#request-the-permissions-in-the-app-registration-portal) + +For assigning permissions: + +* permissions can be delegated to client applications, with the agreement of the current user +* permissions can be directly assigned to client applications, with the agreement of a tenancy administrator + +**Note:** Direct assignment is needed for non-interactive applications, such as daemons. + +#### Registering an application in Azure + +[Follow these instructions](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app). + +**Note:** These instructions apply both to applications that protected by this provider (protected applications), and +those that will be granted access to use such applications, possibly by a user (client applications). + +### Testing support + +For testing applications, a local/test JSON Web Key Set (JWKS) can be used to sign local/test JSON Web Tokens (JWTs) +without relying on Azure. Local tokens can include, or not include, arbitrary scopes/roles, which can ensure +requirements for specific scopes are properly enforced by this provider. + +This requires using local tokens signed by the test keys, and patching the `FlaskAzureOauth._get_jwks` method to +validate tokens using the same test keys. + +For example: + +```python +import unittest + +from http import HTTPStatus +from unittest.mock import patch + +from flask_azure_oauth import FlaskAzureOauth +from flask_azure_oauth.mocks.keys import TestJwk +from flask_azure_oauth.mocks.tokens import TestJwt + +from examples import create_app + + +class AppTestCase(unittest.TestCase): + def setUp(self): + self.test_jwks = TestJwk() + + with patch.object(FlaskAzureOauth, "_get_jwks") as mocked_get_jwks: + mocked_get_jwks.return_value = self.test_jwks.jwks() + + # `self.app` should be set to a Flask application, either by direct import, or by calling an app factory + self.app = create_app() + + self.app.config["TEST_JWKS"] = self.test_jwks + self.app_context = self.app.app_context() + self.app_context.push() + self.client = self.app.test_client() + + def test_protected_route_with_multiple_scopes_authorised(self): + # Generate token with required roles + token = TestJwt( + app=self.app, roles=["BAS.MAGIC.ADD.Records.Publish.All", "BAS.MAGIC.ADD.Records.ReadWrite.All"] + ) + + # Make request to protected route with token + response = self.client.get( + "/protected-with-multiple-scopes", headers={"authorization": f"bearer { token.dumps() }"} + ) + self.assertEqual(HTTPStatus.OK, response.status_code) + self.app_context.pop() + + def test_protected_route_with_multiple_scopes_unauthorised(self): + # Generate token with no scopes + token = TestJwt(app=self.app) + + # Make request to protected route with token + response = self.client.get( + "/protected-with-multiple-scopes", headers={"authorization": f"bearer { token.dumps() }"} + ) + self.assertEqual(HTTPStatus.FORBIDDEN, response.status_code) + self.app_context.pop() +``` + +## Developing + +This provider is developed as a Python library. A bundled Flask application is used to simulate its usage and act as +framework for running tests etc. + +### Development environment + +Git and [Poetry](https://python-poetry.org) are required to set up a local development environment of this project. + +**Note:** If you use [Pyenv](https://github.com/pyenv/pyenv), this project sets a local Python version for consistency. + +```shell +# clone from the BAS GitLab instance if possible +$ git clone https://gitlab.data.bas.ac.uk/web-apps/flask-extensions/flask-azure-oauth.git + +# alternatively, clone from the GitHub mirror +$ git clone https://github.com/antarctica/flask-azure-oauth.git + +# setup virtual environment +$ cd flask-azure-oauth +$ poetry install +``` + +### Code Style + +PEP-8 style and formatting guidelines must be used for this project, except the 80 character line limit. +[Black](https://github.com/psf/black) is used for formatting, configured in `pyproject.toml` and enforced as part of +[Python code linting](#code-linting). + +Black can be integrated with a range of editors, such as +[PyCharm](https://black.readthedocs.io/en/stable/integrations/editors.html#pycharm-intellij-idea), to apply formatting +automatically when saving files. + +To apply formatting manually: + +```shell +$ poetry run black src/ tests/ +``` + +### Code Linting + +[Flake8](https://flake8.pycqa.org) and various extensions are used to lint Python files. Specific checks, and any +configuration options, are documented in the `./.flake8` config file. + +To check files manually: + +```shell +$ poetry run flake8 src/ examples/ +``` + +Checks are run automatically in [Continuous Integration](#continuous-integration). + +### Dependencies + +Python dependencies for this project are managed with [Poetry](https://python-poetry.org) in `pyproject.toml`. + +Non-code files, such as static files, can also be included in the [Python package](#python-package) using the +`include` key in `pyproject.toml`. + +#### Adding new dependencies + +To add a new (development) dependency: + +```shell +$ poetry add [dependency] (--dev) +``` + +Then update the Docker image used for CI/CD builds and push to the BAS Docker Registry (which is provided by GitLab): + +```shell +$ docker build -f gitlab-ci.Dockerfile -t docker-registry.data.bas.ac.uk/web-apps/flask-extensions/flask-azure-oauth:latest . +$ docker push docker-registry.data.bas.ac.uk/web-apps/flask-extensions/flask-azure-oauth:latest +``` + +#### Updating dependencies + +```shell +$ poetry update +``` + +See the instructions above to update the Docker image used in CI/CD. + +#### Dependency vulnerability checks + +The [Safety](https://pypi.org/project/safety/) package is used to check dependencies against known vulnerabilities. + +**IMPORTANT!** As with all security tools, Safety is an aid for spotting common mistakes, not a guarantee of secure +code. In particular this is using the free vulnerability database, which is updated less frequently than paid options. + +This is a good tool for spotting low-hanging fruit in terms of vulnerabilities. It isn't a substitute for proper +vetting of dependencies, or a proper audit of potential issues by security professionals. If in any doubt you MUST seek +proper advice. + +Checks are run automatically in [Continuous Integration](#continuous-integration). + +To check locally: + +```shell +$ poetry export --without-hashes -f requirements.txt | poetry run safety check --full-report --stdin +``` + +#### `authlib` package + +The `authlib` dependency is locked to version `0.14.3` as the `0.15.x` release series contains a bug that prevents the +`kid` claim from being accessed from Jason Web Key (JWK) instances. This is a known issue and will be resolved in the +`1.x` release. See https://github.com/lepture/authlib/issues/314 for more information. + +### Static security scanning + +To ensure the security of this API, source code is checked against [Bandit](https://github.com/PyCQA/bandit) +and enforced as part of [Python code linting](#code-linting-python). + +**Warning:** Bandit is a static analysis tool and can't check for issues that are only be detectable when running the +application. As with all security tools, Bandit is an aid for spotting common mistakes, not a guarantee of secure code. + +To check manually: + +```shell +$ poetry run bandit -r src/ examples/ +``` + +**Note:** This package contains a number of testing methods that deliberately do insecure or nonsensical things. These +are necessary to test failure modes and error handling, they are not a risk when using this package as intended. These +workarounds have been exempted from these security checks where they apply. + +Checks are run automatically in [Continuous Integration](#continuous-integration). + +## Testing + +### Integration tests + +This project uses integration tests to ensure features work as expected and to guard against regressions and +vulnerabilities. + +The Python [UnitTest](https://docs.python.org/3/library/unittest.html) library is used for running tests using Flask's +test framework. Test cases are defined in files within `tests/` and are automatically loaded when using the `test` +Flask CLI command included in the local Flask application in the development environment. + +To run tests manually using PyCharm, use the included *App (tests)* run/debug configuration. + +To run tests manually: + +```shell +$ FLASK_APP=examples FLASK_ENV=testing poetry run python -m unittest discover +``` + +Tests are ran automatically in [Continuous Integration](#continuous-integration). + +### Continuous Integration + +All commits will trigger a Continuous Integration process using GitLab's CI/CD platform, configured in `.gitlab-ci.yml`. + +### Test/Example applications + +For verifying this provider works for real-world use-cases, a test Flask application is included in +`examples/__init__.py`. This test application acts as both an application providing access to, and accessing, protected +resources. It can use a number of application registrations registered in the BAS Web & Applications Test Azure AD. + +These applications allow testing different versions of access tokens for example. These applications are intended for +testing only. They do not represent real applications, or contain any sensitive or protected information. + +To test requesting resources from protected resources as an API, set the appropriate config options and run the +application container: + +```shell +$ FLASK_APP=examples poetry run flask +``` + +To test requesting resources from protected resources as a browser application, set the appropriate config options and +start the application container: + +```shell +$ FLASK_APP=examples poetry run flask run +``` + +Terraform is used to provision the application registrations used: + +``` +$ cd provisioning/terraform +$ docker-compose run terraform +$ az login --allow-no-subscriptions +$ terraform init +$ terraform validate +$ terraform apply +``` + +**Note:** Several properties in the application registration resources require setting once the registration has been +initially made (identifiers for example). These will need commenting out before use. + +Some properties, such as client secrets, can only be set once applications have been registered in the Azure Portal. + +Terraform state information is held in the BAS Terraform Remote State project (internal). + +## Deployment + +### Python package + +This project is distributed as a Python package, hosted in [PyPi](https://pypi.org/project/flask-azure-oauth). + +Source and binary packages are built and published automatically using +[Poetry](https://python-poetry.org) in [Continuous Deployment](#continuous-deployment). + +**Note:** Except for tagged releases, Python packages built in CD will use `0.0.0` as a version to indicate they are +not formal releases. + +### Continuous Deployment + +A Continuous Deployment process using GitLab's CI/CD platform is configured in `.gitlab-ci.yml`. + +## Release procedure + +For all releases: + +1. create a `release` branch +2. bump the version as appropriate in `pyproject.toml` +3. close release in `CHANGELOG.md` +4. push changes, merge the `release` branch into `main`, and tag with version + +The project will be built and published to PyPi automatically through [Continuous Deployment](#continuous-deployment). + +## Feedback + +The maintainer of this project is the BAS Web & Applications Team, they can be contacted at: +[servicedesk@bas.ac.uk](mailto:servicedesk@bas.ac.uk). + +## Issue tracking + +This project uses issue tracking, see the +[Issue tracker](https://gitlab.data.bas.ac.uk/web-apps/flask-extensions/flask-azure-oauth/issues) for more information. + +**Note:** Read & write access to this issue tracker is restricted. Contact the project maintainer to request access. + +## License + +Copyright (c) 2019-2022 UK Research and Innovation (UKRI), British Antarctic Survey. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +%package -n python3-flask-azure-oauth +Summary: Python Flask extension for using Azure Active Directory with OAuth to protect applications +Provides: python-flask-azure-oauth +BuildRequires: python3-devel +BuildRequires: python3-setuptools +BuildRequires: python3-pip +%description -n python3-flask-azure-oauth +# Flask Azure AD OAuth Provider + +Python Flask extension for securing apps with Azure Active Directory OAuth + +## Purpose + +Provide an [AuthLib](https://authlib.org) +[Resource Protector/Server](https://docs.authlib.org/en/latest/flask/2/resource-server.html) to authenticate and +authorise users and applications using a Flask application with OAuth functionality offered by +[Azure Active Directory](https://azure.microsoft.com/en-us/services/active-directory/), as part of the +[Microsoft identity platform](https://docs.microsoft.com/en-us/azure/active-directory/develop/about-microsoft-identity-platform). + +Azure Active Directory, acting as an identity provider, issues +[OAuth access tokens](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens), the claims of +which are validated by this provider. These claims include the identity of the user and client application (used for +authentication), and any permissions/scopes assigned or delegated to the user or application (used for authorisation). + +This provider supports these scenarios: + +1. *application to application* + * supports authentication and authorisation + * used to allow a client application access to some functionality or resources provided by another application + * can be used for non-interactive, machine-to-machine, processes (using the OAuth Client Credentials Grant) + * optionally, uses the identity of the client application for authentication + * optionally, uses permissions assigned directly to the client application for authorisation +2. *user to application* + * supports authentication and authorisation + * used to allow users access to some functionality or resources provided by another application + * can be used for interactive console (using the Device Authorization Grant) or web application (using the OAuth + Authorization Code Grant) processes + * uses the identity of the user, and optionally, the client application they are using, for authentication + * optionally, uses permissions assigned to the user, permissions delegated by the user to the client application, + and/or permissions assigned directly to the client application for authorisation + +Other scenarios may work but are not officially supported, this may change in the future. + +**Note:** This provider does not support client applications requesting tokens from Azure. See the +[Microsoft Authentication Library (MSAL) for Python](https://github.com/AzureAD/microsoft-authentication-library-for-python) +package if you need to do this. + +**Note:** This provider has been written to solve an internal need within applications used by the British Antarctic +Survey. It is offered to others in the hope that's useful for your needs as well, however it does not (and cannot) +cover every option available. + +## Installation + +This package can be installed using Pip from [PyPi](https://pypi.org/project/flask-azure-oauth): + +``` +$ pip install flask-azure-oauth +``` + +**Note:** Since version 0.6.0, this package requires Flask 2.0 or greater. + +## Usage + +This provider provides an [AuthLib](https://authlib.org) +[Resource Protector](https://docs.authlib.org/en/latest/flask/2/resource-server.html) which can be used as a decorator +on Flask routes. + +A minimal application would look like this: + +```python +from flask import Flask + +from flask_azure_oauth import FlaskAzureOauth + +app = Flask(__name__) + +app.config['AZURE_OAUTH_TENANCY'] = 'xxx' +app.config['AZURE_OAUTH_APPLICATION_ID'] = 'xxx' + +auth = FlaskAzureOauth() +auth.init_app(app) + +@app.route('/unprotected') +def unprotected(): + return 'hello world' + +@app.route('/protected') +@auth() +def protected(): + return 'hello authenticated entity' + +@app.route('/protected-with-single-scope') +@auth('required-scope') +def protected_with_scope(): + return 'hello authenticated and authorised entity' + +@app.route('/protected-with-multiple-scopes') +@auth('required-scope1 required-scope2') +def protected_with_multiple_scopes(): + return 'hello authenticated and authorised entity' +``` + +To restrict a route to any valid user or client application (authentication): + +* add the resource protector as a decorator (`auth` in this example) - for example the `/protected` route + +To restrict a route to specific users (authorisation): + +* add any required [Scopes](#permissions-roles-and-scopes) to the decorator - for example the `/projected-with-*` routes + +Independently of these options, it's possible to require specific, trusted, client applications, regardless of the user +using them. This is useful in circumstances where a user may be authorised but the client can't be trusted: + +* set the `AZURE_OAUTH_CLIENT_APPLICATION_IDS` config option to a list of Azure application identifiers + +For example: + +``` +app.config['AZURE_OAUTH_CLIENT_APPLICATION_IDS'] = ['xxx']` +``` + +### Configuration options + +The resource protector requires two configuration options to validate tokens correctly. These are read from the Flask +[config object](http://flask.pocoo.org/docs/1.0/config/) through the `init_app()` method. + +| Configuration Option | Data Type | Required | Description | +| ------------------------------------ | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | +| `AZURE_OAUTH_TENANCY` | Str | Yes | ID of the Azure AD tenancy all applications and users are registered within | +| `AZURE_OAUTH_APPLICATION_ID` | Str | Yes | ID of the Azure AD application registration for the application being protected | +| `AZURE_OAUTH_CLIENT_APPLICATION_IDS` | List[Str] | No | ID(s) of the Azure AD application registration(s) for the application(s) granted access to the application being protected | + +**Note:** If the `AZURE_OAUTH_CLIENT_APPLICATION_IDS` option is not set, all client applications will be trusted and the +`azp` claim, if present, is ignored. + +Before these options can be set you will need to: + +1. [register the application to be protected](#registering-an-application-in-azure) +2. [define the permissions and roles this application supports](#defining-permissions-and-roles-within-an-application) +3. [register the application(s) that will use the protected application](#registering-an-application-in-azure) +4. [assign permissions to users and/or client application(s)](#assigning-permissions-and-roles-to-users-and-applications) + +### Flask session support + +This provider extends the AuthLib ResourceProtector to support detecting access tokens stored in the Flask session. + +This is intended for browser based applications where the `Authorization` header cannot be easily set to include the +access token. This support will be enabled automatically if an `access_token` session key is set. + +### Access token versions + +Since version 0.5.0, this provider is compatible with Azure access token versions 1.0 and 2.0. Prior to version 0.5.0 +only version 2.0 tokens could be used. See +[Microsoft's documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens) for the +differences between token versions. + +**Note:** If you use version 1.0 tokens, this provider expects at least one of the `identifierUris` property values to +be `api://{protected_application_id}`, where `{protected_application_id}` is the application ID of the app registration +representing the application being protected by this provider. Without this, you will receive errors for an invalid +audience. + +### Applications, users, groups and tenancies + +Azure Active Directory has a number of different concepts for agents that represent things being protected and things +that want to interact with protected things: + +* [applications](https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-scenarios#application-model) - + represent services that offer, or wish to use, functionality that should be restricted: + * services offering functionality are *protected applications*, e.g. an API + * services wishing to use functionality interactively or non-interactively, are *client applications*: + * interactive client applications include self-service portals for example + * non-interactive client applications include nightly synchronisation tasks for example +* [users](https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/directory-overview-user-model) - + represent individuals that wish to use functionality offered by protected applications, through one or more + client applications (e.g. a user may use a self-service portal to access information) +* [groups](https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/directory-overview-user-model) - + represent multiple users, for ease of managing permissions to similar users (e.g. administrative users) + +For management purposes, all agents are scoped to an Azure tenancy (with the exception of users that can be used across +tenancies). + +In the Azure management portal: + +* applications are represented by *Application registrations* +* users are represented by *users*, or optionally *groups* of users + +### Permissions, roles and scopes + +Azure Active Directory has a number of mechanisms for controlling how agents can interact with each other: + +* [roles](https://docs.microsoft.com/en-us/azure/architecture/multitenant-identity/app-roles) - functions, designations + or labels conferred on users and/or groups (e.g. `admins`, `staff`) +* [direct permissions](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent) - + capabilities of a protected application client applications can use themselves or without the consent of the current + user (e.g. machine-to-machine access to, or modification of, data from all users) +* [delegated permissions](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent) - + capabilities of a protected application the current user allows a client application to use (e.g. interactive access + to, or modification of, their data) + +Generally, and in terms of the OAuth ecosystem, all of these can be considered as +[scopes](https://tools.ietf.org/html/rfc6749#section-3.3). As discussed in the [Usage](#usage) section, scopes can be +used to control who and/or what can use features within protected applications. + +Scopes are included the access token generated by a client application (possibly interactively by a user) and presented +to the projected application as a bearer token. Azure encodes different mechanisms in different claims: + +* `roles` - for roles assigned to users and permissions directly assigned to client applications +* `scp` - for permissions delegated by the user to a client application + +For ease of use, this extension abstracts these two claims into a single set of `scopes` that can be required for a +given route. Multiple scopes can be required (as a logical AND) to allow scopes to be used more flexibly. + +#### Defining permissions and roles within an application + +Permissions and roles are defined in the +[application manifest](https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-app-manifest) of each +application being protected. They can then be [assigned](#assigning-permissions-and-roles-to-users-and-applications) to +users, groups and client applications. + +1. [register](#registering-an-application-in-azure) the application to be protected +2. [add permissions to application manifest](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps) + +For example: + +```json +"appRoles": [ + { + "allowedMemberTypes": [ + "Application" + ], + "displayName": "List all Foo resources", + "id": "112b3a76-2dd0-4d09-9976-9f94b2ed965d", + "isEnabled": true, + "description": "Allows access to basic information for all Foo resources", + "value": "Foo.List.All" + } +], +``` + +#### Assigning permissions and roles to users and applications + +Permissions and roles (collectively, application roles) are assigned through the Azure portal: + +1. [define roles and permissions in the protected application](#defining-permissions-and-roles-within-an-application) +2. [register](#registering-an-application-in-azure) the client application(s) +3. assign: + * [roles to users/groups](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps) + * [permissions to client applications](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#request-the-permissions-in-the-app-registration-portal) + +For assigning permissions: + +* permissions can be delegated to client applications, with the agreement of the current user +* permissions can be directly assigned to client applications, with the agreement of a tenancy administrator + +**Note:** Direct assignment is needed for non-interactive applications, such as daemons. + +#### Registering an application in Azure + +[Follow these instructions](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app). + +**Note:** These instructions apply both to applications that protected by this provider (protected applications), and +those that will be granted access to use such applications, possibly by a user (client applications). + +### Testing support + +For testing applications, a local/test JSON Web Key Set (JWKS) can be used to sign local/test JSON Web Tokens (JWTs) +without relying on Azure. Local tokens can include, or not include, arbitrary scopes/roles, which can ensure +requirements for specific scopes are properly enforced by this provider. + +This requires using local tokens signed by the test keys, and patching the `FlaskAzureOauth._get_jwks` method to +validate tokens using the same test keys. + +For example: + +```python +import unittest + +from http import HTTPStatus +from unittest.mock import patch + +from flask_azure_oauth import FlaskAzureOauth +from flask_azure_oauth.mocks.keys import TestJwk +from flask_azure_oauth.mocks.tokens import TestJwt + +from examples import create_app + + +class AppTestCase(unittest.TestCase): + def setUp(self): + self.test_jwks = TestJwk() + + with patch.object(FlaskAzureOauth, "_get_jwks") as mocked_get_jwks: + mocked_get_jwks.return_value = self.test_jwks.jwks() + + # `self.app` should be set to a Flask application, either by direct import, or by calling an app factory + self.app = create_app() + + self.app.config["TEST_JWKS"] = self.test_jwks + self.app_context = self.app.app_context() + self.app_context.push() + self.client = self.app.test_client() + + def test_protected_route_with_multiple_scopes_authorised(self): + # Generate token with required roles + token = TestJwt( + app=self.app, roles=["BAS.MAGIC.ADD.Records.Publish.All", "BAS.MAGIC.ADD.Records.ReadWrite.All"] + ) + + # Make request to protected route with token + response = self.client.get( + "/protected-with-multiple-scopes", headers={"authorization": f"bearer { token.dumps() }"} + ) + self.assertEqual(HTTPStatus.OK, response.status_code) + self.app_context.pop() + + def test_protected_route_with_multiple_scopes_unauthorised(self): + # Generate token with no scopes + token = TestJwt(app=self.app) + + # Make request to protected route with token + response = self.client.get( + "/protected-with-multiple-scopes", headers={"authorization": f"bearer { token.dumps() }"} + ) + self.assertEqual(HTTPStatus.FORBIDDEN, response.status_code) + self.app_context.pop() +``` + +## Developing + +This provider is developed as a Python library. A bundled Flask application is used to simulate its usage and act as +framework for running tests etc. + +### Development environment + +Git and [Poetry](https://python-poetry.org) are required to set up a local development environment of this project. + +**Note:** If you use [Pyenv](https://github.com/pyenv/pyenv), this project sets a local Python version for consistency. + +```shell +# clone from the BAS GitLab instance if possible +$ git clone https://gitlab.data.bas.ac.uk/web-apps/flask-extensions/flask-azure-oauth.git + +# alternatively, clone from the GitHub mirror +$ git clone https://github.com/antarctica/flask-azure-oauth.git + +# setup virtual environment +$ cd flask-azure-oauth +$ poetry install +``` + +### Code Style + +PEP-8 style and formatting guidelines must be used for this project, except the 80 character line limit. +[Black](https://github.com/psf/black) is used for formatting, configured in `pyproject.toml` and enforced as part of +[Python code linting](#code-linting). + +Black can be integrated with a range of editors, such as +[PyCharm](https://black.readthedocs.io/en/stable/integrations/editors.html#pycharm-intellij-idea), to apply formatting +automatically when saving files. + +To apply formatting manually: + +```shell +$ poetry run black src/ tests/ +``` + +### Code Linting + +[Flake8](https://flake8.pycqa.org) and various extensions are used to lint Python files. Specific checks, and any +configuration options, are documented in the `./.flake8` config file. + +To check files manually: + +```shell +$ poetry run flake8 src/ examples/ +``` + +Checks are run automatically in [Continuous Integration](#continuous-integration). + +### Dependencies + +Python dependencies for this project are managed with [Poetry](https://python-poetry.org) in `pyproject.toml`. + +Non-code files, such as static files, can also be included in the [Python package](#python-package) using the +`include` key in `pyproject.toml`. + +#### Adding new dependencies + +To add a new (development) dependency: + +```shell +$ poetry add [dependency] (--dev) +``` + +Then update the Docker image used for CI/CD builds and push to the BAS Docker Registry (which is provided by GitLab): + +```shell +$ docker build -f gitlab-ci.Dockerfile -t docker-registry.data.bas.ac.uk/web-apps/flask-extensions/flask-azure-oauth:latest . +$ docker push docker-registry.data.bas.ac.uk/web-apps/flask-extensions/flask-azure-oauth:latest +``` + +#### Updating dependencies + +```shell +$ poetry update +``` + +See the instructions above to update the Docker image used in CI/CD. + +#### Dependency vulnerability checks + +The [Safety](https://pypi.org/project/safety/) package is used to check dependencies against known vulnerabilities. + +**IMPORTANT!** As with all security tools, Safety is an aid for spotting common mistakes, not a guarantee of secure +code. In particular this is using the free vulnerability database, which is updated less frequently than paid options. + +This is a good tool for spotting low-hanging fruit in terms of vulnerabilities. It isn't a substitute for proper +vetting of dependencies, or a proper audit of potential issues by security professionals. If in any doubt you MUST seek +proper advice. + +Checks are run automatically in [Continuous Integration](#continuous-integration). + +To check locally: + +```shell +$ poetry export --without-hashes -f requirements.txt | poetry run safety check --full-report --stdin +``` + +#### `authlib` package + +The `authlib` dependency is locked to version `0.14.3` as the `0.15.x` release series contains a bug that prevents the +`kid` claim from being accessed from Jason Web Key (JWK) instances. This is a known issue and will be resolved in the +`1.x` release. See https://github.com/lepture/authlib/issues/314 for more information. + +### Static security scanning + +To ensure the security of this API, source code is checked against [Bandit](https://github.com/PyCQA/bandit) +and enforced as part of [Python code linting](#code-linting-python). + +**Warning:** Bandit is a static analysis tool and can't check for issues that are only be detectable when running the +application. As with all security tools, Bandit is an aid for spotting common mistakes, not a guarantee of secure code. + +To check manually: + +```shell +$ poetry run bandit -r src/ examples/ +``` + +**Note:** This package contains a number of testing methods that deliberately do insecure or nonsensical things. These +are necessary to test failure modes and error handling, they are not a risk when using this package as intended. These +workarounds have been exempted from these security checks where they apply. + +Checks are run automatically in [Continuous Integration](#continuous-integration). + +## Testing + +### Integration tests + +This project uses integration tests to ensure features work as expected and to guard against regressions and +vulnerabilities. + +The Python [UnitTest](https://docs.python.org/3/library/unittest.html) library is used for running tests using Flask's +test framework. Test cases are defined in files within `tests/` and are automatically loaded when using the `test` +Flask CLI command included in the local Flask application in the development environment. + +To run tests manually using PyCharm, use the included *App (tests)* run/debug configuration. + +To run tests manually: + +```shell +$ FLASK_APP=examples FLASK_ENV=testing poetry run python -m unittest discover +``` + +Tests are ran automatically in [Continuous Integration](#continuous-integration). + +### Continuous Integration + +All commits will trigger a Continuous Integration process using GitLab's CI/CD platform, configured in `.gitlab-ci.yml`. + +### Test/Example applications + +For verifying this provider works for real-world use-cases, a test Flask application is included in +`examples/__init__.py`. This test application acts as both an application providing access to, and accessing, protected +resources. It can use a number of application registrations registered in the BAS Web & Applications Test Azure AD. + +These applications allow testing different versions of access tokens for example. These applications are intended for +testing only. They do not represent real applications, or contain any sensitive or protected information. + +To test requesting resources from protected resources as an API, set the appropriate config options and run the +application container: + +```shell +$ FLASK_APP=examples poetry run flask +``` + +To test requesting resources from protected resources as a browser application, set the appropriate config options and +start the application container: + +```shell +$ FLASK_APP=examples poetry run flask run +``` + +Terraform is used to provision the application registrations used: + +``` +$ cd provisioning/terraform +$ docker-compose run terraform +$ az login --allow-no-subscriptions +$ terraform init +$ terraform validate +$ terraform apply +``` + +**Note:** Several properties in the application registration resources require setting once the registration has been +initially made (identifiers for example). These will need commenting out before use. + +Some properties, such as client secrets, can only be set once applications have been registered in the Azure Portal. + +Terraform state information is held in the BAS Terraform Remote State project (internal). + +## Deployment + +### Python package + +This project is distributed as a Python package, hosted in [PyPi](https://pypi.org/project/flask-azure-oauth). + +Source and binary packages are built and published automatically using +[Poetry](https://python-poetry.org) in [Continuous Deployment](#continuous-deployment). + +**Note:** Except for tagged releases, Python packages built in CD will use `0.0.0` as a version to indicate they are +not formal releases. + +### Continuous Deployment + +A Continuous Deployment process using GitLab's CI/CD platform is configured in `.gitlab-ci.yml`. + +## Release procedure + +For all releases: + +1. create a `release` branch +2. bump the version as appropriate in `pyproject.toml` +3. close release in `CHANGELOG.md` +4. push changes, merge the `release` branch into `main`, and tag with version + +The project will be built and published to PyPi automatically through [Continuous Deployment](#continuous-deployment). + +## Feedback + +The maintainer of this project is the BAS Web & Applications Team, they can be contacted at: +[servicedesk@bas.ac.uk](mailto:servicedesk@bas.ac.uk). + +## Issue tracking + +This project uses issue tracking, see the +[Issue tracker](https://gitlab.data.bas.ac.uk/web-apps/flask-extensions/flask-azure-oauth/issues) for more information. + +**Note:** Read & write access to this issue tracker is restricted. Contact the project maintainer to request access. + +## License + +Copyright (c) 2019-2022 UK Research and Innovation (UKRI), British Antarctic Survey. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +%package help +Summary: Development documents and examples for flask-azure-oauth +Provides: python3-flask-azure-oauth-doc +%description help +# Flask Azure AD OAuth Provider + +Python Flask extension for securing apps with Azure Active Directory OAuth + +## Purpose + +Provide an [AuthLib](https://authlib.org) +[Resource Protector/Server](https://docs.authlib.org/en/latest/flask/2/resource-server.html) to authenticate and +authorise users and applications using a Flask application with OAuth functionality offered by +[Azure Active Directory](https://azure.microsoft.com/en-us/services/active-directory/), as part of the +[Microsoft identity platform](https://docs.microsoft.com/en-us/azure/active-directory/develop/about-microsoft-identity-platform). + +Azure Active Directory, acting as an identity provider, issues +[OAuth access tokens](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens), the claims of +which are validated by this provider. These claims include the identity of the user and client application (used for +authentication), and any permissions/scopes assigned or delegated to the user or application (used for authorisation). + +This provider supports these scenarios: + +1. *application to application* + * supports authentication and authorisation + * used to allow a client application access to some functionality or resources provided by another application + * can be used for non-interactive, machine-to-machine, processes (using the OAuth Client Credentials Grant) + * optionally, uses the identity of the client application for authentication + * optionally, uses permissions assigned directly to the client application for authorisation +2. *user to application* + * supports authentication and authorisation + * used to allow users access to some functionality or resources provided by another application + * can be used for interactive console (using the Device Authorization Grant) or web application (using the OAuth + Authorization Code Grant) processes + * uses the identity of the user, and optionally, the client application they are using, for authentication + * optionally, uses permissions assigned to the user, permissions delegated by the user to the client application, + and/or permissions assigned directly to the client application for authorisation + +Other scenarios may work but are not officially supported, this may change in the future. + +**Note:** This provider does not support client applications requesting tokens from Azure. See the +[Microsoft Authentication Library (MSAL) for Python](https://github.com/AzureAD/microsoft-authentication-library-for-python) +package if you need to do this. + +**Note:** This provider has been written to solve an internal need within applications used by the British Antarctic +Survey. It is offered to others in the hope that's useful for your needs as well, however it does not (and cannot) +cover every option available. + +## Installation + +This package can be installed using Pip from [PyPi](https://pypi.org/project/flask-azure-oauth): + +``` +$ pip install flask-azure-oauth +``` + +**Note:** Since version 0.6.0, this package requires Flask 2.0 or greater. + +## Usage + +This provider provides an [AuthLib](https://authlib.org) +[Resource Protector](https://docs.authlib.org/en/latest/flask/2/resource-server.html) which can be used as a decorator +on Flask routes. + +A minimal application would look like this: + +```python +from flask import Flask + +from flask_azure_oauth import FlaskAzureOauth + +app = Flask(__name__) + +app.config['AZURE_OAUTH_TENANCY'] = 'xxx' +app.config['AZURE_OAUTH_APPLICATION_ID'] = 'xxx' + +auth = FlaskAzureOauth() +auth.init_app(app) + +@app.route('/unprotected') +def unprotected(): + return 'hello world' + +@app.route('/protected') +@auth() +def protected(): + return 'hello authenticated entity' + +@app.route('/protected-with-single-scope') +@auth('required-scope') +def protected_with_scope(): + return 'hello authenticated and authorised entity' + +@app.route('/protected-with-multiple-scopes') +@auth('required-scope1 required-scope2') +def protected_with_multiple_scopes(): + return 'hello authenticated and authorised entity' +``` + +To restrict a route to any valid user or client application (authentication): + +* add the resource protector as a decorator (`auth` in this example) - for example the `/protected` route + +To restrict a route to specific users (authorisation): + +* add any required [Scopes](#permissions-roles-and-scopes) to the decorator - for example the `/projected-with-*` routes + +Independently of these options, it's possible to require specific, trusted, client applications, regardless of the user +using them. This is useful in circumstances where a user may be authorised but the client can't be trusted: + +* set the `AZURE_OAUTH_CLIENT_APPLICATION_IDS` config option to a list of Azure application identifiers + +For example: + +``` +app.config['AZURE_OAUTH_CLIENT_APPLICATION_IDS'] = ['xxx']` +``` + +### Configuration options + +The resource protector requires two configuration options to validate tokens correctly. These are read from the Flask +[config object](http://flask.pocoo.org/docs/1.0/config/) through the `init_app()` method. + +| Configuration Option | Data Type | Required | Description | +| ------------------------------------ | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | +| `AZURE_OAUTH_TENANCY` | Str | Yes | ID of the Azure AD tenancy all applications and users are registered within | +| `AZURE_OAUTH_APPLICATION_ID` | Str | Yes | ID of the Azure AD application registration for the application being protected | +| `AZURE_OAUTH_CLIENT_APPLICATION_IDS` | List[Str] | No | ID(s) of the Azure AD application registration(s) for the application(s) granted access to the application being protected | + +**Note:** If the `AZURE_OAUTH_CLIENT_APPLICATION_IDS` option is not set, all client applications will be trusted and the +`azp` claim, if present, is ignored. + +Before these options can be set you will need to: + +1. [register the application to be protected](#registering-an-application-in-azure) +2. [define the permissions and roles this application supports](#defining-permissions-and-roles-within-an-application) +3. [register the application(s) that will use the protected application](#registering-an-application-in-azure) +4. [assign permissions to users and/or client application(s)](#assigning-permissions-and-roles-to-users-and-applications) + +### Flask session support + +This provider extends the AuthLib ResourceProtector to support detecting access tokens stored in the Flask session. + +This is intended for browser based applications where the `Authorization` header cannot be easily set to include the +access token. This support will be enabled automatically if an `access_token` session key is set. + +### Access token versions + +Since version 0.5.0, this provider is compatible with Azure access token versions 1.0 and 2.0. Prior to version 0.5.0 +only version 2.0 tokens could be used. See +[Microsoft's documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens) for the +differences between token versions. + +**Note:** If you use version 1.0 tokens, this provider expects at least one of the `identifierUris` property values to +be `api://{protected_application_id}`, where `{protected_application_id}` is the application ID of the app registration +representing the application being protected by this provider. Without this, you will receive errors for an invalid +audience. + +### Applications, users, groups and tenancies + +Azure Active Directory has a number of different concepts for agents that represent things being protected and things +that want to interact with protected things: + +* [applications](https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-scenarios#application-model) - + represent services that offer, or wish to use, functionality that should be restricted: + * services offering functionality are *protected applications*, e.g. an API + * services wishing to use functionality interactively or non-interactively, are *client applications*: + * interactive client applications include self-service portals for example + * non-interactive client applications include nightly synchronisation tasks for example +* [users](https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/directory-overview-user-model) - + represent individuals that wish to use functionality offered by protected applications, through one or more + client applications (e.g. a user may use a self-service portal to access information) +* [groups](https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/directory-overview-user-model) - + represent multiple users, for ease of managing permissions to similar users (e.g. administrative users) + +For management purposes, all agents are scoped to an Azure tenancy (with the exception of users that can be used across +tenancies). + +In the Azure management portal: + +* applications are represented by *Application registrations* +* users are represented by *users*, or optionally *groups* of users + +### Permissions, roles and scopes + +Azure Active Directory has a number of mechanisms for controlling how agents can interact with each other: + +* [roles](https://docs.microsoft.com/en-us/azure/architecture/multitenant-identity/app-roles) - functions, designations + or labels conferred on users and/or groups (e.g. `admins`, `staff`) +* [direct permissions](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent) - + capabilities of a protected application client applications can use themselves or without the consent of the current + user (e.g. machine-to-machine access to, or modification of, data from all users) +* [delegated permissions](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent) - + capabilities of a protected application the current user allows a client application to use (e.g. interactive access + to, or modification of, their data) + +Generally, and in terms of the OAuth ecosystem, all of these can be considered as +[scopes](https://tools.ietf.org/html/rfc6749#section-3.3). As discussed in the [Usage](#usage) section, scopes can be +used to control who and/or what can use features within protected applications. + +Scopes are included the access token generated by a client application (possibly interactively by a user) and presented +to the projected application as a bearer token. Azure encodes different mechanisms in different claims: + +* `roles` - for roles assigned to users and permissions directly assigned to client applications +* `scp` - for permissions delegated by the user to a client application + +For ease of use, this extension abstracts these two claims into a single set of `scopes` that can be required for a +given route. Multiple scopes can be required (as a logical AND) to allow scopes to be used more flexibly. + +#### Defining permissions and roles within an application + +Permissions and roles are defined in the +[application manifest](https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-app-manifest) of each +application being protected. They can then be [assigned](#assigning-permissions-and-roles-to-users-and-applications) to +users, groups and client applications. + +1. [register](#registering-an-application-in-azure) the application to be protected +2. [add permissions to application manifest](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps) + +For example: + +```json +"appRoles": [ + { + "allowedMemberTypes": [ + "Application" + ], + "displayName": "List all Foo resources", + "id": "112b3a76-2dd0-4d09-9976-9f94b2ed965d", + "isEnabled": true, + "description": "Allows access to basic information for all Foo resources", + "value": "Foo.List.All" + } +], +``` + +#### Assigning permissions and roles to users and applications + +Permissions and roles (collectively, application roles) are assigned through the Azure portal: + +1. [define roles and permissions in the protected application](#defining-permissions-and-roles-within-an-application) +2. [register](#registering-an-application-in-azure) the client application(s) +3. assign: + * [roles to users/groups](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps) + * [permissions to client applications](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#request-the-permissions-in-the-app-registration-portal) + +For assigning permissions: + +* permissions can be delegated to client applications, with the agreement of the current user +* permissions can be directly assigned to client applications, with the agreement of a tenancy administrator + +**Note:** Direct assignment is needed for non-interactive applications, such as daemons. + +#### Registering an application in Azure + +[Follow these instructions](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app). + +**Note:** These instructions apply both to applications that protected by this provider (protected applications), and +those that will be granted access to use such applications, possibly by a user (client applications). + +### Testing support + +For testing applications, a local/test JSON Web Key Set (JWKS) can be used to sign local/test JSON Web Tokens (JWTs) +without relying on Azure. Local tokens can include, or not include, arbitrary scopes/roles, which can ensure +requirements for specific scopes are properly enforced by this provider. + +This requires using local tokens signed by the test keys, and patching the `FlaskAzureOauth._get_jwks` method to +validate tokens using the same test keys. + +For example: + +```python +import unittest + +from http import HTTPStatus +from unittest.mock import patch + +from flask_azure_oauth import FlaskAzureOauth +from flask_azure_oauth.mocks.keys import TestJwk +from flask_azure_oauth.mocks.tokens import TestJwt + +from examples import create_app + + +class AppTestCase(unittest.TestCase): + def setUp(self): + self.test_jwks = TestJwk() + + with patch.object(FlaskAzureOauth, "_get_jwks") as mocked_get_jwks: + mocked_get_jwks.return_value = self.test_jwks.jwks() + + # `self.app` should be set to a Flask application, either by direct import, or by calling an app factory + self.app = create_app() + + self.app.config["TEST_JWKS"] = self.test_jwks + self.app_context = self.app.app_context() + self.app_context.push() + self.client = self.app.test_client() + + def test_protected_route_with_multiple_scopes_authorised(self): + # Generate token with required roles + token = TestJwt( + app=self.app, roles=["BAS.MAGIC.ADD.Records.Publish.All", "BAS.MAGIC.ADD.Records.ReadWrite.All"] + ) + + # Make request to protected route with token + response = self.client.get( + "/protected-with-multiple-scopes", headers={"authorization": f"bearer { token.dumps() }"} + ) + self.assertEqual(HTTPStatus.OK, response.status_code) + self.app_context.pop() + + def test_protected_route_with_multiple_scopes_unauthorised(self): + # Generate token with no scopes + token = TestJwt(app=self.app) + + # Make request to protected route with token + response = self.client.get( + "/protected-with-multiple-scopes", headers={"authorization": f"bearer { token.dumps() }"} + ) + self.assertEqual(HTTPStatus.FORBIDDEN, response.status_code) + self.app_context.pop() +``` + +## Developing + +This provider is developed as a Python library. A bundled Flask application is used to simulate its usage and act as +framework for running tests etc. + +### Development environment + +Git and [Poetry](https://python-poetry.org) are required to set up a local development environment of this project. + +**Note:** If you use [Pyenv](https://github.com/pyenv/pyenv), this project sets a local Python version for consistency. + +```shell +# clone from the BAS GitLab instance if possible +$ git clone https://gitlab.data.bas.ac.uk/web-apps/flask-extensions/flask-azure-oauth.git + +# alternatively, clone from the GitHub mirror +$ git clone https://github.com/antarctica/flask-azure-oauth.git + +# setup virtual environment +$ cd flask-azure-oauth +$ poetry install +``` + +### Code Style + +PEP-8 style and formatting guidelines must be used for this project, except the 80 character line limit. +[Black](https://github.com/psf/black) is used for formatting, configured in `pyproject.toml` and enforced as part of +[Python code linting](#code-linting). + +Black can be integrated with a range of editors, such as +[PyCharm](https://black.readthedocs.io/en/stable/integrations/editors.html#pycharm-intellij-idea), to apply formatting +automatically when saving files. + +To apply formatting manually: + +```shell +$ poetry run black src/ tests/ +``` + +### Code Linting + +[Flake8](https://flake8.pycqa.org) and various extensions are used to lint Python files. Specific checks, and any +configuration options, are documented in the `./.flake8` config file. + +To check files manually: + +```shell +$ poetry run flake8 src/ examples/ +``` + +Checks are run automatically in [Continuous Integration](#continuous-integration). + +### Dependencies + +Python dependencies for this project are managed with [Poetry](https://python-poetry.org) in `pyproject.toml`. + +Non-code files, such as static files, can also be included in the [Python package](#python-package) using the +`include` key in `pyproject.toml`. + +#### Adding new dependencies + +To add a new (development) dependency: + +```shell +$ poetry add [dependency] (--dev) +``` + +Then update the Docker image used for CI/CD builds and push to the BAS Docker Registry (which is provided by GitLab): + +```shell +$ docker build -f gitlab-ci.Dockerfile -t docker-registry.data.bas.ac.uk/web-apps/flask-extensions/flask-azure-oauth:latest . +$ docker push docker-registry.data.bas.ac.uk/web-apps/flask-extensions/flask-azure-oauth:latest +``` + +#### Updating dependencies + +```shell +$ poetry update +``` + +See the instructions above to update the Docker image used in CI/CD. + +#### Dependency vulnerability checks + +The [Safety](https://pypi.org/project/safety/) package is used to check dependencies against known vulnerabilities. + +**IMPORTANT!** As with all security tools, Safety is an aid for spotting common mistakes, not a guarantee of secure +code. In particular this is using the free vulnerability database, which is updated less frequently than paid options. + +This is a good tool for spotting low-hanging fruit in terms of vulnerabilities. It isn't a substitute for proper +vetting of dependencies, or a proper audit of potential issues by security professionals. If in any doubt you MUST seek +proper advice. + +Checks are run automatically in [Continuous Integration](#continuous-integration). + +To check locally: + +```shell +$ poetry export --without-hashes -f requirements.txt | poetry run safety check --full-report --stdin +``` + +#### `authlib` package + +The `authlib` dependency is locked to version `0.14.3` as the `0.15.x` release series contains a bug that prevents the +`kid` claim from being accessed from Jason Web Key (JWK) instances. This is a known issue and will be resolved in the +`1.x` release. See https://github.com/lepture/authlib/issues/314 for more information. + +### Static security scanning + +To ensure the security of this API, source code is checked against [Bandit](https://github.com/PyCQA/bandit) +and enforced as part of [Python code linting](#code-linting-python). + +**Warning:** Bandit is a static analysis tool and can't check for issues that are only be detectable when running the +application. As with all security tools, Bandit is an aid for spotting common mistakes, not a guarantee of secure code. + +To check manually: + +```shell +$ poetry run bandit -r src/ examples/ +``` + +**Note:** This package contains a number of testing methods that deliberately do insecure or nonsensical things. These +are necessary to test failure modes and error handling, they are not a risk when using this package as intended. These +workarounds have been exempted from these security checks where they apply. + +Checks are run automatically in [Continuous Integration](#continuous-integration). + +## Testing + +### Integration tests + +This project uses integration tests to ensure features work as expected and to guard against regressions and +vulnerabilities. + +The Python [UnitTest](https://docs.python.org/3/library/unittest.html) library is used for running tests using Flask's +test framework. Test cases are defined in files within `tests/` and are automatically loaded when using the `test` +Flask CLI command included in the local Flask application in the development environment. + +To run tests manually using PyCharm, use the included *App (tests)* run/debug configuration. + +To run tests manually: + +```shell +$ FLASK_APP=examples FLASK_ENV=testing poetry run python -m unittest discover +``` + +Tests are ran automatically in [Continuous Integration](#continuous-integration). + +### Continuous Integration + +All commits will trigger a Continuous Integration process using GitLab's CI/CD platform, configured in `.gitlab-ci.yml`. + +### Test/Example applications + +For verifying this provider works for real-world use-cases, a test Flask application is included in +`examples/__init__.py`. This test application acts as both an application providing access to, and accessing, protected +resources. It can use a number of application registrations registered in the BAS Web & Applications Test Azure AD. + +These applications allow testing different versions of access tokens for example. These applications are intended for +testing only. They do not represent real applications, or contain any sensitive or protected information. + +To test requesting resources from protected resources as an API, set the appropriate config options and run the +application container: + +```shell +$ FLASK_APP=examples poetry run flask +``` + +To test requesting resources from protected resources as a browser application, set the appropriate config options and +start the application container: + +```shell +$ FLASK_APP=examples poetry run flask run +``` + +Terraform is used to provision the application registrations used: + +``` +$ cd provisioning/terraform +$ docker-compose run terraform +$ az login --allow-no-subscriptions +$ terraform init +$ terraform validate +$ terraform apply +``` + +**Note:** Several properties in the application registration resources require setting once the registration has been +initially made (identifiers for example). These will need commenting out before use. + +Some properties, such as client secrets, can only be set once applications have been registered in the Azure Portal. + +Terraform state information is held in the BAS Terraform Remote State project (internal). + +## Deployment + +### Python package + +This project is distributed as a Python package, hosted in [PyPi](https://pypi.org/project/flask-azure-oauth). + +Source and binary packages are built and published automatically using +[Poetry](https://python-poetry.org) in [Continuous Deployment](#continuous-deployment). + +**Note:** Except for tagged releases, Python packages built in CD will use `0.0.0` as a version to indicate they are +not formal releases. + +### Continuous Deployment + +A Continuous Deployment process using GitLab's CI/CD platform is configured in `.gitlab-ci.yml`. + +## Release procedure + +For all releases: + +1. create a `release` branch +2. bump the version as appropriate in `pyproject.toml` +3. close release in `CHANGELOG.md` +4. push changes, merge the `release` branch into `main`, and tag with version + +The project will be built and published to PyPi automatically through [Continuous Deployment](#continuous-deployment). + +## Feedback + +The maintainer of this project is the BAS Web & Applications Team, they can be contacted at: +[servicedesk@bas.ac.uk](mailto:servicedesk@bas.ac.uk). + +## Issue tracking + +This project uses issue tracking, see the +[Issue tracker](https://gitlab.data.bas.ac.uk/web-apps/flask-extensions/flask-azure-oauth/issues) for more information. + +**Note:** Read & write access to this issue tracker is restricted. Contact the project maintainer to request access. + +## License + +Copyright (c) 2019-2022 UK Research and Innovation (UKRI), British Antarctic Survey. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +%prep +%autosetup -n flask-azure-oauth-0.7.0 + +%build +%py3_build + +%install +%py3_install +install -d -m755 %{buildroot}/%{_pkgdocdir} +if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi +if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi +if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi +if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi +pushd %{buildroot} +if [ -d usr/lib ]; then + find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/lib64 ]; then + find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/bin ]; then + find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/sbin ]; then + find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst +fi +touch doclist.lst +if [ -d usr/share/man ]; then + find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst +fi +popd +mv %{buildroot}/filelist.lst . +mv %{buildroot}/doclist.lst . + +%files -n python3-flask-azure-oauth -f filelist.lst +%dir %{python3_sitelib}/* + +%files help -f doclist.lst +%{_docdir}/* + +%changelog +* Fri May 05 2023 Python_Bot <Python_Bot@openeuler.org> - 0.7.0-1 +- Package Spec generated |
