-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
## Summary Fixes #3102 ### Time to review: __10 mins__ ## Changes proposed Added a client (and mock) for calling an OAuth token endpoint A lot of restructuring of test utils for clearer setup ## Context for reviewers https://developers.login.gov/oidc/token/ This gets the token from the OAuth server and parses the response. There is more work to do on this later as login.gov requires a special JWT to also be passed, but a basic version doesn't need that (our local mock doesn't care), so I'll follow-up on that later. This approach to setting up a client is following some patterns I've used before that worked well. Building a mock version alongside the real one helps with testing. ## Additional information Still nothing new visually, under the hood it is just one more big step remaining to process the token --------- Co-authored-by: nava-platform-bot <[email protected]>
- Loading branch information
1 parent
7a7b8a8
commit 08c13b7
Showing
17 changed files
with
365 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Empty file.
Empty file.
50 changes: 50 additions & 0 deletions
50
api/src/adapters/oauth/login_gov/login_gov_oauth_client.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
from typing import Any | ||
|
||
import requests | ||
|
||
from src.adapters.oauth.oauth_client import BaseOauthClient | ||
from src.adapters.oauth.oauth_client_models import OauthTokenRequest, OauthTokenResponse | ||
from src.auth.login_gov_jwt_auth import LoginGovConfig, get_config | ||
|
||
|
||
class LoginGovOauthClient(BaseOauthClient): | ||
|
||
def __init__(self, config: LoginGovConfig | None = None): | ||
if config is None: | ||
config = get_config() | ||
|
||
self.config = config | ||
self.session = self._build_session() | ||
|
||
def _build_session(self, session: requests.Session | None = None) -> requests.Session: | ||
"""Set things on the session that should be shared between all requests""" | ||
if not session: | ||
session = requests.Session() | ||
|
||
session.headers.update({"Content-Type": "application/x-www-form-urlencoded"}) | ||
|
||
return session | ||
|
||
def _request(self, method: str, full_url: str, **kwargs: Any) -> requests.Response: | ||
"""Utility method for making a request with our session""" | ||
|
||
# By default timeout after 5 seconds | ||
if "timeout" not in kwargs: | ||
kwargs["timeout"] = 5 | ||
|
||
return self.session.request(method, full_url, **kwargs) | ||
|
||
def get_token(self, request: OauthTokenRequest) -> OauthTokenResponse: | ||
"""Query the login.gov token endpoint""" | ||
|
||
body = { | ||
"code": request.code, | ||
"grant_type": request.grant_type, | ||
# TODO https://github.com/HHS/simpler-grants-gov/issues/3103 | ||
# when we support client assertion, we need to not add the client_id | ||
"client_id": self.config.client_id, | ||
} | ||
|
||
response = self._request("POST", self.config.login_gov_token_endpoint, data=body) | ||
|
||
return OauthTokenResponse.model_validate_json(response.text) |
21 changes: 21 additions & 0 deletions
21
api/src/adapters/oauth/login_gov/mock_login_gov_oauth_client.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from src.adapters.oauth.oauth_client import BaseOauthClient | ||
from src.adapters.oauth.oauth_client_models import OauthTokenRequest, OauthTokenResponse | ||
|
||
|
||
class MockLoginGovOauthClient(BaseOauthClient): | ||
|
||
def __init__(self) -> None: | ||
self.responses: dict[str, OauthTokenResponse] = {} | ||
|
||
def add_token_response(self, code: str, response: OauthTokenResponse) -> None: | ||
self.responses[code] = response | ||
|
||
def get_token(self, request: OauthTokenRequest) -> OauthTokenResponse: | ||
response = self.responses.get(request.code, None) | ||
|
||
if response is None: | ||
response = OauthTokenResponse( | ||
error="error", error_description="default mock error description" | ||
) | ||
|
||
return response |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import abc | ||
|
||
from src.adapters.oauth.oauth_client_models import OauthTokenRequest, OauthTokenResponse | ||
|
||
|
||
class BaseOauthClient(abc.ABC, metaclass=abc.ABCMeta): | ||
|
||
@abc.abstractmethod | ||
def get_token(self, request: OauthTokenRequest) -> OauthTokenResponse: | ||
"""Call the POST token endpoint | ||
See: https://developers.login.gov/oidc/token/ | ||
""" | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
from dataclasses import dataclass | ||
|
||
from pydantic import BaseModel | ||
|
||
|
||
@dataclass | ||
class OauthTokenRequest: | ||
"""https://developers.login.gov/oidc/token/#request-parameters""" | ||
|
||
code: str | ||
grant_type: str = "authorization_code" | ||
|
||
# TODO: https://github.com/HHS/simpler-grants-gov/issues/3103 | ||
# client_assertion: str | None = None | ||
# client_assertion_type: str = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer | ||
|
||
|
||
class OauthTokenResponse(BaseModel): | ||
"""https://developers.login.gov/oidc/token/#token-response""" | ||
|
||
# These fields are given defaults so we don't need None-checks | ||
# for them elsewhere, if the response didn't error, they have valid values | ||
id_token: str = "" | ||
access_token: str = "" | ||
token_type: str = "" | ||
expires_in: int = 0 | ||
|
||
# These fields are only set if the response errored | ||
error: str | None = None | ||
error_description: str | None = None | ||
|
||
def is_error_response(self) -> bool: | ||
return self.error is not None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.