Skip to content

Commit

Permalink
Merge pull request #76 from Colin-b/develop
Browse files Browse the repository at this point in the history
Release 7.0.0
  • Loading branch information
Colin-b authored Apr 27, 2023
2 parents 0b20c00 + 8822a9d commit 55ec3a5
Show file tree
Hide file tree
Showing 14 changed files with 1,076 additions and 90 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10']
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand All @@ -28,8 +28,8 @@ jobs:
pytest --cov=requests_auth --cov-fail-under=100 --cov-report=term-missing
- name: Create packages
run: |
python -m pip install wheel
python setup.py sdist bdist_wheel
python -m pip install build
python -m build .
- name: Publish packages
run: |
python -m pip install twine
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10']
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
repos:
- repo: https://github.com/psf/black
rev: 21.12b0
rev: 23.3.0
hooks:
- id: black
17 changes: 15 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [7.0.0] - 2023-04-27
### Changed
- `requests_auth.OAuth2ResourceOwnerPasswordCredentials` does not send basic authentication by default.

### Added
- `session_auth` as a parameter of `requests_auth.OAuth2ResourceOwnerPasswordCredentials`. Allowing to provide any kind of optional authentication.
- `requests_auth.OktaResourceOwnerPasswordCredentials` providing Okta resource owner password credentials flow easy setup.
- Explicit support for Python 3.11

### Removed
- Explicit support for Python 3.6

## [6.0.0] - 2022-01-11
### Changed
- `requests_auth.oauth2_tokens.TokenMemoryCache.get_token` method now requires arguments to be named.
Expand Down Expand Up @@ -167,7 +179,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Public release

[Unreleased]: https://github.com/Colin-b/requests_auth/compare/v6.0.0...HEAD
[Unreleased]: https://github.com/Colin-b/requests_auth/compare/v7.0.0...HEAD
[7.0.0]: https://github.com/Colin-b/requests_auth/compare/v6.0.0...v7.0.0
[6.0.0]: https://github.com/Colin-b/requests_auth/compare/v5.3.0...v6.0.0
[5.3.0]: https://github.com/Colin-b/requests_auth/compare/v5.2.0...v5.3.0
[5.2.0]: https://github.com/Colin-b/requests_auth/compare/v5.1.0...v5.2.0
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022 Colin Bounouar
Copyright (c) 2023 Colin Bounouar

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
45 changes: 43 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<a href="https://github.com/Colin-b/requests_auth/actions"><img alt="Build status" src="https://github.com/Colin-b/requests_auth/workflows/Release/badge.svg"></a>
<a href="https://github.com/Colin-b/requests_auth/actions"><img alt="Coverage" src="https://img.shields.io/badge/coverage-100%25-brightgreen"></a>
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
<a href="https://github.com/Colin-b/requests_auth/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-275 passed-blue"></a>
<a href="https://github.com/Colin-b/requests_auth/actions"><img alt="Number of tests" src="https://img.shields.io/badge/tests-305 passed-blue"></a>
<a href="https://pypi.org/project/requests-auth/"><img alt="Number of downloads" src="https://img.shields.io/pypi/dm/requests_auth"></a>
</p>

Expand Down Expand Up @@ -236,7 +236,7 @@ Usual extra parameters are:
| `client_secret` | If client is not authenticated with the authorization server |
| `nonce` | Refer to [OpenID ID Token specifications][3] for more details |

### Resource Owner Password Credentials flow
### Resource Owner Password Credentials flow

