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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 43 additions & 9 deletions authlib/integrations/base_client/async_app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging
import time

from authlib.common.security import generate_token

from authlib.common.urls import urlparse

from .errors import MissingRequestTokenError
Expand Down Expand Up @@ -90,28 +92,60 @@ async def request(self, method, url, token=None, **kwargs):
async with self._get_oauth_client(**metadata) as session:
return await _http_request(self, session, method, url, token, kwargs)

async def create_authorization_url(self, redirect_uri=None, **kwargs):
async def create_authorization_url(self, redirect_uri=None, authorize_url_params=None, **authorize_params):
"""Generate the authorization url and state for HTTP redirect.

:param redirect_uri: Callback or redirect URI for authorization.
:param kwargs: Extra parameters to include.
:param authorize_url_params: Extra parameters specifically for the authorize_url endpoint, regardless of method.
:param authorize_params: Extra parameters to include for authorization.
:return: dict
"""
metadata = await self.load_server_metadata()
if self.authorize_params:
authorize_params.update(self.authorize_params)

authorize_url_params = authorize_url_params or {}
if self.authorize_url_params:
authorize_url_params.update(self.authorize_url_params)

authorization_endpoint = self.authorize_url or metadata.get(
"authorization_endpoint"
)
if not authorization_endpoint:
raise RuntimeError('Missing "authorize_url" value')

if self.authorize_params:
kwargs.update(self.authorize_params)
par_endpoint = self.pushed_authorization_url or metadata.get(
"pushed_authorization_request_endpoint"
)
par_required = metadata.get("require_pushed_authorization_requests")
if par_required and not par_endpoint:
raise RuntimeError('Missing "pushed_authorization_url" value')
par_required = par_required or par_endpoint

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A detail, I would have called this var should_use_par instead.

This makes me think that this enforces the usage of PAR if available, and this is probably a good security practice. However, are there situations when one might want to avoid using PAR even if it is available on the server?


async with self._get_oauth_client(**metadata) as client:
client.redirect_uri = redirect_uri
return self._create_oauth2_authorization_url(
client, authorization_endpoint, **kwargs
)
if redirect_uri is not None:
client.redirect_uri = redirect_uri

if par_required:
rv, request_uri = await self._pushed_authorization_request(client, par_endpoint, **authorize_params)
url, _ = client.create_authorization_url(
authorization_endpoint,
request_uri=request_uri['request_uri'],
**authorize_url_params)
rv['url'] = url
return rv
else:
authorize_params.update(authorize_url_params)
return self._create_oauth2_authorization_url(client, authorization_endpoint, **authorize_params)

async def _pushed_authorization_request(self, client, par_endpoint, **kwargs):
rv = {}
params = self._get_authorization_params(client, **kwargs)
rv.update(params)
kwargs.update(params)
request_uri, state = await client.pushed_authorization(par_endpoint, **kwargs)
rv["state"] = state
return rv, request_uri

async def fetch_access_token(self, redirect_uri=None, **kwargs):
"""Fetch access token in the final step.
Expand Down Expand Up @@ -149,4 +183,4 @@ async def _http_request(ctx, session, method, url, token, kwargs):
raise MissingTokenError()

session.token = token
return await session.request(method, url, **kwargs)
return await session.request(method, url, **kwargs)
4 changes: 3 additions & 1 deletion authlib/integrations/base_client/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"refresh_token_params",
"authorize_url",
"authorize_params",
"authorize_url_params",
"pushed_authorization_url",
"api_base_url",
"client_kwargs",
"server_metadata_url",
Expand Down Expand Up @@ -136,4 +138,4 @@ def _config_client(config, kwargs, overwrite):
kwargs[k].update(v)
else:
kwargs[k] = v
return kwargs
return kwargs
71 changes: 56 additions & 15 deletions authlib/integrations/base_client/sync_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ def __init__(
access_token_params=None,
authorize_url=None,
authorize_params=None,
authorize_redirect_params=None,
pushed_authorization_url=None,
api_base_url=None,
client_kwargs=None,
server_metadata_url=None,
Expand All @@ -213,6 +215,8 @@ def __init__(
self.access_token_params = access_token_params
self.authorize_url = authorize_url
self.authorize_params = authorize_params
self.authorize_url_params = authorize_redirect_params
self.pushed_authorization_url = pushed_authorization_url
self.api_base_url = api_base_url
self.client_kwargs = client_kwargs or {}

Expand All @@ -237,6 +241,8 @@ def _get_oauth_client(self, **metadata):
client_kwargs["authorization_endpoint"] = self.authorize_url
if self.access_token_url:
client_kwargs["token_endpoint"] = self.access_token_url
if self.pushed_authorization_url:
client_kwargs["pushed_authorization_request_endpoint"] = self.pushed_authorization_url

session = self.client_cls(
client_id=self.client_id,
Expand Down Expand Up @@ -269,14 +275,13 @@ def _format_state_params(state_data, params):
return params

@staticmethod
def _create_oauth2_authorization_url(client, authorization_endpoint, **kwargs):
rv = {}
def _get_authorization_params(client, **kwargs):
params = {}
if client.code_challenge_method:
code_verifier = kwargs.get("code_verifier")
if not code_verifier:
code_verifier = generate_token(48)
kwargs["code_verifier"] = code_verifier
rv["code_verifier"] = code_verifier
params["code_verifier"] = code_verifier
log.debug(f"Using code_verifier: {code_verifier!r}")

scope = kwargs.get("scope", client.scope)
Expand All @@ -290,9 +295,15 @@ def _create_oauth2_authorization_url(client, authorization_endpoint, **kwargs):
nonce = kwargs.get("nonce")
if not nonce:
nonce = generate_token(20)
kwargs["nonce"] = nonce
rv["nonce"] = nonce
params["nonce"] = nonce
return params

@staticmethod
def _create_oauth2_authorization_url(client, authorization_endpoint, **kwargs):
rv = {}
params = OAuth2Base._get_authorization_params(client, **kwargs)
rv.update(params)
kwargs.update(params)
url, state = client.create_authorization_url(authorization_endpoint, **kwargs)
rv["url"] = url
rv["state"] = state
Expand Down Expand Up @@ -331,30 +342,60 @@ def load_server_metadata(self):
self.server_metadata.update(metadata)
return self.server_metadata

def create_authorization_url(self, redirect_uri=None, **kwargs):
def create_authorization_url(self, redirect_uri=None, authorize_url_params=None, **authorize_params):
"""Generate the authorization url and state for HTTP redirect.

