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.
- reflex_google_auth/google_auth.py +18 -1
- reflex_google_auth/state.py +127 -17
- {reflex_google_auth-0.1.0.dist-info → reflex_google_auth-0.2.0a1.dist-info}/METADATA +17 -2
- reflex_google_auth-0.2.0a1.dist-info/RECORD +8 -0
- {reflex_google_auth-0.1.0.dist-info → reflex_google_auth-0.2.0a1.dist-info}/WHEEL +1 -1
- reflex_google_auth-0.1.0.dist-info/RECORD +0 -8
- {reflex_google_auth-0.1.0.dist-info → reflex_google_auth-0.2.0a1.dist-info}/top_level.txt +0 -0
@@ -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:
|
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
|
),
|
reflex_google_auth/state.py
CHANGED
@@ -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
|
-
|
25
|
-
|
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
|
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
|
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
|
-
|
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,
|
54
|
-
if "code" in
|
100
|
+
async def on_success(self, response: dict):
|
101
|
+
if "code" in response:
|
55
102
|
# Handle auth-code flow
|
56
|
-
|
57
|
-
|
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) ->
|
171
|
+
def tokeninfo(self) -> TokenCredential:
|
65
172
|
try:
|
66
|
-
return
|
67
|
-
|
68
|
-
|
69
|
-
|
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.
|
181
|
+
if self.token_response_json:
|
73
182
|
print(f"Error verifying token: {exc!r}") # noqa: T201
|
74
|
-
self.
|
183
|
+
self.token_response_json = ""
|
75
184
|
return {}
|
76
185
|
|
77
186
|
@rx.event
|
78
187
|
def logout(self):
|
79
|
-
self.
|
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.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: reflex-google-auth
|
3
|
-
Version: 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,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,,
|
File without changes
|