reflex-google-auth 0.1.0__tar.gz → 0.2.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/.github/workflows/publish.yml +3 -3
  2. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/PKG-INFO +17 -2
  3. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/README.md +16 -1
  4. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/custom_components/reflex_google_auth/google_auth.py +18 -1
  5. reflex_google_auth-0.2.0a1/custom_components/reflex_google_auth/state.py +206 -0
  6. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/custom_components/reflex_google_auth.egg-info/PKG-INFO +17 -2
  7. reflex_google_auth-0.2.0a1/google_auth_demo/google_auth_demo/google_auth_demo.py +263 -0
  8. reflex_google_auth-0.2.0a1/google_auth_demo/requirements.txt +3 -0
  9. reflex_google_auth-0.1.0/custom_components/reflex_google_auth/state.py +0 -96
  10. reflex_google_auth-0.1.0/google_auth_demo/google_auth_demo/google_auth_demo.py +0 -83
  11. reflex_google_auth-0.1.0/google_auth_demo/requirements.txt +0 -2
  12. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/.github/workflows/pre-commit.yaml +0 -0
  13. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/.gitignore +0 -0
  14. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/.pre-commit-config.yaml +0 -0
  15. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/custom_components/reflex_google_auth/__init__.py +0 -0
  16. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/custom_components/reflex_google_auth/decorator.py +0 -0
  17. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/custom_components/reflex_google_auth.egg-info/SOURCES.txt +0 -0
  18. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/custom_components/reflex_google_auth.egg-info/dependency_links.txt +0 -0
  19. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/custom_components/reflex_google_auth.egg-info/requires.txt +0 -0
  20. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/custom_components/reflex_google_auth.egg-info/top_level.txt +0 -0
  21. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/google_auth_demo/.gitignore +0 -0
  22. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/google_auth_demo/assets/favicon.ico +0 -0
  23. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/google_auth_demo/google_auth_demo/__init__.py +0 -0
  24. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/google_auth_demo/rxconfig.py +0 -0
  25. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/pyproject.toml +0 -0
  26. {reflex_google_auth-0.1.0 → reflex_google_auth-0.2.0a1}/setup.cfg +0 -0
@@ -15,15 +15,15 @@ jobs:
15
15
  HAS_PYPI_TOKEN: ${{ secrets.PYPI_TOKEN != '' }}
16
16
  steps:
17
17
  - uses: actions/checkout@master
18
- - name: Set up Python 3.12
19
- uses: actions/setup-python@v3
18
+ - name: Install the latest version of uv
19
+ uses: astral-sh/setup-uv@v6
20
20
  with:
21
21
  python-version: "3.12"
22
22
  - name: Install package
23
23
  run: pip install .
24
24
  - name: Publish to PyPI
25
25
  if: ${{ env.HAS_PYPI_TOKEN == 'true' }}
26
- run: reflex component publish -t ${{ secrets.PYPI_TOKEN }} --no-share --no-validate-project-info
26
+ run: uv build && uv publish -u __token__ -p ${{ secrets.PYPI_TOKEN }}
27
27
  deploy:
28
28
  name: Deploy Demo App to Reflex Cloud
29
29
  runs-on: ubuntu-latest
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: reflex-google-auth
3
- Version: 0.1.0
3
+ Version: 0.2.0a1
4
4
  Summary: Sign in with Google
5
5
  Author-email: Masen Furer <m_github@0x26.net>
6
6
  License: Apache-2.0
@@ -127,3 +127,18 @@ def custom_button() -> rx.Component:
127
127
  f"{GoogleAuthState.tokeninfo['email']} clicked a custom button to login, nice",
128
128
  )