:param redirect_uri: Callback or redirect URI for authorization.
:param kwargs: Extra parameters to include.
:param authorize_url_params: Extra parameters specifically for the authorize_url endpoint, regardless of method.
:param authorize_params: Extra parameters to include for authorization.
:return: dict
"""
metadata = self.load_server_metadata()
if self.authorize_params:
authorize_params.update(self.authorize_params)

authorize_url_params = authorize_url_params or {}
if self.authorize_url_params:
authorize_url_params.update(self.authorize_url_params)

authorization_endpoint = self.authorize_url or metadata.get(
"authorization_endpoint"
)

if not authorization_endpoint:
raise RuntimeError('Missing "authorize_url" value')

if self.authorize_params:
kwargs.update(self.authorize_params)
par_endpoint = self.pushed_authorization_url or metadata.get(
"pushed_authorization_request_endpoint"
)
par_required = metadata.get("require_pushed_authorization_requests")
if par_required and not par_endpoint:
raise RuntimeError('Missing "pushed_authorization_url" value')
par_required = par_required or par_endpoint

with self._get_oauth_client(**metadata) as client:
if redirect_uri is not None:
client.redirect_uri = redirect_uri
return self._create_oauth2_authorization_url(
client, authorization_endpoint, **kwargs
)

if par_required:
rv, request_uri = self._pushed_authorization_request(client, par_endpoint, **authorize_params)
url, _ = client.create_authorization_url(
authorization_endpoint,
request_uri=request_uri['request_uri'],
**authorize_url_params)
rv['url'] = url
return rv
else:
authorize_params.update(authorize_url_params)
return self._create_oauth2_authorization_url(client, authorization_endpoint, **authorize_params)

def _pushed_authorization_request(self, client, par_endpoint, **kwargs):
rv = {}
params = self._get_authorization_params(client, **kwargs)
rv.update(params)
kwargs.update(params)
request_uri, state = client.pushed_authorization(par_endpoint, **kwargs)
rv["state"] = state
return rv, request_uri

def fetch_access_token(self, redirect_uri=None, **kwargs):
"""Fetch access token in the final step.
Expand All @@ -374,4 +415,4 @@ def fetch_access_token(self, redirect_uri=None, **kwargs):
params.update(self.access_token_params)
params.update(kwargs)
token = client.fetch_token(token_endpoint, **params)
return token
return token
14 changes: 13 additions & 1 deletion authlib/integrations/httpx_client/oauth2_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,18 @@ async def _refresh_token(

return self.token

async def _pushed_authorization(
self, url, state, body="", headers=None, auth=None, **kwargs
):
resp = await self.post(
url, data=dict(url_decode(body)), headers=headers, auth=auth, **kwargs
)

for hook in self.compliance_hook["pushed_authorization_response"]:
resp = hook(resp)

return self.parse_response_request_uri(resp), state

def _http_post(
self, url, body=None, auth=USE_CLIENT_DEFAULT, headers=None, **kwargs
):
Expand Down Expand Up @@ -282,4 +294,4 @@ def stream(

auth = self.token_auth

return super().stream(method, url, auth=auth, **kwargs)
return super().stream(method, url, auth=auth, **kwargs)
7 changes: 4 additions & 3 deletions authlib/integrations/starlette_client/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,19 @@ async def save_authorize_data(self, request, **kwargs):
else:
raise RuntimeError("Missing state value")

async def authorize_redirect(self, request, redirect_uri=None, **kwargs):
async def authorize_redirect(self, request, redirect_uri=None, authorize_url_params=None, **kwargs):
"""Create a HTTP Redirect for Authorization Endpoint.

:param request: HTTP request instance from Starlette view.
:param redirect_uri: Callback or redirect URI for authorization.
:param kwargs: Extra parameters to include.
:param authorize_url_params: Extra parameters specifically for the authorize_url endpoint, regardless of method.
:param kwargs: Extra parameters to include for authorization.
:return: A HTTP redirect response.
"""
# Handle Starlette >= 0.26.0 where redirect_uri may now be a URL and not a string
if redirect_uri and isinstance(redirect_uri, URL):
redirect_uri = str(redirect_uri)
rv = await self.create_authorization_url(redirect_uri, **kwargs)
rv = await self.create_authorization_url(redirect_uri, authorize_url_params, **kwargs)
await self.save_authorize_data(request, redirect_uri=redirect_uri, **rv)
return RedirectResponse(rv["url"], status_code=302)

Expand Down
Loading
Loading