Skip to content

Commit

Permalink
Merge pull request #13 from Colin-b/develop
Browse files Browse the repository at this point in the history
Release 0.5.0
  • Loading branch information
Colin-b authored Aug 19, 2020
2 parents caa6b26 + 6333771 commit 4774b1d
Show file tree
Hide file tree
Showing 13 changed files with 290 additions and 50 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.5.0] - 2020-08-19
### Added
- Allow to provide an `httpx.Client` instance for `*AuthorizationCode` flows (even `PKCE`), `*ClientCredentials` and `*ResourceOwnerPasswordCredentials` flows.

## [0.4.0] - 2020-08-07
### Changed
- Mock an access token by default in `httpx_auth.testing.token_cache_mock`. Getting rid of `pyjwt` default dependency for testing.
Expand Down Expand Up @@ -43,7 +47,8 @@ Note that a few changes were made:
### Added
- Placeholder for port of requests_auth to httpx

[Unreleased]: https://github.com/Colin-b/httpx_auth/compare/v0.4.0...HEAD
[Unreleased]: https://github.com/Colin-b/httpx_auth/compare/v0.5.0...HEAD
[0.5.0]: https://github.com/Colin-b/httpx_auth/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/Colin-b/httpx_auth/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/Colin-b/httpx_auth/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/Colin-b/httpx_auth/compare/v0.1.0...v0.2.0
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ with httpx.Client() as client:
| `code_field_name` | Field name containing the code. | Optional | code |
| `username` | User name in case basic authentication should be used to retrieve token. | Optional | |
| `password` | User password in case basic authentication should be used to retrieve token. | Optional | |
| `client` | `httpx.Client` 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 query parameter in the authorization URL and as body parameters in the token URL.

Expand Down Expand Up @@ -133,6 +134,7 @@ with httpx.Client() as client:
| `failure_display_time` | In case received token is not valid, this is the maximum amount of milliseconds the failure page will be displayed in your browser. | Optional | 5000 |
| `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} |
| `client` | `httpx.Client` 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 query parameter in the authorization URL.

Expand Down Expand Up @@ -172,6 +174,7 @@ with httpx.Client() as client:
| `response_type` | Value of the response_type query parameter if not already provided in authorization URL. | Optional | code |
| `token_field_name` | Field name containing the token. | Optional | access_token |
| `code_field_name` | Field name containing the code. | Optional | code |
| `client` | `httpx.Client` 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 query parameter in the authorization URL and as body parameters in the token URL.

Expand Down Expand Up @@ -224,6 +227,7 @@ with httpx.Client() as client:
| `failure_display_time` | In case received token is not valid, this is the maximum amount of milliseconds the failure page will be displayed in your browser. | Optional | 5000 |
| `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} |
| `client` | `httpx.Client` 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 query parameter in the authorization URL and as body parameters in the token URL.

Expand Down Expand Up @@ -260,6 +264,7 @@ with httpx.Client() as client:
| `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 to token URL as body. Can also be a list of scopes. | Optional | |
| `token_field_name` | Field name containing the token. | Optional | access_token |
| `client` | `httpx.Client` 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 parameter in the token URL.

Expand Down Expand Up @@ -289,6 +294,7 @@ with httpx.Client() as client:
| `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 to token URL as body. Can also be a list of scopes. | Optional | |
| `token_field_name` | Field name containing the token. | Optional | access_token |
| `client` | `httpx.Client` 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 parameter in the token URL.