129
129
  ```
130
+
131
+ ### Requesting Scopes
132
+
133
+ By default, only the basic profile scopes are requested. To request additional
134
+ scopes for accessing other Google APIs in the context of the authenticated user:
135
+
136
+ 1. Update your app registration in the Google Cloud Console to add the additional scopes
137
+ you want to request.
138
+ 2. Pass the `scope` parameter to `handle_google_login` event handler. NOTE:
139
+ scopes can only be requested with the `auth-code` flow, so you must set
140
+ `GOOGLE_CLIENT_SECRET` and `GOOGLE_REDIRECT_URI` environment variables as
141
+ described above.
142
+
143
+ See the `custom_scope` example in `google_auth_demo.py` for how to request
144
+ Drive scopes and and then use Google Drive API to store and retrieve app data.
@@ -109,4 +109,19 @@ def custom_button() -> rx.Component:
109
109
  return rx.vstack(
110
110
  f"{GoogleAuthState.tokeninfo['email']} clicked a custom button to login, nice",
111
111
  )
112
- ```
112
+ ```
113
+
114
+ ### Requesting Scopes
115
+
116
+ By default, only the basic profile scopes are requested. To request additional
117
+ scopes for accessing other Google APIs in the context of the authenticated user:
118
+
119
+ 1. Update your app registration in the Google Cloud Console to add the additional scopes
120
+ you want to request.
121
+ 2. Pass the `scope` parameter to `handle_google_login` event handler. NOTE:
122
+ scopes can only be requested with the `auth-code` flow, so you must set
123
+ `GOOGLE_CLIENT_SECRET` and `GOOGLE_REDIRECT_URI` environment variables as
124
+ described above.
125
+
126
+ See the `custom_scope` example in `google_auth_demo.py` for how to request
127
+ Drive scopes and and then use Google Drive API to store and retrieve app data.
@@ -43,8 +43,18 @@ google_login = GoogleLogin.create
43
43
 
44
44
 
45
45
  def handle_google_login(
46
+ scope: str | list[str] | rx.Var[str] | rx.Var[list[str]] = "openid profile email",
46
47
  on_success: EventType[dict] = GoogleAuthState.on_success,
47
48
  ) -> rx.Var[rx.EventChain]:
49
+ """Create a login event chain to handle Google login.
50
+
51
+ Args:
52
+ scope: The space-separated OAuth scopes to request (default "openid profile email").
53
+ on_success: The event to call on successful login (default GoogleAuthState.on_success).
54
+
55
+ Returns:
56
+ An event chain that handles the login process.
57
+ """
48
58
  on_success_event_chain = rx.Var.create(
49
59
  rx.EventChain.create(
50
60
  value=on_success, # type: ignore
@@ -52,6 +62,9 @@ def handle_google_login(
52
62
  key="on_success",
53
63
  )
54
64
  )
65
+ scope = rx.Var.create(scope) if not isinstance(scope, rx.Var) else scope
66
+ if isinstance(scope, rx.vars.ArrayVar):
67
+ scope = scope.join(" ")
55
68
  return rx.Var(
56
69
  "() => login()",
57
70
  _var_type=rx.EventChain,
@@ -61,8 +74,12 @@ def handle_google_login(
61
74
  const login = useGoogleLogin({
62
75
  onSuccess: %s,
63
76
  flow: 'auth-code',
77
+ scope: %s,
64
78
  });"""
65
- % on_success_event_chain: on_success_event_chain._get_all_var_data(),
79
+ % (on_success_event_chain, scope): rx.vars.VarData.merge(
80
+ on_success_event_chain._get_all_var_data(),
81
+ scope._get_all_var_data(),
82
+ ),
66
83
  },
67
84
  imports={LIBRARY: "useGoogleLogin"},
68
85
  ),
@@ -0,0 +1,206 @@
1
+ """Handle Google Auth."""
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ from typing import TypedDict
7
+
8
+ import reflex as rx
9
+ from google.auth.transport import requests
10
+ from google.oauth2.id_token import verify_oauth2_token
11
+ from httpx import AsyncClient
12
+
13
+ TOKEN_URI = "https://oauth2.googleapis.com/token"
14
+ CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "")
15
+ CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", "")
16
+ REDIRECT_URI = os.environ.get("GOOGLE_REDIRECT_URI", "")
17
+
18
+
19
+ def set_client_id(client_id: str):
20
+ """Set the client id."""
21
+ global CLIENT_ID
22
+ CLIENT_ID = client_id
23
+
24
+
25
+ class TokenResponse(TypedDict, total=False):
26
+ access_token: str
27
+ expires_in: int
28
+ refresh_token: str
29
+ scope: str
30
+ token_type: str
31
+ id_token: str
32
+
33
+
34
+ class TokenCredential(TypedDict, total=False):
35
+ iss: str
36
+ azp: str
37
+ aud: str
38
+ sub: str
39
+ hd: str
40
+ email: str
41
+ email_verified: bool
42
+ at_hash: str
43
+ nbf: int
44
+ name: str
45
+ picture: str
46
+ given_name: str
47
+ family_name: str
48
+ iat: int
49
+ exp: int
50
+ jti: str
51
+
52
+
53
+ async def get_token(auth_code) -> TokenResponse:
54
+ """Get the token(s) from an auth code.
55
+
56
+ Args:
57
+ auth_code: Returned from an 'auth-code' flow.
58
+
59
+ Returns:
60
+ The token response, containing access_token, refresh_token and id_token.
61
+ """
62
+ async with AsyncClient() as client:
63
+ response = await client.post(
64
+ TOKEN_URI,
65
+ data={
66
+ "code": auth_code,
67
+ "client_id": CLIENT_ID,
68
+ "client_secret": CLIENT_SECRET,
69
+ "redirect_uri": REDIRECT_URI,
70
+ "grant_type": "authorization_code",
71
+ },
72
+ )
73
+ response.raise_for_status()
74
+ response_data = response.json()
75
+ return TokenResponse(response_data)
76
+
77
+
78
+ async def get_id_token(auth_code: str) -> str:
79
+ token_data = await get_token(auth_code)
80
+ if "id_token" not in token_data:
81
+ raise ValueError("No id_token in token response")
82
+ return token_data["id_token"]
83
+
84
+
85
+ class GoogleAuthState(rx.State):
86
+ token_response_json: str = rx.LocalStorage()
87
+ refresh_token: str = rx.LocalStorage()
88
+
89
+ @rx.var
90
+ def id_token_json(self) -> str:
91
+ """For compatibility only. Use token_response_json instead."""
92
+ try:
93
+ return json.dumps(
94
+ {"credential": json.loads(self.token_response_json).get("id_token", "")}
95
+ )
96
+ except Exception:
97
+ return ""
98
+
99
+ @rx.event
100
+ async def on_success(self, response: dict):
101
+ if "code" in response:
102
+ # Handle auth-code flow
103
+ token_response = await get_token(response["code"])
104
+ self.token_response_json = json.dumps(token_response)
105
+ if "refresh_token" in token_response:
106
+ self.refresh_token = token_response["refresh_token"]
107
+ elif "credential" in response:
108
+ # Handle id-token flow
109
+ self.token_response_json = json.dumps({"id_token": response["credential"]})
110
+ self.refresh_token = ""
111
+ else:
112
+ self.token_response_json = ""
113
+ self.refresh_token = ""
114
+ raise ValueError("No code or credential in response")
115
+
116
+ @rx.event
117
+ async def refresh_access_token(self):
118
+ try:
119
+ if not self.access_token:
120
+ return # no token to refresh
121
+ if not self.refresh_token:
122
+ # token not available, must re-auth
123
+ self.token_response_json = ""
124
+ return
125
+ async with AsyncClient() as client:
126
+ response = await client.post(
127
+ TOKEN_URI,
128
+ data={
129
+ "client_id": CLIENT_ID,
130
+ "client_secret": CLIENT_SECRET,
131
+ "refresh_token": self.refresh_token,
132
+ "grant_type": "refresh_token",
133
+ },
134
+ )
135
+ response.raise_for_status()
136
+ new_token_data = response.json()
137
+ # Save the new refresh token if provided
138
+ if "refresh_token" in new_token_data:
139
+ self.refresh_token = new_token_data["refresh_token"]
140
+ self.token_response_json = json.dumps(new_token_data)
141
+ except Exception as exc:
142
+ print(f"Error refreshing token: {exc!r}") # noqa: T201
143
+
144
+ @rx.var(cache=True)
145
+ def client_id(self) -> str:
146
+ return CLIENT_ID or os.environ.get("GOOGLE_CLIENT_ID", "")
147
+
148
+ @rx.var
149
+ def scopes(self) -> list[str]:
150
+ try:
151
+ scope_str = json.loads(self.token_response_json).get("scope", "")
152
+ return scope_str.split(" ") if scope_str else []
153
+ except Exception:
154
+ return []
155
+
156
+ @rx.var
157
+ def access_token(self) -> str:
158
+ try:
159
+ return json.loads(self.token_response_json).get("access_token", "")
160
+ except Exception:
161
+ return ""
162
+
163
+ @rx.var
164
+ def id_token(self) -> str:
165
+ try:
166
+ return json.loads(self.token_response_json).get("id_token", "")
167
+ except Exception:
168
+ return ""
169
+
170
+ @rx.var(cache=True)
171
+ def tokeninfo(self) -> TokenCredential:
172
+ try:
173
+ return TokenCredential(
174
+ verify_oauth2_token(
175
+ self.id_token,
176
+ requests.Request(),
177
+ self.client_id,
178
+ )
179
+ )
180
+ except Exception as exc:
181
+ if self.token_response_json:
182
+ print(f"Error verifying token: {exc!r}") # noqa: T201
183
+ self.token_response_json = ""
184
+ return {}
185
+
186
+ @rx.event
187
+ def logout(self):
188
+ self.token_response_json = ""
189
+ self.refresh_token = ""
190
+
191
+ @rx.var(cache=False)
192
+ def token_is_valid(self) -> bool:
193
+ try:
194
+ return bool(
195
+ self.tokeninfo and int(self.tokeninfo.get("exp", 0)) > time.time()
196
+ )
197
+ except Exception:
198
+ return False
199
+
200
+ @rx.var(cache=True)
201
+ def user_name(self) -> str:
202
+ return self.tokeninfo.get("name", "")
203
+
204
+ @rx.var(cache=True)
205
+ def user_email(self) -> str:
206
+ return self.tokeninfo.get("email", "")
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: reflex-google-auth
3
- Version: 0.1.0
3
+ Version: 0.2.0a1
4
4
  Summary: Sign in with Google
5
5
  Author-email: Masen Furer <m_github@0x26.net>
6
6
  License: Apache-2.0
@@ -127,3 +127,18 @@ def custom_button() -> rx.Component:
127
127
  f"{GoogleAuthState.tokeninfo['email']} clicked a custom button to login, nice",
128
128
  )
129
129
  ```