Resource Owner Password Credentials Grant is implemented following [rfc6749](https://tools.ietf.org/html/rfc6749#section-4.3).

Expand All @@ -256,6 +256,7 @@ requests.get('https://www.example.com', auth=OAuth2ResourceOwnerPasswordCredenti
| `token_url` | OAuth 2 token URL. | Mandatory | |
| `username` | Resource owner user name. | Mandatory | |
| `password` | Resource owner password. | Mandatory | |
| `session_auth` | Client authentication if the client type is confidential or the client was issued client credentials (or assigned other authentication requirements). Can be a tuple or any requests authentication class instance. | Optional | |
| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 |
| `header_name` | Name of the header field used to send token. | Optional | Authorization |
| `header_value` | Format used to send the token value. "{token}" must be present as it will be replaced by the actual token. | Optional | Bearer {token} |
Expand All @@ -266,6 +267,46 @@ requests.get('https://www.example.com', auth=OAuth2ResourceOwnerPasswordCredenti

Any other parameter will be put as body parameter in the token URL.

#### Common providers

Most of [OAuth2](https://oauth.net/2/) Resource Owner Password Credentials providers are supported.

If the one you are looking for is not yet supported, feel free to [ask for its implementation](https://github.com/Colin-b/requests_auth/issues/new).

##### Okta (OAuth2 Resource Owner Password Credentials)

[Okta Resource Owner Password Credentials](https://developer.okta.com/docs/guides/implement-grant-type/ropassword/main/) providing access tokens is supported.

Use `requests_auth.OktaResourceOwnerPasswordCredentials` to configure this kind of authentication.

```python
import requests
from requests_auth import OktaResourceOwnerPasswordCredentials


okta = OktaResourceOwnerPasswordCredentials(instance='testserver.okta-emea.com', username='user name', password='user password', client_id='54239d18-c68c-4c47-8bdd-ce71ea1d50cd', client_secret="0c5MB")
requests.get('https://www.example.com', auth=okta)
```

###### Parameters

| Name | Description | Mandatory | Default value |
|:------------------------|:---------------------------|:----------|:--------------|
| `instance` | Okta instance (like "testserver.okta-emea.com"). | Mandatory | |
| `username` | Resource owner user name. | Mandatory | |
| `password` | Resource owner password. | Mandatory | |
| `client_id` | Okta Application Identifier (formatted as an Universal Unique Identifier). | Mandatory | |
| `client_secret` | Resource owner password. | Mandatory | |
| `timeout` | Maximum amount of seconds to wait for a token to be received once requested. | Optional | 60 |
| `header_name` | Name of the header field used to send token. | Optional | Authorization |
| `header_value` | Format used to send the token value. "{token}" must be present as it will be replaced by the actual token. | Optional | Bearer {token} |
| `scope` | Scope parameter sent in query. Can also be a list of scopes. | Optional | openid |
| `token_field_name` | Field name containing the token. | Optional | access_token |
| `early_expiry` | Number of seconds before actual token expiry where token will be considered as expired. Used to ensure token will not expire between the time of retrieval and the time the request reaches the actual server. Set it to 0 to deactivate this feature and use the same token until actual expiry. | Optional | 30.0 |
| `session` | `requests.Session` instance that will be used to request the token. Use it to provide a custom proxying rule for instance. | Optional | |

Any other parameter will be put as body parameters in the token URL.

### Client Credentials flow

Client Credentials Grant is implemented following [rfc6749](https://tools.ietf.org/html/rfc6749#section-4.4).
Expand Down
66 changes: 66 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
[build-system]
requires = ["setuptools", "setuptools_scm"]
build-backend = "setuptools.build_meta"

[project]
name = "requests_auth"
description = "Authentication for Requests"
readme = "README.md"
requires-python = ">=3.7"
license = {file = "LICENSE"}
authors = [
{name = "Colin Bounouar", email = "[email protected]" }
]
maintainers = [
{name = "Colin Bounouar", email = "[email protected]" }
]
keywords = [
"authentication",
"ntlm",
"oauth2",
"azure-active-directory",
"azure-ad",
"okta",
"apikey",
"multiple",
]
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Software Development :: Build Tools",
]
dependencies = [
"requests==2.*",
]
dynamic = ["version"]

[project.urls]
documentation = "https://colin-b.github.io/requests_auth/"
repository = "https://github.com/Colin-b/requests_auth"
changelog = "https://github.com/Colin-b/requests_auth/blob/master/CHANGELOG.md"
issues = "https://github.com/Colin-b/requests_auth/issues"

[project.optional-dependencies]
testing = [
# Used to generate test tokens
"pyjwt==2.*",
# Used to mock requests
"pytest-responses==0.5.*",
# Used to check coverage
"pytest-cov==4.*",
]

[tool.setuptools.packages.find]
exclude = ["tests*"]

[tool.setuptools.dynamic]
version = {attr = "requests_auth.version.__version__"}
1 change: 1 addition & 0 deletions requests_auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
OAuth2ClientCredentials,
OktaClientCredentials,
OAuth2ResourceOwnerPasswordCredentials,
OktaResourceOwnerPasswordCredentials,
)
from requests_auth.oauth2_tokens import JsonTokenFileCache
from requests_auth.errors import (
Expand Down
65 changes: 64 additions & 1 deletion requests_auth/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs):
:param token_url: OAuth 2 token URL.
:param username: Resource owner user name.
:param password: Resource owner password.
:param session_auth: Client authentication if the client type is confidential
or the client was issued client credentials (or assigned other authentication requirements).
Can be a tuple or any requests authentication class instance.
:param timeout: Maximum amount of seconds to wait for a token to be received once requested.
Wait for 1 minute by default.
:param header_name: Name of the header field used to send token.
Expand Down Expand Up @@ -177,7 +180,9 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs):
# Time is expressed in seconds
self.timeout = int(kwargs.pop("timeout", None) or 60)
self.session = kwargs.pop("session", None) or requests.Session()
self.session.auth = (self.username, self.password)
session_auth = kwargs.pop("session_auth", None)
if session_auth:
self.session.auth = session_auth

# As described in https://tools.ietf.org/html/rfc6749#section-4.3.2
self.data = {
Expand Down Expand Up @@ -1187,6 +1192,64 @@ def __init__(self, instance: str, client_id: str, client_secret: str, **kwargs):
)


class OktaResourceOwnerPasswordCredentials(OAuth2ResourceOwnerPasswordCredentials):
"""
Describes an Okta (OAuth 2) resource owner password credentials (also called password) flow requests authentication.
"""

def __init__(
self,
instance: str,
username: str,
password: str,
client_id: str,
client_secret: str,
**kwargs,
):
"""
:param instance: Okta instance (like "testserver.okta-emea.com")
:param username: Resource owner user name.
:param password: Resource owner password.
:param client_id: Okta Application Identifier (formatted as an Universal Unique Identifier)
:param client_secret: Resource owner password.
:param authorization_server: Okta authorization server
default by default.
:param timeout: Maximum amount of seconds to wait for a token to be received once requested.
Wait for 1 minute by default.
:param header_name: Name of the header field used to send token.
Token will be sent in Authorization header field by default.
:param header_value: Format used to send the token value.
"{token}" must be present as it will be replaced by the actual token.
Token will be sent as "Bearer {token}" by default.
:param scope: Scope parameter sent to token URL as body. Can also be a list of scopes.
Request 'openid' by default.
:param token_field_name: Field name containing the token. access_token by default.
:param early_expiry: Number of seconds before actual token expiry where token will be considered as expired.
Default to 30 seconds to ensure token will not expire between the time of retrieval and the time the request
reaches the actual server. Set it to 0 to deactivate this feature and use the same token until actual expiry.
:param session: requests.Session instance that will be used to request the token.
Use it to provide a custom proxying rule for instance.
:param kwargs: all additional authorization parameters that should be put as body parameters in the token URL.
"""
if not instance:
raise Exception("Instance is mandatory.")
if not client_id:
raise Exception("Client ID is mandatory.")
if not client_secret:
raise Exception("Client secret is mandatory.")
authorization_server = kwargs.pop("authorization_server", None) or "default"
scopes = kwargs.pop("scope", "openid")
kwargs["scope"] = " ".join(scopes) if isinstance(scopes, list) else scopes
OAuth2ResourceOwnerPasswordCredentials.__init__(
self,
f"https://{instance}/oauth2/{authorization_server}/v1/token",
username=username,
password=password,
session_auth=(client_id, client_secret),
**kwargs,
)


class HeaderApiKey(requests.auth.AuthBase, SupportMultiAuth):
"""Describes an API Key requests authentication."""

Expand Down
12 changes: 6 additions & 6 deletions requests_auth/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@


class AuthenticationFailed(Exception):
""" User was not authenticated. """
"""User was not authenticated."""

def __init__(self):
Exception.__init__(self, "User was not authenticated.")


class TimeoutOccurred(Exception):
""" No response within timeout interval. """
"""No response within timeout interval."""

def __init__(self, timeout: float):
Exception.__init__(
Expand All @@ -21,14 +21,14 @@ def __init__(self, timeout: float):


class InvalidToken(Exception):
""" Token is invalid. """
"""Token is invalid."""

def __init__(self, token_name: str):
Exception.__init__(self, f"{token_name} is invalid.")


class GrantNotProvided(Exception):
""" Grant was not provided. """
"""Grant was not provided."""

def __init__(self, grant_name: str, dictionary_without_grant: dict):
Exception.__init__(
Expand Down Expand Up @@ -115,7 +115,7 @@ def _pop(key: str) -> str:


class StateNotProvided(Exception):
""" State was not provided. """
"""State was not provided."""

def __init__(self, dictionary_without_state: dict):
Exception.__init__(
Expand All @@ -124,7 +124,7 @@ def __init__(self, dictionary_without_state: dict):


class TokenExpiryNotProvided(Exception):
""" Token expiry was not provided. """
"""Token expiry was not provided."""

def __init__(self, token_body: dict):
Exception.__init__(self, f"Expiry (exp) is not provided in {token_body}.")
2 changes: 1 addition & 1 deletion requests_auth/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
# Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0)
# Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0)
# Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9)
__version__ = "6.0.0"
__version__ = "7.0.0"
Loading

0 comments on commit 55ec3a5

Please sign in to comment.