Skip to content

Commit f457cb3

Browse files
authored
✨ Feature: improve refresh token usage and docs (#128)
1 parent 68ea192 commit f457cb3

File tree

2 files changed

+122
-16
lines changed

2 files changed

+122
-16
lines changed

README.md

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ githubkit supports **both pydantic v1 and v2**, but pydantic v2 is recommended.
8181
8282
## Quick Start
8383

84-
Here is some common use cases to help you get started quickly. For more detailed usage, please refer to the [Usage](#usage) section.
84+
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.
8585

8686
> APIs are fully typed. Type hints in the following examples are just for reference only.
8787
@@ -103,6 +103,12 @@ data: dict = github.graphql("{ viewer { login } }")
103103

104104
### Develop an OAuth APP (GitHub APP) with web flow
105105

106+
OAuth web flow allows you to authenticate as a user and act on behalf of the user.
107+
108+
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.
109+
110+
If you are developing an OAuth APP or a GitHub APP without user-to-server token expiration:
111+
106112
```python
107113
from githubkit.versions.latest.models import PublicUser, PrivateUser
108114
from githubkit import GitHub, OAuthAppAuthStrategy, OAuthTokenAuthStrategy
@@ -114,35 +120,68 @@ github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))
114120
# one time usage
115121
user_github = github.with_auth(github.auth.as_web_user("<code>"))
116122

117-
# or, store the user token in a database
123+
# or, store the user token in a database for later use
118124
auth: OAuthTokenAuthStrategy = github.auth.as_web_user("<code>").exchange_token(github)
125+
# store the user token to database
119126
access_token = auth.token
120-
refresh_token = auth.refresh_token
127+
121128
# restore the user token from database
122129

123-
# when using OAuth APP or GitHub APP without user-to-server token expiration
124130
user_github = github.with_auth(
125131
OAuthTokenAuthStrategy(
126132
"<client_id>", "<client_secret>", token=access_token
127133
)
128134
)
129-
# OR when using GitHub APP with user-to-server token expiration
135+
136+
# now you can act as the user
137+
resp = user_github.rest.users.get_authenticated()
138+
user: PublicUser | PrivateUser = resp.parsed_data
139+
140+
# you can get the user name and id now
141+
username = user.login
142+
user_id = user.id
143+
```
144+
145+
If you are developing a GitHub APP with user-to-server token expiration:
146+
147+
```python
148+
from githubkit.versions.latest.models import PublicUser, PrivateUser
149+
from githubkit import GitHub, OAuthAppAuthStrategy, OAuthTokenAuthStrategy
150+
151+
github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))
152+
153+
# redirect user to github oauth page and get the code from callback
154+
155+
# one time usage
156+
user_github = github.with_auth(github.auth.as_web_user("<code>"))
157+
158+
# or, store the user refresh token in a database for later use
159+
auth: OAuthTokenAuthStrategy = github.auth.as_web_user("<code>").exchange_token(github)
160+
refresh_token = auth.refresh_token
161+
162+
# restore the user refresh token from database
163+
130164
# you can use the refresh_token to generate a new token
131-
user_github = github.with_auth(
132-
OAuthTokenAuthStrategy(
133-
"<client_id>", "<client_secret>", refresh_token=refresh_token
134-
)
165+
auth = OAuthTokenAuthStrategy(
166+
"<client_id>", "<client_secret>", refresh_token=refresh_token
135167
)
168+
# refresh the token manually if you want to store the new refresh token
169+
# otherwise, the token will be refreshed automatically when you make a request
170+
auth.refresh(github)
171+
refresh_token = auth.refresh_token
172+
173+
user_github = github.with_auth(auth)
136174

137175
# now you can act as the user
138176
resp = user_github.rest.users.get_authenticated()
139177
user: PublicUser | PrivateUser = resp.parsed_data
140178

141-
# you can get the user login id now
142-
login_id = user.login
179+
# you can get the user name and id now
180+
username = user.login
181+
user_id = user.id
143182
```
144183

145-
### Develop an OAuth APP with device flow
184+
### Develop an OAuth APP (GitHub APP) with device flow
146185

147186
```python
148187
from githubkit import GitHub, OAuthDeviceAuthStrategy, OAuthTokenAuthStrategy
@@ -167,7 +206,7 @@ user_github = user_github.with_auth(
167206

168207
### Develop a GitHub APP
169208

170-
Authenticating as a repository installation to do something with the repository:
209+
Authenticating as a installation by repository name:
171210

172211
```python
173212
from githubkit import GitHub, AppAuthStrategy
@@ -184,11 +223,12 @@ installation_github = github.with_auth(
184223
github.auth.as_installation(repo_installation.id)
185224
)
186225

187-
resp = installation_github.rest.issues.get("owner", "repo", 1)
188-
issue: Issue = resp.parsed_data
226+
# create a comment on an issue
227+
resp = installation_github.rest.issues.create_comment("owner", "repo", 1, body="Hello")
228+
issue: IssueComment = resp.parsed_data
189229
```
190230

191-
Authenticating as a user installation to do something on behalf of the user:
231+
Authenticating as a installation by username:
192232

193233
```python
194234
from githubkit import GitHub, AppAuthStrategy
@@ -205,6 +245,7 @@ installation_github = github.with_auth(
205245
github.auth.as_installation(user_installation.id)
206246
)
207247

248+
# create a comment on an issue
208249
resp = installation_github.rest.issues.create_comment("owner", "repo", 1, body="Hello")
209250
issue: IssueComment = resp.parsed_data
210251
```

githubkit/auth/oauth.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from time import sleep
2+
from typing_extensions import Self
23
from dataclasses import field, dataclass
34
from datetime import datetime, timezone, timedelta
45
from typing import (
@@ -609,6 +610,62 @@ def __post_init__(self):
609610
"when both token and refresh_token are provided."
610611
)
611612

613+
def refresh(self, github: "GitHubCore") -> Self:
614+
"""Refresh access token with refresh token in place and return self."""
615+
616+
if self.refresh_token is None:
617+
raise AuthCredentialError("Refresh token is not provided.")
618+
619+
flow = refresh_token(
620+
github, self.client_id, self.client_secret, self.refresh_token
621+
)
622+
with github:
623+
with github.get_sync_client() as client:
624+
refresh_request = next(flow)
625+
while True:
626+
response = client.send(refresh_request)
627+
response.read()
628+
try:
629+
refresh_request = flow.send(response)
630+
except StopIteration as e:
631+
data = e.value
632+
break
633+
634+
result = _parse_token_exchange_response(data)
635+
self.token = result["token"]
636+
self.expire_time = result["expire_time"]
637+
self.refresh_token = result["refresh_token"]
638+
self.refresh_token_expire_time = result["refresh_token_expire_time"]
639+
return self
640+
641+
async def async_refresh(self, github: "GitHubCore") -> Self:
642+
"""Refresh access token with refresh token in place and return self."""
643+
644+
if self.refresh_token is None:
645+
raise AuthCredentialError("Refresh token is not provided.")
646+
647+
flow = refresh_token(
648+
github, self.client_id, self.client_secret, self.refresh_token
649+
)
650+
async with github:
651+
async with github.get_async_client() as client:
652+
refresh_request = next(flow)
653+
while True:
654+
response = await client.send(refresh_request)
655+
await response.aread()
656+
try:
657+
refresh_request = flow.send(response)
658+
except StopIteration as e:
659+
data = e.value
660+
break
661+
662+
result = _parse_token_exchange_response(data)
663+
self.token = result["token"]
664+
self.expire_time = result["expire_time"]
665+
self.refresh_token = result["refresh_token"]
666+
self.refresh_token_expire_time = result["refresh_token_expire_time"]
667+
return self
668+
612669
def get_auth_flow(self, github: "GitHubCore") -> httpx.Auth:
613670
return OAuthTokenAuth(github, self)
614671

@@ -655,6 +712,8 @@ def refresh_token_expire_time(self) -> Optional[datetime]:
655712
return self._token_auth.refresh_token_expire_time
656713

657714
def exchange_token(self, github: "GitHubCore") -> OAuthTokenAuthStrategy:
715+
"""Exchange token using code and return the new token auth strategy."""
716+
658717
if self._token_auth is not None:
659718
return self._token_auth
660719

@@ -681,6 +740,8 @@ def exchange_token(self, github: "GitHubCore") -> OAuthTokenAuthStrategy:
681740
async def async_exchange_token(
682741
self, github: "GitHubCore"
683742
) -> OAuthTokenAuthStrategy:
743+
"""Exchange token using code and return the new token auth strategy."""
744+
684745
if self._token_auth is not None:
685746
return self._token_auth
686747

@@ -756,6 +817,8 @@ def refresh_token_expire_time(self) -> Optional[datetime]:
756817
return self._token_auth.refresh_token_expire_time
757818

758819
def exchange_token(self, github: "GitHubCore") -> OAuthTokenAuthStrategy:
820+
"""Exchange token using device code and return the new token auth strategy."""
821+
759822
if self._token_auth is not None:
760823
return self._token_auth
761824

@@ -803,6 +866,8 @@ def exchange_token(self, github: "GitHubCore") -> OAuthTokenAuthStrategy:
803866
async def async_exchange_token(
804867
self, github: "GitHubCore"
805868
) -> OAuthTokenAuthStrategy:
869+
"""Exchange token using device code and return the new token auth strategy."""
870+
806871
if self._token_auth is not None:
807872
return self._token_auth
808873

0 commit comments

Comments
 (0)