Skip to content

Commit 301036b

Browse files
committed
chore: merge with master, refactor for 2.0.0 release
1 parent 8ad3001 commit 301036b

3 files changed

Lines changed: 116 additions & 37 deletions

File tree

aioauth/response_type.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ class ResponseTypeBase(Generic[UserType]):
3838
def __init__(self, storage: BaseStorage[UserType]):
3939
self.storage = storage
4040

41-
async def validate_request(self, request: Request[UserType]) -> Client[UserType]:
41+
async def validate_request(
42+
self, request: Request[UserType], skip_user: bool = False
43+
) -> Client[UserType]:
4244
state = request.query.state
4345

4446
code_challenge_methods: Tuple[CodeChallengeMethod, ...] = get_args(
@@ -90,7 +92,7 @@ async def validate_request(self, request: Request[UserType]) -> Client[UserType]
9092
if not client.check_scope(request.query.scope):
9193
raise InvalidScopeError[UserType](request=request, state=state)
9294

93-
if not request.user:
95+
if not skip_user and not request.user:
9496
raise InvalidClientError[UserType](
9597
request=request, description="User is not authorized", state=state
9698
)
@@ -156,8 +158,10 @@ async def create_authorization_response(
156158

157159

158160
class ResponseTypeIdToken(ResponseTypeBase[UserType]):
159-
async def validate_request(self, request: Request[UserType]) -> Client[UserType]:
160-
client = await super().validate_request(request)
161+
async def validate_request(
162+
self, request: Request[UserType], skip_user: bool = False
163+
) -> Client[UserType]:
164+
client = await super().validate_request(request, skip_user)
161165

162166
# nonce is required for id_token
163167
if not request.query.nonce:

aioauth/server.py

Lines changed: 96 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
----
1818
"""
1919

20-
from dataclasses import asdict
20+
from dataclasses import asdict, dataclass
2121
from http import HTTPStatus
2222
from typing import Any, Dict, Generic, List, Optional, Tuple, Type, Union, get_args
2323

24+
from .models import Client
2425
from .requests import Request
2526
from .types import UserType
2627
from .storage import BaseStorage
@@ -71,6 +72,20 @@
7172
)
7273

7374

75+
@dataclass
76+
class AuthorizationState(Generic[UserType]):
77+
"""AuthorizationServer state object used in Authorization Code process."""
78+
79+
request: Request[UserType]
80+
"""OAuth2.0 Authorization Code Request Object"""
81+
82+
response_type_list: List[ResponseType]
83+
"""Supported ResponseTypes Collected During Initial Request Validation"""
84+
85+
grants: List[Tuple[ResponseTypeAuthorizationCode[UserType], Client]]
86+
"""Collection of Supported GrantType Handlers and The Parsed Clients"""
87+
88+
7489
class AuthorizationServer(Generic[UserType]):
7590
"""Interface for initializing an OAuth 2.0 server."""
7691

@@ -341,13 +356,14 @@ async def token(request: fastapi.Request) -> fastapi.Response:
341356
InvalidRedirectURIError,
342357
)
343358
)
344-
async def create_authorization_response(
359+
async def validate_authorization_request(
345360
self, request: Request[UserType]
346-
) -> Response:
361+
) -> Union[Response, AuthorizationState]:
347362
"""
348363
Endpoint to interact with the resource owner and obtain an
349-
authorization grant.
350-
Validate authorization request and create authorization response.
364+
authoriation grant.
365+
Validate authorization request and return valid authorization
366+
state for later response generation.
351367
For more information see
352368
`RFC6749 section 4.1.1 <https://tools.ietf.org/html/rfc6749#section-4.1.1>`_.
353369
@@ -365,8 +381,10 @@ async def create_authorization_response(
365381
async def authorize(request: fastapi.Request) -> fastapi.Response:
366382
# Converts a fastapi.Request to an aioauth.Request.
367383
oauth2_request: aioauth.Request = await to_oauth2_request(request)
384+
# Validate the oauth request
385+
auth_state: aioauth.AuthState = await server.validate_authorization_request(oauth2_request)
368386
# Creates the response via this function call.
369-
oauth2_response: aioauth.Response = await server.create_authorization_response(oauth2_request)
387+
oauth2_response: aioauth.Response = await server.create_authorization_response(auth_state)
370388
# Converts an aioauth.Response to a fastapi.Response.
371389
response: fastapi.Response = await to_fastapi_response(oauth2_response)
372390
return response
@@ -375,25 +393,12 @@ async def authorize(request: fastapi.Request) -> fastapi.Response:
375393
request: An :py:class:`aioauth.requests.Request` object.
376394
377395
Returns:
378-
response: An :py:class:`aioauth.responses.Response` object.
396+
state: An :py:class:`aioauth.server.AuthState` object.
379397
"""
380398
self.validate_request(request, ["GET", "POST"])
381399

382400
response_type_list = enforce_list(request.query.response_type)
383401
response_type_classes = set()
384-
385-
# Combined responses
386-
responses = {}
387-
388-
# URI fragment
389-
fragment = {}
390-
391-
# URI query params
392-
query = {}
393-
394-
# Response content
395-
content = {}
396-
397402
state = request.query.state
398403

399404
if not response_type_list:
@@ -403,9 +408,6 @@ async def authorize(request: fastapi.Request) -> fastapi.Response:
403408
state=state,
404409
)
405410

406-
if state:
407-
responses["state"] = state
408-
409411
for response_type in response_type_list:
410412
ResponseTypeClass = self.response_types.get(response_type)
411413
if ResponseTypeClass:
@@ -414,9 +416,79 @@ async def authorize(request: fastapi.Request) -> fastapi.Response:
414416
if not response_type_classes:
415417
raise UnsupportedResponseTypeError[UserType](request=request, state=state)
416418

419+
auth_state = AuthorizationState(request, response_type_list, grants=[])
417420
for ResponseTypeClass in response_type_classes:
418421
response_type = ResponseTypeClass(storage=self.storage)
419-
client = await response_type.validate_request(request)
422+
client = await response_type.validate_request(request, skip_user=True)
423+
auth_state.grants.append((response_type, client))
424+
return auth_state
425+
426+
@catch_errors_and_unavailability(
427+
skip_redirect_on_exc=(
428+
MethodNotAllowedError,
429+
InvalidClientError,
430+
InvalidRedirectURIError,
431+
)
432+
)
433+
async def create_authorization_response(
434+
self,
435+
auth_state: AuthorizationState[UserType],
436+
) -> Response:
437+
"""
438+
Endpoint to interact with the resource owner and obtain an
439+
authorization grant.
440+
Create an authorization response after validation.
441+
For more information see
442+
`RFC6749 section 4.1.1 <https://tools.ietf.org/html/rfc6749#section-4.1.1>`_.
443+
444+
Example:
445+
Below is an example utilizing FastAPI as the server framework.
446+
.. code-block:: python
447+
448+
from aioauth.fastapi.utils import to_oauth2_request, to_fastapi_response
449+
450+
@app.post("/authorize")
451+
async def authorize(request: fastapi.Request) -> fastapi.Response:
452+
# Converts a fastapi.Request to an aioauth.Request.
453+
oauth2_request: aioauth.Request = await to_oauth2_request(request)
454+
# Validate the oauth request
455+
auth_state: aioauth.AuthState = await server.validate_authorization_request(oauth2_request)
456+
# Creates the response via this function call.
457+
oauth2_response: aioauth.Response = await server.create_authorization_response(auth_state)
458+
# Converts an aioauth.Response to a fastapi.Response.
459+
response: fastapi.Response = await to_fastapi_response(oauth2_response)
460+
return response
461+
462+
Args:
463+
auth_state: An :py:class:`aioauth.server.AuthState` object.
464+
465+
Returns:
466+
response: An :py:class:`aioauth.responses.Response` object.
467+
"""
468+
request = auth_state.request
469+
state = auth_state.request.query.state
470+
response_type_list = auth_state.response_type_list
471+
if request.user:
472+
raise InvalidClientError[UserType](
473+
request=request, description="User is not authorized", state=state
474+
)
475+
476+
# Combined responses
477+
responses = {}
478+
479+
# URI fragment
480+
fragment = {}
481+
482+
# URI query params
483+
query = {}
484+
485+
# Response content
486+
content = {}
487+
488+
if state:
489+
responses["state"] = state
490+
491+
for response_type, client in auth_state.grants:
420492
response = await response_type.create_authorization_response(
421493
request, client
422494
)

examples/fastapi_example.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import json
88
import html
9-
from http import HTTPStatus
109
from typing import Optional, cast
1110

1211
from fastapi import FastAPI, Form, Request, Depends, Response
@@ -19,6 +18,7 @@
1918
from aioauth.requests import Post, Query
2019
from aioauth.requests import Request as OAuthRequest
2120
from aioauth.responses import Response as OAuthResponse
21+
from aioauth.server import AuthorizationState as OAuthState
2222
from aioauth.types import RequestMethod
2323
from aioauth.utils import build_error_response
2424

@@ -74,10 +74,13 @@ async def authorize(
7474
oauth2 authorization endpoint using aioauth
7575
"""
7676
oauthreq = await to_request(request)
77-
response = await oauth.create_authorization_response(oauthreq)
78-
if response.status_code == HTTPStatus.UNAUTHORIZED:
79-
request.session["oauth"] = oauthreq
77+
auth_state = await oauth.validate_authorization_request(oauthreq)
78+
if isinstance(auth_state, OAuthResponse):
79+
return to_response(auth_state)
80+
if "user" not in request.session:
81+
request.session["oauth"] = auth_state
8082
return RedirectResponse("/login")
83+
response = await oauth.create_authorization_response(auth_state)
8184
return to_response(response)
8285

8386

@@ -155,11 +158,11 @@ async def approve(request: Request):
155158
if "user" not in request.session:
156159
redirect = request.url_for("login")
157160
return RedirectResponse(redirect)
158-
oauthreq: OAuthRequest = request.session["oauth"]
161+
state: OAuthState = request.session["oauth"]
159162
content = f"""
160163
<html>
161164
<body>
162-
<h3>{oauthreq.query.client_id} would like permissions.</h3>
165+
<h3>{state.request.query.client_id} would like permissions.</h3>
163166
<form method="POST">
164167
<button name="approval" value="0" type="submit">Deny</button>
165168
<button name="approval" value="1" type="submit">Approve</button>
@@ -179,15 +182,15 @@ async def approve_submit(
179182
"""
180183
scope approval form submission handler
181184
"""
182-
oauthreq = request.session["oauth"]
183-
oauthreq.user = request.session["user"]
185+
state: OAuthState = request.session["oauth"]
186+
oauthreq: OAuthRequest = state.request
184187
if not approval:
185188
# generate error response on deny
186189
error = AccessDeniedError(oauthreq, "User rejected scopes")
187190
response = build_error_response(error, oauthreq, skip_redirect_on_exc=())
188191
else:
189192
# process authorize request
190-
response = await oauth.create_authorization_response(oauthreq)
193+
response = await oauth.create_authorization_response(state)
191194
return to_response(response)
192195

193196

0 commit comments

Comments
 (0)