Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: improve refresh token usage and docs #128

Merged
merged 3 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
"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.")

Check warning on line 617 in githubkit/auth/oauth.py

View check run for this annotation

Codecov / codecov/patch

githubkit/auth/oauth.py#L616-L617

Added lines #L616 - L617 were not covered by tests

flow = refresh_token(

Check warning on line 619 in githubkit/auth/oauth.py

View check run for this annotation

Codecov / codecov/patch

githubkit/auth/oauth.py#L619

Added line #L619 was not covered by tests
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

Check warning on line 632 in githubkit/auth/oauth.py

View check run for this annotation

Codecov / codecov/patch

githubkit/auth/oauth.py#L622-L632

Added lines #L622 - L632 were not covered by tests

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

Check warning on line 639 in githubkit/auth/oauth.py

View check run for this annotation

Codecov / codecov/patch

githubkit/auth/oauth.py#L634-L639

Added lines #L634 - L639 were not covered by tests

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.")

Check warning on line 645 in githubkit/auth/oauth.py

View check run for this annotation

Codecov / codecov/patch

githubkit/auth/oauth.py#L644-L645

Added lines #L644 - L645 were not covered by tests

flow = refresh_token(

Check warning on line 647 in githubkit/auth/oauth.py

View check run for this annotation

Codecov / codecov/patch

githubkit/auth/oauth.py#L647

Added line #L647 was not covered by tests
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

Check warning on line 660 in githubkit/auth/oauth.py

View check run for this annotation

Codecov / codecov/patch

githubkit/auth/oauth.py#L650-L660

Added lines #L650 - L660 were not covered by tests

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

Check warning on line 667 in githubkit/auth/oauth.py

View check run for this annotation

Codecov / codecov/patch

githubkit/auth/oauth.py#L662-L667

Added lines #L662 - L667 were not covered by tests

def get_auth_flow(self, github: "GitHubCore") -> httpx.Auth:
return OAuthTokenAuth(github, self)

Expand Down Expand Up @@ -655,6 +712,8 @@
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 @@
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 @@
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 @@
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
Loading