reflex-google-auth 0.1.0__py3-none-any.whl → 0.2.0a1__py3-none-any.whl

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.
@@ -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
  ),
@@ -3,6 +3,7 @@
3
3
  import json
4
4
  import os
5
5
  import time
6
+ from typing import TypedDict
6
7
 
7
8
  import reflex as rx
8
9
  from google.auth.transport import requests
@@ -21,14 +22,42 @@ def set_client_id(client_id: str):
21
22
  CLIENT_ID = client_id
22
23
 
23
24
 
24
- async def get_id_token(auth_code) -> str:
25
- """Get the id token credential from an auth code.
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.
26
55
 
27
56
  Args:
28
57
  auth_code: Returned from an 'auth-code' flow.
29
58
 
30
59
  Returns:
31
- The id token credential.
60
+ The token response, containing access_token, refresh_token and id_token.
32
61
  """
33
62
  async with AsyncClient() as client:
34
63
  response = await client.post(
@@ -43,40 +72,121 @@ async def get_id_token(auth_code) -> str:
43
72
  )
44
73
  response.raise_for_status()
45
74
  response_data = response.json()
46
- return response_data.get("id_token")
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"]
47
83
 
48
84
 
49
85
  class GoogleAuthState(rx.State):
50
- id_token_json: str = rx.LocalStorage()
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 ""
51
98
 
52
99
  @rx.event
53
- async def on_success(self, id_token: dict):
54
- if "code" in id_token:
100
+ async def on_success(self, response: dict):
101
+ if "code" in response:
55
102
  # Handle auth-code flow
56
- id_token["credential"] = await get_id_token(id_token["code"])
57
- self.id_token_json = json.dumps(id_token)
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
58
143
 
59
144
  @rx.var(cache=True)
60
145
  def client_id(self) -> str:
61
146
  return CLIENT_ID or os.environ.get("GOOGLE_CLIENT_ID", "")
62
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
+
63
170
  @rx.var(cache=True)
64
- def tokeninfo(self) -> dict[str, str]:
171
+ def tokeninfo(self) -> TokenCredential:
65
172
  try:
66
- return verify_oauth2_token(
67
- json.loads(self.id_token_json)["credential"],
68
- requests.Request(),
69
- self.client_id,
173
+ return TokenCredential(
174
+ verify_oauth2_token(
175
+ self.id_token,
176
+ requests.Request(),
177
+ self.client_id,
178
+ )
70
179
  )
71
180
  except Exception as exc:
72
- if self.id_token_json:
181
+ if self.token_response_json:
73
182
  print(f"Error verifying token: {exc!r}") # noqa: T201
74
- self.id_token_json = ""
183
+ self.token_response_json = ""
75
184
  return {}
76
185
 
77
186
  @rx.event
78
187
  def logout(self):
79
- self.id_token_json = ""
188
+ self.token_response_json = ""
189
+ self.refresh_token = ""
80
190
 
81
191
  @rx.var(cache=False)
82
192
  def token_is_valid(self) -> bool:
@@ -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,8 @@
1
+ reflex_google_auth/__init__.py,sha256=A3PO7fwBSKoX2nPN5JohqrnZ1cu9nLaP5JsLR9SjbZI,339
2
+ reflex_google_auth/decorator.py,sha256=xjlC4AgU5lX4CyTjM67hTQUFIc_0I1JBC8V4CqvJtM0,1763
3
+ reflex_google_auth/google_auth.py,sha256=sdg3UlON3UYL6GtA9i8yCyjqSpLgNZOKEhJePzzxjvE,2402
4
+ reflex_google_auth/state.py,sha256=fXK2TtPUT4YdUxe5BrtyWwzHJ5GGZbD7rLisr3GzmBI,6164
5
+ reflex_google_auth-0.2.0a1.dist-info/METADATA,sha256=COQ0FYWkpCMESwlOKye1EGAMsGz9PeEJTYaCNDNli44,5562
6
+ reflex_google_auth-0.2.0a1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ reflex_google_auth-0.2.0a1.dist-info/top_level.txt,sha256=JB7Letu_7VOfOx5EhyGn_bElQjzSq43-LHafE90wqyM,19
8
+ reflex_google_auth-0.2.0a1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,8 +0,0 @@
1
- reflex_google_auth/__init__.py,sha256=A3PO7fwBSKoX2nPN5JohqrnZ1cu9nLaP5JsLR9SjbZI,339
2
- reflex_google_auth/decorator.py,sha256=xjlC4AgU5lX4CyTjM67hTQUFIc_0I1JBC8V4CqvJtM0,1763
3
- reflex_google_auth/google_auth.py,sha256=PG2QUFcCVQDtUKGqfJ0DabrIOvO6jthNxdXWPPoB-8Y,1696
4
- reflex_google_auth/state.py,sha256=-rQTvZjkUYiOgYzUkEJNwgMY7V2lgDLA4D94yNUmHQ8,2691
5
- reflex_google_auth-0.1.0.dist-info/METADATA,sha256=aGXubR8qOuOo5Qo3a5FZrZCWKsuQQPAn7mUVELQKduQ,4859
6
- reflex_google_auth-0.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
7
- reflex_google_auth-0.1.0.dist-info/top_level.txt,sha256=JB7Letu_7VOfOx5EhyGn_bElQjzSq43-LHafE90wqyM,19
8
- reflex_google_auth-0.1.0.dist-info/RECORD,,