130
+
131
+ ### Requesting Scopes
132
+
133
+ By default, only the basic profile scopes are requested. To request additional
134
+ scopes for accessing other Google APIs in the context of the authenticated user:
135
+
136
+ 1. Update your app registration in the Google Cloud Console to add the additional scopes
137
+ you want to request.
138
+ 2. Pass the `scope` parameter to `handle_google_login` event handler. NOTE:
139
+ scopes can only be requested with the `auth-code` flow, so you must set
140
+ `GOOGLE_CLIENT_SECRET` and `GOOGLE_REDIRECT_URI` environment variables as
141
+ described above.
142
+
143
+ See the `custom_scope` example in `google_auth_demo.py` for how to request
144
+ Drive scopes and and then use Google Drive API to store and retrieve app data.
@@ -0,0 +1,263 @@
1
+ import io
2
+ import json
3
+
4
+ import google.oauth2.credentials
5
+ import reflex as rx
6
+ from googleapiclient.discovery import build
7
+ from googleapiclient.errors import HttpError
8
+ from googleapiclient.http import MediaIoBaseDownload, MediaIoBaseUpload
9
+ from reflex_google_auth import (
10
+ GoogleAuthState,
11
+ handle_google_login,
12
+ require_google_login,
13
+ )
14
+
15
+
16
+ class State(GoogleAuthState):
17
+ @rx.var(cache=True)
18
+ def protected_content(self) -> str:
19
+ if self.token_is_valid:
20
+ return f"This content can only be viewed by a logged in User. Nice to see you {self.tokeninfo.get('name')}"
21
+ return "Not logged in."
22
+
23
+
24
+ def user_info(tokeninfo: rx.vars.ObjectVar) -> rx.Component:
25
+ return rx.hstack(
26
+ rx.avatar(
27
+ src=tokeninfo["picture"],
28
+ fallback=tokeninfo["name"],
29
+ size="5",
30
+ ),
31
+ rx.vstack(
32
+ rx.heading(tokeninfo["name"], size="6"),
33
+ rx.text(tokeninfo["email"]),
34
+ align_items="flex-start",
35
+ ),
36
+ rx.button("Logout", on_click=GoogleAuthState.logout),
37
+ padding="10px",
38
+ )
39
+
40
+
41
+ def index():
42
+ return rx.vstack(
43
+ rx.heading("Google OAuth", size="8"),
44
+ rx.link("Protected Page", href="/protected"),
45
+ rx.link("Partially Protected Page", href="/partially-protected"),
46
+ rx.link("Custom Login Button", href="/custom-button"),
47
+ rx.link("Custom Scope", href="/custom-scope"),
48
+ align="center",
49
+ )
50
+
51
+
52
+ @rx.page(route="/protected")
53
+ @require_google_login
54
+ def protected() -> rx.Component:
55
+ return rx.vstack(
56
+ user_info(GoogleAuthState.tokeninfo),
57
+ rx.text(State.protected_content),
58
+ rx.link("Home", href="/"),
59
+ )
60
+
61
+
62
+ @require_google_login
63
+ def user_name_or_sign_in() -> rx.Component:
64
+ return rx.heading(GoogleAuthState.tokeninfo["name"], size="6")
65
+
66
+
67
+ @rx.page(route="/partially-protected")
68
+ def partially_protected() -> rx.Component:
69
+ return rx.vstack(
70
+ rx.heading("This page is partially protected."),
71
+ rx.text(
72
+ "If you are signed in with google, you should see your name below, otherwise "
73
+ "you will see a sign in button",
74
+ ),
75
+ user_name_or_sign_in(),
76
+ )
77
+
78
+
79
+ @rx.page(route="/custom-button")
80
+ @require_google_login(
81
+ button=rx.button("Google Login 🚀", on_click=handle_google_login())
82
+ )
83
+ def custom_button() -> rx.Component:
84
+ return rx.vstack(
85
+ user_info(GoogleAuthState.tokeninfo),
86
+ "You clicked a custom button to login, nice",
87
+ )
88
+
89
+
90
+ class DriveState(rx.State):
91
+ app_data: str = ""
92
+ loading: bool = False
93
+
94
+ async def _get_config_json_metadata(self) -> dict[str, str] | None:
95
+ google_auth_state = await self.get_state(GoogleAuthState)
96
+ if not google_auth_state.token_is_valid:
97
+ return None
98
+ credentials = google.oauth2.credentials.Credentials(
99
+ google_auth_state.access_token,
100
+ refresh_token=google_auth_state.refresh_token,
101
+ )
102
+ service = build("drive", "v3", credentials=credentials)
103
+ results = await rx.run_in_thread(
104
+ service.files()
105
+ .list(
106
+ spaces="appDataFolder",
107
+ fields="nextPageToken, files(id, name)",
108
+ pageSize=10,
109
+ )
110
+ .execute
111
+ )
112
+ items = results.get("files", [])
113
+ if not items:
114
+ print("No files found.") # noqa: T201
115
+ return None
116
+ return items[0]
117
+
118
+ async def _save_file_to_drive(self, content: str):
119
+ google_auth_state = await self.get_state(GoogleAuthState)
120
+ if not google_auth_state.token_is_valid:
121
+ return
122
+ credentials = google.oauth2.credentials.Credentials(
123
+ google_auth_state.access_token,
124
+ refresh_token=google_auth_state.refresh_token,
125
+ )
126
+ payload = {
127
+ "content": content,
128
+ }
129
+ service = build("drive", "v3", credentials=credentials)
130
+ upload_content = MediaIoBaseUpload(
131
+ io.BytesIO(json.dumps(payload).encode("utf-8")),
132
+ mimetype="application/json",
133
+ resumable=True,
134
+ )
135
+
136
+ existing_file = await self._get_config_json_metadata()
137
+ if existing_file:
138
+ file = await rx.run_in_thread(
139
+ service.files()
140
+ .update(
141
+ fileId=existing_file["id"], media_body=upload_content, fields="id"
142
+ )
143
+ .execute
144
+ )
145
+ else:
146
+ file_metadata = {
147
+ "name": "config.json",
148
+ "parents": ["appDataFolder"],
149
+ }
150
+ file = await rx.run_in_thread(
151
+ service.files()
152
+ .create(body=file_metadata, media_body=upload_content, fields="id")
153
+ .execute
154
+ )
155
+ print(f"File ID: {file.get('id')}") # noqa: T201
156
+
157
+ async def _load_file_from_drive(self) -> str:
158
+ google_auth_state = await self.get_state(GoogleAuthState)
159
+ if not google_auth_state.token_is_valid:
160
+ return ""
161
+ credentials = google.oauth2.credentials.Credentials(
162
+ google_auth_state.access_token,
163
+ refresh_token=google_auth_state.refresh_token,
164
+ )
165
+ service = build("drive", "v3", credentials=credentials)
166
+ existing_file = await self._get_config_json_metadata()
167
+ if not existing_file:
168
+ return ""
169
+ try:
170
+ # pylint: disable=maybe-no-member
171
+ request = await rx.run_in_thread(
172
+ lambda: service.files().get_media(fileId=existing_file["id"])
173
+ )
174
+ file = io.BytesIO()
175
+ downloader = MediaIoBaseDownload(file, request)
176
+ done = False
177
+ while done is False:
178
+ status, done = await rx.run_in_thread(downloader.next_chunk)
179
+ except HttpError as error:
180
+ print(f"An error occurred: {error}") # noqa: T201
181
+ file = None
182
+ if file:
183
+ file.seek(0)
184
+ try:
185
+ payload = json.loads(file.read().decode("utf-8"))
186
+ return payload.get("content", "")
187
+ except Exception as exc:
188
+ print(f"Error reading config file: {exc!r}") # noqa: T201
189
+ return ""
190
+
191
+ @rx.event
192
+ async def set_app_data(self, value: str):
193
+ self.app_data = value
194
+ self.loading = True
195
+ yield
196
+ try:
197
+ await self._save_file_to_drive(value)
198
+ finally:
199
+ self.loading = False
200
+
201
+ @rx.event
202
+ async def on_load(self):
203
+ self.loading = True
204
+ yield
205
+ try:
206
+ self.app_data = await self._load_file_from_drive()
207
+ finally:
208
+ self.loading = False
209
+
210
+
211
+ @rx.page(route="/custom-scope", on_load=DriveState.on_load)
212
+ @require_google_login(
213
+ button=rx.button(
214
+ "Login with Drive API scope",
215
+ on_click=handle_google_login(
216
+ scope=(
217
+ "https://www.googleapis.com/auth/drive.appdata "
218
+ "https://www.googleapis.com/auth/drive.file "
219
+ "https://www.googleapis.com/auth/drive.install "
220
+ ),
221
+ ),
222
+ )
223
+ )
224
+ def custom_scope() -> rx.Component:
225
+ return rx.vstack(
226
+ user_info(GoogleAuthState.tokeninfo),
227
+ rx.vstack(
228
+ rx.heading("App Data Stored in Drive"),
229
+ rx.hstack(
230
+ rx.text_area(
231
+ default_value=DriveState.app_data,
232
+ key=DriveState.app_data,
233
+ on_blur=DriveState.set_app_data,
234
+ ),
235
+ rx.cond(
236
+ DriveState.loading,
237
+ rx.spinner(),
238
+ rx.button("Refresh", on_click=DriveState.on_load),
239
+ ),
240
+ ),
241
+ rx.heading("Authorized Scopes"),
242
+ rx.foreach(
243
+ GoogleAuthState.scopes,
244
+ lambda scope: rx.code(scope),
245
+ ),
246
+ rx.heading("Raw Access Token"),
247
+ rx.code(GoogleAuthState.access_token),
248
+ rx.button("Refresh AT", on_click=GoogleAuthState.refresh_access_token),
249
+ rx.hstack(
250
+ "Time until token expiry",
251
+ rx.moment(
252
+ GoogleAuthState.tokeninfo.exp,
253
+ unix=True,
254
+ duration_from_now=True,
255
+ interval=1000,
256
+ ),
257
+ ),
258
+ ),
259
+ )
260
+
261
+
262
+ app = rx.App()
263
+ app.add_page(index)
@@ -0,0 +1,3 @@
1
+ reflex>=0.6.6
2
+ reflex-google-auth
3
+ google-api-python-client>=2.184.0
@@ -1,96 +0,0 @@
1
- """Handle Google Auth."""
2
-
3
- import json
4
- import os
5
- import time
6
-
7
- import reflex as rx
8
- from google.auth.transport import requests
9
- from google.oauth2.id_token import verify_oauth2_token
10
- from httpx import AsyncClient
11
-
12
- TOKEN_URI = "https://oauth2.googleapis.com/token"
13
- CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "")
14
- CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", "")
15
- REDIRECT_URI = os.environ.get("GOOGLE_REDIRECT_URI", "")
16
-
17
-
18
- def set_client_id(client_id: str):
19
- """Set the client id."""
20
- global CLIENT_ID
21
- CLIENT_ID = client_id
22
-
23
-
24
- async def get_id_token(auth_code) -> str:
25
- """Get the id token credential from an auth code.
26
-
27
- Args:
28
- auth_code: Returned from an 'auth-code' flow.
29
-
30
- Returns:
31
- The id token credential.
32
- """
33
- async with AsyncClient() as client:
34
- response = await client.post(
35
- TOKEN_URI,
36
- data={
37
- "code": auth_code,
38
- "client_id": CLIENT_ID,
39
- "client_secret": CLIENT_SECRET,
40
- "redirect_uri": REDIRECT_URI,
41
- "grant_type": "authorization_code",
42
- },
43
- )
44
- response.raise_for_status()
45
- response_data = response.json()
46
- return response_data.get("id_token")
47
-
48
-
49
- class GoogleAuthState(rx.State):
50
- id_token_json: str = rx.LocalStorage()
51
-
52
- @rx.event
53
- async def on_success(self, id_token: dict):
54
- if "code" in id_token:
55
- # Handle auth-code flow
56
- id_token["credential"] = await get_id_token(id_token["code"])
57
- self.id_token_json = json.dumps(id_token)
58
-
59
- @rx.var(cache=True)
60
- def client_id(self) -> str:
61
- return CLIENT_ID or os.environ.get("GOOGLE_CLIENT_ID", "")
62
-
63
- @rx.var(cache=True)
64
- def tokeninfo(self) -> dict[str, str]:
65
- try:
66
- return verify_oauth2_token(
67
- json.loads(self.id_token_json)["credential"],
68
- requests.Request(),
69
- self.client_id,
70
- )
71
- except Exception as exc:
72
- if self.id_token_json:
73
- print(f"Error verifying token: {exc!r}") # noqa: T201
74
- self.id_token_json = ""
75
- return {}
76
-
77
- @rx.event
78
- def logout(self):
79
- self.id_token_json = ""
80
-
81
- @rx.var(cache=False)
82
- def token_is_valid(self) -> bool:
83
- try:
84
- return bool(
85
- self.tokeninfo and int(self.tokeninfo.get("exp", 0)) > time.time()
86
- )
87
- except Exception:
88
- return False
89
-
90
- @rx.var(cache=True)
91
- def user_name(self) -> str:
92
- return self.tokeninfo.get("name", "")
93
-
94
- @rx.var(cache=True)
95
- def user_email(self) -> str:
96
- return self.tokeninfo.get("email", "")
@@ -1,83 +0,0 @@
1
- import reflex as rx
2
- from reflex_google_auth import (
3
- GoogleAuthState,
4
- handle_google_login,
5
- require_google_login,
6
- )
7
-
8
-
9
- class State(GoogleAuthState):
10
- @rx.var(cache=True)
11
- def protected_content(self) -> str:
12
- if self.token_is_valid:
13
- return f"This content can only be viewed by a logged in User. Nice to see you {self.tokeninfo['name']}"
14
- return "Not logged in."
15
-
16
-
17
- def user_info(tokeninfo: rx.vars.ObjectVar) -> rx.Component:
18
- return rx.hstack(
19
- rx.avatar(
20
- src=tokeninfo["picture"],
21
- fallback=tokeninfo["name"],
22
- size="5",
23
- ),
24
- rx.vstack(
25
- rx.heading(tokeninfo["name"], size="6"),
26
- rx.text(tokeninfo["email"]),
27
- align_items="flex-start",
28
- ),
29
- rx.button("Logout", on_click=GoogleAuthState.logout),
30
- padding="10px",
31
- )
32
-
33
-
34
- def index():
35
- return rx.vstack(
36
- rx.heading("Google OAuth", size="8"),
37
- rx.link("Protected Page", href="/protected"),
38
- rx.link("Partially Protected Page", href="/partially-protected"),
39
- rx.link("Custom Login Button", href="/custom-button"),
40
- align="center",
41
- )
42
-
43
-
44
- @rx.page(route="/protected")
45
- @require_google_login
46
- def protected() -> rx.Component:
47
- return rx.vstack(
48
- user_info(GoogleAuthState.tokeninfo),
49
- rx.text(State.protected_content),
50
- rx.link("Home", href="/"),
51
- )
52
-
53
-
54
- @require_google_login
55
- def user_name_or_sign_in() -> rx.Component:
56
- return rx.heading(GoogleAuthState.tokeninfo["name"], size="6")
57
-
58
-
59
- @rx.page(route="/partially-protected")
60
- def partially_protected() -> rx.Component:
61
- return rx.vstack(
62
- rx.heading("This page is partially protected."),
63
- rx.text(
64
- "If you are signed in with google, you should see your name below, otherwise "
65
- "you will see a sign in button",
66
- ),
67
- user_name_or_sign_in(),
68
- )
69
-
70
-
71
- @rx.page(route="/custom-button")
72
- @require_google_login(
73
- button=rx.button("Google Login 🚀", on_click=handle_google_login())
74
- )
75
- def custom_button() -> rx.Component:
76
- return rx.vstack(
77
- user_info(GoogleAuthState.tokeninfo),
78
- "You clicked a custom button to login, nice",
79
- )
80
-
81
-
82
- app = rx.App()
83
- app.add_page(index)
@@ -1,2 +0,0 @@
1
- reflex>=0.6.6
2
- reflex-google-auth