Skip to content

Commit

Permalink
Implement a more generic get_profile method on clients
Browse files Browse the repository at this point in the history
  • Loading branch information
frankie567 committed Dec 6, 2024
1 parent f33e92e commit cd34774
Show file tree
Hide file tree
Showing 16 changed files with 294 additions and 118 deletions.
10 changes: 8 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,15 @@ access_token = await client.refresh_token("REFRESH_TOKEN")

For providers supporting it, you can ask to revoke an access or refresh token. For this, use the [revoke_token][httpx_oauth.oauth2.BaseOAuth2.revoke_token] method.

## Get authenticated user ID and email
## Get profile

For convenience, we provide a method that'll use a valid access token to query the provider API and get the ID and the email (if available) of the authenticated user. For this, use the [get_id_email][httpx_oauth.oauth2.BaseOAuth2.get_id_email] method.
For convenience, we provide a method that'll use a valid access token to query the provider API and get the profile of the authenticated user. For this, use the [get_profile][httpx_oauth.oauth2.BaseOAuth2.get_profile] method.

This method is implemented specifically on each provider. Please note it's a raw JSON output from the provider API, so it might vary greatly.

### Get authenticated user ID and email

Often, what you need is only the ID and the email. We offer another convenience method that'll do the heavy lifting of retrieving them from the profile output: the [get_id_email][httpx_oauth.oauth2.BaseOAuth2.get_id_email] method.

This method is implemented specifically on each provider.

Expand Down
24 changes: 15 additions & 9 deletions httpx_oauth/clients/discord.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, Optional, cast

from httpx_oauth.exceptions import GetIdEmailError
from httpx_oauth.exceptions import GetIdEmailError, GetProfileError
from httpx_oauth.oauth2 import BaseOAuth2

AUTHORIZE_ENDPOINT = "https://discord.com/api/oauth2/authorize"
Expand Down Expand Up @@ -52,22 +52,28 @@ def __init__(
revocation_endpoint_auth_method="client_secret_basic",
)

async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
async def get_profile(self, token: str) -> dict[str, Any]:
async with self.get_httpx_client() as client:
response = await client.get(
PROFILE_ENDPOINT,
headers={**self.request_headers, "Authorization": f"Bearer {token}"},
)

if response.status_code >= 400:
raise GetIdEmailError(response=response)
raise GetProfileError(response=response)

return cast(dict[str, Any], response.json())

data = cast(dict[str, Any], response.json())
async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
try:
profile = await self.get_profile(token)
except GetProfileError as e:
raise GetIdEmailError(response=e.response) from e

user_id = data["id"]
user_email = data.get("email")
user_id = profile["id"]
user_email = profile.get("email")

if not data.get("verified", False):
user_email = None
if not profile.get("verified", False):
user_email = None

return user_id, user_email
return user_id, user_email
15 changes: 10 additions & 5 deletions httpx_oauth/clients/facebook.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, Optional, cast

from httpx_oauth.exceptions import GetIdEmailError
from httpx_oauth.exceptions import GetIdEmailError, GetProfileError
from httpx_oauth.oauth2 import BaseOAuth2, OAuth2RequestError, OAuth2Token

AUTHORIZE_ENDPOINT = "https://www.facebook.com/v5.0/dialog/oauth"
Expand Down Expand Up @@ -89,16 +89,21 @@ async def get_long_lived_access_token(self, token: str) -> OAuth2Token:
data = self.get_json(response, exc_class=GetLongLivedAccessTokenError)
return OAuth2Token(data)

async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
async def get_profile(self, token: str) -> dict[str, Any]:
async with self.get_httpx_client() as client:
response = await client.get(
PROFILE_ENDPOINT,
params={"fields": "id,email", "access_token": token},
)

if response.status_code >= 400:
raise GetIdEmailError(response=response)
raise GetProfileError(response=response)

data = cast(dict[str, Any], response.json())
return cast(dict[str, Any], response.json())

