Skip to content

Commit

Permalink
✨ Feature: improve refresh token usage and docs (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
yanyongyu authored Sep 4, 2024
1 parent 68ea192 commit f457cb3
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 16 deletions.
73 changes: 57 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -114,35 +120,68 @@ github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))
# one time usage
user_github = github.with_auth(github.auth.as_web_user("<code>"))

# 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("<code>").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(
"<client_id>", "<client_secret>", 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("<client_id>", "<client_secret>"))

# 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("<code>"))

# or, store the user refresh token in a database for later use
auth: OAuthTokenAuthStrategy = github.auth.as_web_user("<code>").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(
"<client_id>", "<client_secret>", refresh_token=refresh_token
)
auth = OAuthTokenAuthStrategy(
"<client_id>", "<client_secret>", 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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
```
Expand Down
65 changes: 65 additions & 0 deletions githubkit/auth/oauth.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down

0 comments on commit f457cb3

Please sign in to comment.