From f457cb3c39276887d5ed22b7beca28ffe2ed54b2 Mon Sep 17 00:00:00 2001 From: Ju4tCode <42488585+yanyongyu@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:37:02 +0800 Subject: [PATCH] :sparkles: Feature: improve refresh token usage and docs (#128) --- README.md | 73 ++++++++++++++++++++++++++++++++--------- githubkit/auth/oauth.py | 65 ++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 769e25f6a..202aa2de1 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ githubkit supports **both pydantic v1 and v2**, but pydantic v2 is recommended. ## Quick Start -Here is some common use cases to help you get started quickly. For more detailed usage, please refer to the [Usage](#usage) section. +Here is some common use cases to help you get started quickly. The following examples are written in sync style, you can also use async style by using functions with `async_` prefix. For more detailed usage, please refer to the [Usage](#usage) section. > APIs are fully typed. Type hints in the following examples are just for reference only. @@ -103,6 +103,12 @@ data: dict = github.graphql("{ viewer { login } }") ### Develop an OAuth APP (GitHub APP) with web flow +OAuth web flow allows you to authenticate as a user and act on behalf of the user. + +Note that if you are developing a GitHub APP, you may opt-in / opt-out of the user-to-server token expiration feature. If you opt-in, the user-to-server token will expire after a certain period of time, and you need to use the refresh token to generate a new token. In this case, you need to do more work to handle the token refresh. See [GitHub Docs - Refreshing user access tokens](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens) for more information. + +If you are developing an OAuth APP or a GitHub APP without user-to-server token expiration: + ```python from githubkit.versions.latest.models import PublicUser, PrivateUser from githubkit import GitHub, OAuthAppAuthStrategy, OAuthTokenAuthStrategy @@ -114,35 +120,68 @@ github = GitHub(OAuthAppAuthStrategy("", "")) # one time usage user_github = github.with_auth(github.auth.as_web_user("")) -# or, store the user token in a database +# or, store the user token in a database for later use auth: OAuthTokenAuthStrategy = github.auth.as_web_user("").exchange_token(github) +# store the user token to database access_token = auth.token -refresh_token = auth.refresh_token + # restore the user token from database -# when using OAuth APP or GitHub APP without user-to-server token expiration user_github = github.with_auth( OAuthTokenAuthStrategy( "", "", token=access_token ) ) -# OR when using GitHub APP with user-to-server token expiration + +# now you can act as the user +resp = user_github.rest.users.get_authenticated() +user: PublicUser | PrivateUser = resp.parsed_data + +# you can get the user name and id now +username = user.login +user_id = user.id +``` + +If you are developing a GitHub APP with user-to-server token expiration: + +```python +from githubkit.versions.latest.models import PublicUser, PrivateUser +from githubkit import GitHub, OAuthAppAuthStrategy, OAuthTokenAuthStrategy + +github = GitHub(OAuthAppAuthStrategy("", "")) + +# redirect user to github oauth page and get the code from callback + +# one time usage +user_github = github.with_auth(github.auth.as_web_user("")) + +# or, store the user refresh token in a database for later use +auth: OAuthTokenAuthStrategy = github.auth.as_web_user("").exchange_token(github) +refresh_token = auth.refresh_token + +# restore the user refresh token from database + # you can use the refresh_token to generate a new token -user_github = github.with_auth( - OAuthTokenAuthStrategy( - "", "", refresh_token=refresh_token - ) +auth = OAuthTokenAuthStrategy( + "", "", refresh_token=refresh_token ) +# refresh the token manually if you want to store the new refresh token +# otherwise, the token will be refreshed automatically when you make a request +auth.refresh(github) +refresh_token = auth.refresh_token + +user_github = github.with_auth(auth) # now you can act as the user resp = user_github.rest.users.get_authenticated() user: PublicUser | PrivateUser = resp.parsed_data -# you can get the user login id now -login_id = user.login +# you can get the user name and id now +username = user.login +user_id = user.id ``` -### Develop an OAuth APP with device flow +### Develop an OAuth APP (GitHub APP) with device flow ```python from githubkit import GitHub, OAuthDeviceAuthStrategy, OAuthTokenAuthStrategy @@ -167,7 +206,7 @@ user_github = user_github.with_auth( ### Develop a GitHub APP -Authenticating as a repository installation to do something with the repository: +Authenticating as a installation by repository name: ```python from githubkit import GitHub, AppAuthStrategy @@ -184,11 +223,12 @@ installation_github = github.with_auth( github.auth.as_installation(repo_installation.id) ) -resp = installation_github.rest.issues.get("owner", "repo", 1) -issue: Issue = resp.parsed_data +# create a comment on an issue +resp = installation_github.rest.issues.create_comment("owner", "repo", 1, body="Hello") +issue: IssueComment = resp.parsed_data ``` -Authenticating as a user installation to do something on behalf of the user: +Authenticating as a installation by username: ```python from githubkit import GitHub, AppAuthStrategy @@ -205,6 +245,7 @@ installation_github = github.with_auth( github.auth.as_installation(user_installation.id) ) +# create a comment on an issue resp = installation_github.rest.issues.create_comment("owner", "repo", 1, body="Hello") issue: IssueComment = resp.parsed_data ``` diff --git a/githubkit/auth/oauth.py b/githubkit/auth/oauth.py index e0508fdb2..5bbeec402 100644 --- a/githubkit/auth/oauth.py +++ b/githubkit/auth/oauth.py @@ -1,4 +1,5 @@ from time import sleep +from typing_extensions import Self from dataclasses import field, dataclass from datetime import datetime, timezone, timedelta from typing import ( @@ -609,6 +610,62 @@ def __post_init__(self): "when both token and refresh_token are provided." ) + def refresh(self, github: "GitHubCore") -> Self: + """Refresh access token with refresh token in place and return self.""" + + if self.refresh_token is None: + raise AuthCredentialError("Refresh token is not provided.") + + flow = refresh_token( + github, self.client_id, self.client_secret, self.refresh_token + ) + with github: + with github.get_sync_client() as client: + refresh_request = next(flow) + while True: + response = client.send(refresh_request) + response.read() + try: + refresh_request = flow.send(response) + except StopIteration as e: + data = e.value + break + + result = _parse_token_exchange_response(data) + self.token = result["token"] + self.expire_time = result["expire_time"] + self.refresh_token = result["refresh_token"] + self.refresh_token_expire_time = result["refresh_token_expire_time"] + return self + + async def async_refresh(self, github: "GitHubCore") -> Self: + """Refresh access token with refresh token in place and return self.""" + + if self.refresh_token is None: + raise AuthCredentialError("Refresh token is not provided.") + + flow = refresh_token( + github, self.client_id, self.client_secret, self.refresh_token + ) + async with github: + async with github.get_async_client() as client: + refresh_request = next(flow) + while True: + response = await client.send(refresh_request) + await response.aread() + try: + refresh_request = flow.send(response) + except StopIteration as e: + data = e.value + break + + result = _parse_token_exchange_response(data) + self.token = result["token"] + self.expire_time = result["expire_time"] + self.refresh_token = result["refresh_token"] + self.refresh_token_expire_time = result["refresh_token_expire_time"] + return self + def get_auth_flow(self, github: "GitHubCore") -> httpx.Auth: return OAuthTokenAuth(github, self) @@ -655,6 +712,8 @@ def refresh_token_expire_time(self) -> Optional[datetime]: return self._token_auth.refresh_token_expire_time def exchange_token(self, github: "GitHubCore") -> OAuthTokenAuthStrategy: + """Exchange token using code and return the new token auth strategy.""" + if self._token_auth is not None: return self._token_auth @@ -681,6 +740,8 @@ def exchange_token(self, github: "GitHubCore") -> OAuthTokenAuthStrategy: async def async_exchange_token( self, github: "GitHubCore" ) -> OAuthTokenAuthStrategy: + """Exchange token using code and return the new token auth strategy.""" + if self._token_auth is not None: return self._token_auth @@ -756,6 +817,8 @@ def refresh_token_expire_time(self) -> Optional[datetime]: return self._token_auth.refresh_token_expire_time def exchange_token(self, github: "GitHubCore") -> OAuthTokenAuthStrategy: + """Exchange token using device code and return the new token auth strategy.""" + if self._token_auth is not None: return self._token_auth @@ -803,6 +866,8 @@ def exchange_token(self, github: "GitHubCore") -> OAuthTokenAuthStrategy: async def async_exchange_token( self, github: "GitHubCore" ) -> OAuthTokenAuthStrategy: + """Exchange token using device code and return the new token auth strategy.""" + if self._token_auth is not None: return self._token_auth