return data["id"], data.get("email")
async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
try:
profile = await self.get_profile(token)
except GetProfileError as e:
raise GetIdEmailError(response=e.response) from e
return profile["id"], profile.get("email")
16 changes: 11 additions & 5 deletions httpx_oauth/clients/franceconnect.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import secrets
from typing import Any, Literal, Optional, TypedDict

from httpx_oauth.exceptions import GetIdEmailError
from httpx_oauth.exceptions import GetIdEmailError, GetProfileError
from httpx_oauth.oauth2 import BaseOAuth2

ENDPOINTS = {
Expand Down Expand Up @@ -72,16 +72,22 @@ async def get_authorization_url(
redirect_uri, state, scope, extras_params=_extras_params
)

async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
async def get_profile(self, token: str) -> dict[str, Any]:
async with self.get_httpx_client() as client:
response = await client.get(
self.profile_endpoint,
headers={**self.request_headers, "Authorization": f"Bearer {token}"},
)

if response.status_code >= 400:
raise GetIdEmailError(response=response)
raise GetProfileError(response=response)

return response.json()

data: dict[str, Any] = response.json()
async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
try:
profile = await self.get_profile(token)
except GetProfileError as e:
raise GetIdEmailError(response=e.response) from e

return str(data["sub"]), data.get("email")
return str(profile["sub"]), profile.get("email")
88 changes: 63 additions & 25 deletions httpx_oauth/clients/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import httpx

from httpx_oauth.exceptions import GetIdEmailError
from httpx_oauth.exceptions import GetIdEmailError, GetProfileError
from httpx_oauth.oauth2 import BaseOAuth2, OAuth2Token, RefreshTokenError

AUTHORIZE_ENDPOINT = "https://github.com/login/oauth/authorize"
Expand Down Expand Up @@ -106,10 +106,20 @@ async def refresh_token(self, refresh_token: str) -> OAuth2Token:

return OAuth2Token(data)

async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
async def get_profile(self, token: str) -> dict[str, Any]:
async with httpx.AsyncClient(
headers={**self.request_headers, "Authorization": f"token {token}"}
) as client:
response = await client.get(PROFILE_ENDPOINT)

if response.status_code >= 400:
raise GetProfileError(response=response)

return cast(dict[str, Any], response.json())

async def get_emails(self, token: str) -> list[dict[str, Any]]:
"""
Returns the id and the email (if available) of the authenticated user
from the API provider.
Return the emails of the authenticated user from the API provider.
!!! tip
You should enable **Email addresses** permission
Expand All @@ -120,43 +130,71 @@ async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
token: The access token.
Returns:
A tuple with the id and the email of the authenticated user.
A list of emails as described in the [GitHub API](https://docs.github.com/en/rest/users/emails?apiVersion=2022-11-28#list-email-addresses-for-the-authenticated-user).
Raises:
httpx_oauth.exceptions.GetIdEmailError:
An error occurred while getting the id and email.
httpx_oauth.exceptions.GetProfileError:
An error occurred while getting the emails.
Examples:
```py
user_id, user_email = await client.get_id_email("TOKEN")
emails = await client.get_emails("TOKEN")
```
"""
async with httpx.AsyncClient(
headers={**self.request_headers, "Authorization": f"token {token}"}
) as client:
response = await client.get(PROFILE_ENDPOINT)
response = await client.get(EMAILS_ENDPOINT)

if response.status_code >= 400:
raise GetIdEmailError(response=response)
raise GetProfileError(response=response)

return cast(list[dict[str, Any]], response.json())

async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
"""
Returns the id and the email (if available) of the authenticated user
from the API provider.
data = cast(dict[str, Any], response.json())
!!! tip
You should enable **Email addresses** permission
in the **Permissions & events** section of your GitHub app parameters.
You can find it at [https://github.com/settings/apps/{YOUR_APP}/permissions](https://github.com/settings/apps/{YOUR_APP}/permissions).
id = data["id"]
email = data.get("email")
Args:
token: The access token.
# No public email, make a separate call to /user/emails
if email is None:
response = await client.get(EMAILS_ENDPOINT)
Returns:
A tuple with the id and the email of the authenticated user.
if response.status_code >= 400:
raise GetIdEmailError(response=response)
emails = cast(list[dict[str, Any]], response.json())
Raises:
httpx_oauth.exceptions.GetIdEmailError:
An error occurred while getting the id and email.
# Use the primary email if it exists, otherwise the first
email = next(
(e["email"] for e in emails if e.get("primary")), emails[0]["email"]
)
Examples:
```py
user_id, user_email = await client.get_id_email("TOKEN")
```
"""
try:
profile = await self.get_profile(token)
except GetProfileError as e:
raise GetIdEmailError(response=e.response) from e

id = profile["id"]
email = profile.get("email")

# No public email, make a separate call to /user/emails
if email is None:
try:
emails = await self.get_emails(token)
except GetProfileError as e:
raise GetIdEmailError(response=e.response) from e

# Use the primary email if it exists, otherwise the first
email = next(
(e["email"] for e in emails if e.get("primary")), emails[0]["email"]
)

return str(id), email
return str(id), email
28 changes: 17 additions & 11 deletions httpx_oauth/clients/google.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, Literal, Optional, TypedDict, cast

from httpx_oauth.exceptions import GetIdEmailError
from httpx_oauth.exceptions import GetIdEmailError, GetProfileError
from httpx_oauth.oauth2 import BaseOAuth2

AUTHORIZE_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth"
Expand Down Expand Up @@ -65,7 +65,7 @@ def __init__(
revocation_endpoint_auth_method="client_secret_post",
)

async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
async def get_profile(self, token: str) -> dict[str, Any]:
async with self.get_httpx_client() as client:
response = await client.get(
PROFILE_ENDPOINT,
Expand All @@ -74,15 +74,21 @@ async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
)

if response.status_code >= 400:
raise GetIdEmailError(response=response)
raise GetProfileError(response=response)

data = cast(dict[str, Any], response.json())
return cast(dict[str, Any], response.json())

user_id = data["resourceName"]
user_email = next(
email["value"]
for email in data["emailAddresses"]
if email["metadata"]["primary"]
)
async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
try:
profile = await self.get_profile(token)
except GetProfileError as e:
raise GetIdEmailError(response=e.response) from e

user_id = profile["resourceName"]
user_email = next(
email["value"]
for email in profile["emailAddresses"]
if email["metadata"]["primary"]
)

return user_id, user_email
return user_id, user_email
21 changes: 14 additions & 7 deletions httpx_oauth/clients/kakao.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
from typing import Any, Optional, cast

from httpx_oauth.exceptions import GetIdEmailError
from httpx_oauth.exceptions import GetIdEmailError, GetProfileError
from httpx_oauth.oauth2 import BaseOAuth2

AUTHORIZE_ENDPOINT = "https://kauth.kakao.com/oauth/authorize"
Expand Down Expand Up @@ -52,7 +52,7 @@ def __init__(
revocation_endpoint_auth_method="client_secret_post",
)

async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
async def get_profile(self, token: str) -> dict[str, Any]:
async with self.get_httpx_client() as client:
response = await client.post(
PROFILE_ENDPOINT,
Expand All @@ -61,9 +61,16 @@ async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
)

if response.status_code >= 400:
raise GetIdEmailError(response=response)
raise GetProfileError(response=response)

return cast(dict[str, Any], response.json())

async def get_id_email(self, token: str) -> tuple[str, Optional[str]]:
try:
profile = await self.get_profile(token)
except GetProfileError as e:
raise GetIdEmailError(response=e.response) from e

payload = cast(dict[str, Any], response.json())
account_id = str(payload["id"])
email = payload["kakao_account"].get("email")
return account_id, email
account_id = str(profile["id"])
email = profile["kakao_account"].get("email")
return account_id, email
Loading

0 comments on commit cd34774

Please sign in to comment.