Skip to content
Merged
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
12 changes: 8 additions & 4 deletions aioauth/response_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ class ResponseTypeBase(Generic[UserType]):
def __init__(self, storage: BaseStorage[UserType]):
self.storage = storage

async def validate_request(self, request: Request[UserType]) -> Client[UserType]:
async def validate_request(
self, request: Request[UserType], skip_user: bool = False
) -> Client[UserType]:
state = request.query.state

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

if not request.user:
if not skip_user and not request.user:
raise InvalidClientError[UserType](
request=request, description="User is not authorized", state=state
)
Expand Down Expand Up @@ -156,8 +158,10 @@ async def create_authorization_response(


class ResponseTypeIdToken(ResponseTypeBase[UserType]):
async def validate_request(self, request: Request[UserType]) -> Client[UserType]:
client = await super().validate_request(request)
async def validate_request(
self, request: Request[UserType], skip_user: bool = False
) -> Client[UserType]:
client = await super().validate_request(request, skip_user)

# nonce is required for id_token
if not request.query.nonce:
Expand Down
120 changes: 96 additions & 24 deletions aioauth/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
----
"""

from dataclasses import asdict
from dataclasses import asdict, dataclass
from http import HTTPStatus
from typing import Any, Dict, Generic, List, Optional, Tuple, Type, Union, get_args

from .models import Client
from .requests import Request
from .types import UserType
from .storage import BaseStorage
Expand Down Expand Up @@ -71,6 +72,20 @@
)


@dataclass
class AuthorizationState(Generic[UserType]):
"""AuthorizationServer state object used in Authorization Code process."""

request: Request[UserType]
"""OAuth2.0 Authorization Code Request Object"""

response_type_list: List[ResponseType]
"""Supported ResponseTypes Collected During Initial Request Validation"""

grants: List[Tuple[ResponseTypeAuthorizationCode[UserType], Client]]
"""Collection of Supported GrantType Handlers and The Parsed Clients"""


class AuthorizationServer(Generic[UserType]):
"""Interface for initializing an OAuth 2.0 server."""

Expand Down Expand Up @@ -341,13 +356,14 @@ async def token(request: fastapi.Request) -> fastapi.Response:
InvalidRedirectURIError,
)
)
async def create_authorization_response(
async def validate_authorization_request(
self, request: Request[UserType]
) -> Response:
) -> Union[Response, AuthorizationState]:
"""
Endpoint to interact with the resource owner and obtain an
authorization grant.
Validate authorization request and create authorization response.
authoriation grant.
Validate authorization request and return valid authorization
state for later response generation.
For more information see
`RFC6749 section 4.1.1 <https://tools.ietf.org/html/rfc6749#section-4.1.1>`_.

Expand All @@ -365,8 +381,10 @@ async def create_authorization_response(
async def authorize(request: fastapi.Request) -> fastapi.Response:
# Converts a fastapi.Request to an aioauth.Request.
oauth2_request: aioauth.Request = await to_oauth2_request(request)
# Validate the oauth request
auth_state: aioauth.AuthState = await server.validate_authorization_request(oauth2_request)
# Creates the response via this function call.
oauth2_response: aioauth.Response = await server.create_authorization_response(oauth2_request)
oauth2_response: aioauth.Response = await server.create_authorization_response(auth_state)
# Converts an aioauth.Response to a fastapi.Response.
response: fastapi.Response = await to_fastapi_response(oauth2_response)
return response
Expand All @@ -375,25 +393,12 @@ async def authorize(request: fastapi.Request) -> fastapi.Response:
request: An :py:class:`aioauth.requests.Request` object.

Returns:
response: An :py:class:`aioauth.responses.Response` object.
state: An :py:class:`aioauth.server.AuthState` object.
"""
self.validate_request(request, ["GET", "POST"])

response_type_list = enforce_list(request.query.response_type)
response_type_classes = set()

# Combined responses
responses = {}

# URI fragment
fragment = {}

# URI query params
query = {}

# Response content
content = {}

state = request.query.state

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

if state:
responses["state"] = state

for response_type in response_type_list:
ResponseTypeClass = self.response_types.get(response_type)
if ResponseTypeClass:
Expand All @@ -414,9 +416,79 @@ async def authorize(request: fastapi.Request) -> fastapi.Response:
if not response_type_classes:
raise UnsupportedResponseTypeError[UserType](request=request, state=state)

auth_state = AuthorizationState(request, response_type_list, grants=[])
for ResponseTypeClass in response_type_classes:
response_type = ResponseTypeClass(storage=self.storage)
client = await response_type.validate_request(request)
client = await response_type.validate_request(request, skip_user=True)
auth_state.grants.append((response_type, client))
return auth_state

@catch_errors_and_unavailability(
skip_redirect_on_exc=(
MethodNotAllowedError,
InvalidClientError,
InvalidRedirectURIError,
)
)
async def create_authorization_response(
self,
auth_state: AuthorizationState[UserType],
) -> Response:
"""
Endpoint to interact with the resource owner and obtain an
authorization grant.
Create an authorization response after validation.
For more information see
`RFC6749 section 4.1.1 <https://tools.ietf.org/html/rfc6749#section-4.1.1>`_.

Example:
Below is an example utilizing FastAPI as the server framework.
.. code-block:: python

from aioauth.fastapi.utils import to_oauth2_request, to_fastapi_response

@app.post("/authorize")
async def authorize(request: fastapi.Request) -> fastapi.Response:
# Converts a fastapi.Request to an aioauth.Request.
oauth2_request: aioauth.Request = await to_oauth2_request(request)
# Validate the oauth request
auth_state: aioauth.AuthState = await server.validate_authorization_request(oauth2_request)
# Creates the response via this function call.
oauth2_response: aioauth.Response = await server.create_authorization_response(auth_state)
# Converts an aioauth.Response to a fastapi.Response.
response: fastapi.Response = await to_fastapi_response(oauth2_response)
return response

Args:
auth_state: An :py:class:`aioauth.server.AuthState` object.

Returns:
response: An :py:class:`aioauth.responses.Response` object.
"""
request = auth_state.request
state = auth_state.request.query.state
response_type_list = auth_state.response_type_list
if request.user:
raise InvalidClientError[UserType](
request=request, description="User is not authorized", state=state
)

# Combined responses
responses = {}

# URI fragment
fragment = {}

# URI query params
query = {}

# Response content
content = {}

if state:
responses["state"] = state

for response_type, client in auth_state.grants:
response = await response_type.create_authorization_response(
request, client
)
Expand Down
21 changes: 12 additions & 9 deletions examples/fastapi_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import json
import html
from http import HTTPStatus
from typing import Optional, cast

from fastapi import FastAPI, Form, Request, Depends, Response
Expand All @@ -19,6 +18,7 @@
from aioauth.requests import Post, Query
from aioauth.requests import Request as OAuthRequest
from aioauth.responses import Response as OAuthResponse
from aioauth.server import AuthorizationState as OAuthState
from aioauth.types import RequestMethod
from aioauth.utils import build_error_response

Expand Down Expand Up @@ -74,10 +74,13 @@ async def authorize(
oauth2 authorization endpoint using aioauth
"""
oauthreq = await to_request(request)
response = await oauth.create_authorization_response(oauthreq)
if response.status_code == HTTPStatus.UNAUTHORIZED:
request.session["oauth"] = oauthreq
auth_state = await oauth.validate_authorization_request(oauthreq)
if isinstance(auth_state, OAuthResponse):
return to_response(auth_state)
if "user" not in request.session:
request.session["oauth"] = auth_state
return RedirectResponse("/login")
response = await oauth.create_authorization_response(auth_state)
return to_response(response)


Expand Down Expand Up @@ -155,11 +158,11 @@ async def approve(request: Request):
if "user" not in request.session:
redirect = request.url_for("login")
return RedirectResponse(redirect)
oauthreq: OAuthRequest = request.session["oauth"]
state: OAuthState = request.session["oauth"]
content = f"""
<html>
<body>
<h3>{oauthreq.query.client_id} would like permissions.</h3>
<h3>{state.request.query.client_id} would like permissions.</h3>
<form method="POST">
<button name="approval" value="0" type="submit">Deny</button>
<button name="approval" value="1" type="submit">Approve</button>
Expand All @@ -179,15 +182,15 @@ async def approve_submit(
"""
scope approval form submission handler
"""
oauthreq = request.session["oauth"]
oauthreq.user = request.session["user"]
state: OAuthState = request.session["oauth"]
oauthreq: OAuthRequest = state.request
if not approval:
# generate error response on deny
error = AccessDeniedError(oauthreq, "User rejected scopes")
response = build_error_response(error, oauthreq, skip_redirect_on_exc=())
else:
# process authorize request
response = await oauth.create_authorization_response(oauthreq)
response = await oauth.create_authorization_response(state)
return to_response(response)


Expand Down
Loading