Expand Down Expand Up @@ -327,6 +333,7 @@ with httpx.Client() as client:
| `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 |
| `client` | `httpx.Client` 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 query parameter in the token URL.

Expand Down
98 changes: 52 additions & 46 deletions httpx_auth/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,17 @@ def _get_query_parameter(url: str, param_name: str) -> Optional[str]:


def request_new_grant_with_post(
url: str, data, grant_name: str, timeout: float, auth=None
url: str, data, grant_name: str, client: httpx.Client
) -> (str, int):
response = httpx.post(url, data=data, timeout=timeout, auth=auth)
if response.is_error:
# As described in https://tools.ietf.org/html/rfc6749#section-5.2
raise InvalidGrantRequest(response)
with client:
response = client.post(url, data=data)

if response.is_error:
# As described in https://tools.ietf.org/html/rfc6749#section-5.2
raise InvalidGrantRequest(response)

content = response.json()

content = response.json()
token = content.get(grant_name)
if not token:
raise GrantNotProvided(grant_name, content)
Expand Down Expand Up @@ -146,6 +149,8 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs):
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. Not sent by default.
:param token_field_name: Field name containing the token. access_token by default.
:param client: httpx.Client 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.
"""
self.token_url = token_url
Expand All @@ -157,33 +162,30 @@ def __init__(self, token_url: str, username: str, password: str, **kwargs):
self.password = password
if not self.password:
raise Exception("Password is mandatory.")
self.kwargs = kwargs

extra_parameters = dict(kwargs)
self.header_name = extra_parameters.pop("header_name", None) or "Authorization"
self.header_value = (
extra_parameters.pop("header_value", None) or "Bearer {token}"
)
self.header_name = kwargs.pop("header_name", None) or "Authorization"
self.header_value = kwargs.pop("header_value", None) or "Bearer {token}"
if "{token}" not in self.header_value:
raise Exception("header_value parameter must contains {token}.")

self.token_field_name = (
extra_parameters.pop("token_field_name", None) or "access_token"
)
self.token_field_name = kwargs.pop("token_field_name", None) or "access_token"

# Time is expressed in seconds
self.timeout = int(extra_parameters.pop("timeout", None) or 60)
self.timeout = int(kwargs.pop("timeout", None) or 60)
self.client = kwargs.pop("client", None) or httpx.Client()
self.client.auth = (self.username, self.password)
self.client.timeout = self.timeout

# As described in https://tools.ietf.org/html/rfc6749#section-4.3.2
self.data = {
"grant_type": "password",
"username": self.username,
"password": self.password,
}
scope = extra_parameters.pop("scope", None)
scope = kwargs.pop("scope", None)
if scope:
self.data["scope"] = " ".join(scope) if isinstance(scope, list) else scope
self.data.update(extra_parameters)
self.data.update(kwargs)

all_parameters_in_url = _add_parameters(self.token_url, self.data)
self.state = sha512(all_parameters_in_url.encode("unicode_escape")).hexdigest()
Expand All @@ -198,11 +200,7 @@ def auth_flow(
def request_new_token(self):
# As described in https://tools.ietf.org/html/rfc6749#section-4.3.3
token, expires_in = request_new_grant_with_post(
self.token_url,
self.data,
self.token_field_name,
self.timeout,
auth=(self.username, self.password),
self.token_url, self.data, self.token_field_name, self.client
)
# Handle both Access and Bearer tokens
return (self.state, token, expires_in) if expires_in else (self.state, token)
Expand Down Expand Up @@ -230,6 +228,8 @@ def __init__(self, token_url: str, client_id: str, client_secret: str, **kwargs)
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. Not sent by default.
:param token_field_name: Field name containing the token. access_token by default.
:param client: httpx.Client 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 query parameter in the token URL.
"""
self.token_url = token_url
Expand All @@ -241,29 +241,27 @@ def __init__(self, token_url: str, client_id: str, client_secret: str, **kwargs)
self.client_secret = client_secret
if not self.client_secret:
raise Exception("client_secret is mandatory.")
self.kwargs = kwargs

extra_parameters = dict(kwargs)
self.header_name = extra_parameters.pop("header_name", None) or "Authorization"
self.header_value = (
extra_parameters.pop("header_value", None) or "Bearer {token}"
)
self.header_name = kwargs.pop("header_name", None) or "Authorization"
self.header_value = kwargs.pop("header_value", None) or "Bearer {token}"
if "{token}" not in self.header_value:
raise Exception("header_value parameter must contains {token}.")

self.token_field_name = (
extra_parameters.pop("token_field_name", None) or "access_token"
)
self.token_field_name = kwargs.pop("token_field_name", None) or "access_token"

# Time is expressed in seconds
self.timeout = int(extra_parameters.pop("timeout", None) or 60)
self.timeout = int(kwargs.pop("timeout", None) or 60)

self.client = kwargs.pop("client", None) or httpx.Client()
self.client.auth = (self.client_id, self.client_secret)
self.client.timeout = self.timeout

# As described in https://tools.ietf.org/html/rfc6749#section-4.4.2
self.data = {"grant_type": "client_credentials"}
scope = extra_parameters.pop("scope", None)
scope = kwargs.pop("scope", None)
if scope:
self.data["scope"] = " ".join(scope) if isinstance(scope, list) else scope
self.data.update(extra_parameters)
self.data.update(kwargs)

all_parameters_in_url = _add_parameters(self.token_url, self.data)
self.state = sha512(all_parameters_in_url.encode("unicode_escape")).hexdigest()
Expand All @@ -278,11 +276,7 @@ def auth_flow(
def request_new_token(self) -> tuple:
# As described in https://tools.ietf.org/html/rfc6749#section-4.4.3
token, expires_in = request_new_grant_with_post(
self.token_url,
self.data,
self.token_field_name,
self.timeout,
auth=(self.client_id, self.client_secret),
self.token_url, self.data, self.token_field_name, self.client
)
# Handle both Access and Bearer tokens
return (self.state, token, expires_in) if expires_in else (self.state, token)
Expand Down Expand Up @@ -327,6 +321,8 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
:param code_field_name: Field name containing the code. code by default.
:param username: User name in case basic authentication should be used to retrieve token.
:param password: User password in case basic authentication should be used to retrieve token.
:param client: httpx.Client 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 query parameter
in the authorization URL and as body parameters in the token URL.
Usual parameters are:
Expand Down Expand Up @@ -354,6 +350,9 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
username = kwargs.pop("username", None)
password = kwargs.pop("password", None)
self.auth = (username, password) if username and password else None
self.client = kwargs.pop("client", None) or httpx.Client()
self.client.auth = self.auth
self.client.timeout = self.timeout

# As described in https://tools.ietf.org/html/rfc6749#section-4.1.2
code_field_name = kwargs.pop("code_field_name", "code")
Expand Down Expand Up @@ -415,11 +414,7 @@ def request_new_token(self):
self.token_data["code"] = code
# As described in https://tools.ietf.org/html/rfc6749#section-4.1.4
token, expires_in = request_new_grant_with_post(
self.token_url,
self.token_data,
self.token_field_name,
self.timeout,
auth=self.auth,
self.token_url, self.token_data, self.token_field_name, self.client
)
# Handle both Access and Bearer tokens
return (self.state, token, expires_in) if expires_in else (self.state, token)
Expand Down Expand Up @@ -462,6 +457,8 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):
code by default.
:param token_field_name: Field name containing the token. access_token by default.
:param code_field_name: Field name containing the code. code by default.
:param client: httpx.Client 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 query parameter
in the authorization URL and as body parameters in the token URL.
Usual parameters are:
Expand All @@ -479,6 +476,9 @@ def __init__(self, authorization_url: str, token_url: str, **kwargs):

BrowserAuth.__init__(self, kwargs)

self.client = kwargs.pop("client", None) or httpx.Client()
self.client.timeout = self.timeout

self.header_name = kwargs.pop("header_name", None) or "Authorization"
self.header_value = kwargs.pop("header_value", None) or "Bearer {token}"
if "{token}" not in self.header_value:
Expand Down Expand Up @@ -560,7 +560,7 @@ def request_new_token(self) -> tuple:
self.token_data["code"] = code
# As described in https://tools.ietf.org/html/rfc6749#section-4.1.4
token, expires_in = request_new_grant_with_post(
self.token_url, self.token_data, self.token_field_name, self.timeout
self.token_url, self.token_data, self.token_field_name, self.client
)
# Handle both Access and Bearer tokens
return (self.state, token, expires_in) if expires_in else (self.state, token)
Expand Down Expand Up @@ -943,6 +943,8 @@ def __init__(self, instance: str, client_id: str, **kwargs):
: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 client: httpx.Client 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 query parameter
in the authorization URL.
Usual parameters are:
Expand Down Expand Up @@ -997,6 +999,8 @@ def __init__(self, instance: str, client_id: str, **kwargs):
: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 client: httpx.Client 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 query parameter
in the authorization URL and as body parameters in the token URL.
Usual parameters are:
Expand Down Expand Up @@ -1037,6 +1041,8 @@ def __init__(self, instance: str, client_id: str, client_secret: str, **kwargs):
: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 client: httpx.Client 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 query parameter in the token URL.
"""
authorization_server = kwargs.pop("authorization_server", None) or "default"
Expand Down
2 changes: 1 addition & 1 deletion httpx_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__ = "0.4.0"
__version__ = "0.5.0"
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
# Used to generate test tokens
"pyjwt==1.*",
# Used to mock httpx
"pytest_httpx==0.6.*",
"pytest_httpx==0.7.*",
# Used to check coverage
"pytest-cov==2.*",
]
Expand Down
2 changes: 1 addition & 1 deletion tests/auth_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# TODO Remove
def get_header(httpx_mock: HTTPXMock, auth: httpx.Auth) -> dict:
# Mock a dummy response
httpx_mock.add_response()
httpx_mock.add_response(url="http://authorized_only", method="GET")
# Send a request to this dummy URL with authentication
response = httpx.get("http://authorized_only", auth=auth)
# Return headers received on this dummy URL
Expand Down
Loading

0 comments on commit 4774b1d

Please sign in to comment.