reflex-google-auth 0.0.7__tar.gz → 0.0.9a1__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.
- reflex_google_auth-0.0.9a1/.github/workflows/publish.yml +41 -0
- reflex_google_auth-0.0.9a1/.gitignore +6 -0
- {reflex_google_auth-0.0.7 → reflex_google_auth-0.0.9a1}/PKG-INFO +42 -4
- {reflex_google_auth-0.0.7 → reflex_google_auth-0.0.9a1}/README.md +40 -2
- {reflex_google_auth-0.0.7 → reflex_google_auth-0.0.9a1}/custom_components/reflex_google_auth/__init__.py +2 -1
- reflex_google_auth-0.0.9a1/custom_components/reflex_google_auth/decorator.py +70 -0
- reflex_google_auth-0.0.9a1/custom_components/reflex_google_auth/google_auth.py +70 -0
- reflex_google_auth-0.0.9a1/custom_components/reflex_google_auth/google_auth.pyi +24 -0
- {reflex_google_auth-0.0.7 → reflex_google_auth-0.0.9a1}/custom_components/reflex_google_auth/state.py +38 -3
- {reflex_google_auth-0.0.7 → reflex_google_auth-0.0.9a1}/custom_components/reflex_google_auth.egg-info/PKG-INFO +42 -4
- {reflex_google_auth-0.0.7 → reflex_google_auth-0.0.9a1}/custom_components/reflex_google_auth.egg-info/SOURCES.txt +10 -1
- {reflex_google_auth-0.0.7 → reflex_google_auth-0.0.9a1}/custom_components/reflex_google_auth.egg-info/requires.txt +1 -1
- reflex_google_auth-0.0.9a1/google_auth_demo/.gitignore +5 -0
- reflex_google_auth-0.0.9a1/google_auth_demo/assets/favicon.ico +0 -0
- reflex_google_auth-0.0.9a1/google_auth_demo/google_auth_demo/__init__.py +0 -0
- reflex_google_auth-0.0.9a1/google_auth_demo/google_auth_demo/google_auth_demo.py +84 -0
- reflex_google_auth-0.0.9a1/google_auth_demo/requirements.txt +2 -0
- reflex_google_auth-0.0.9a1/google_auth_demo/rxconfig.py +5 -0
- {reflex_google_auth-0.0.7 → reflex_google_auth-0.0.9a1}/pyproject.toml +6 -3
- reflex_google_auth-0.0.7/custom_components/reflex_google_auth/decorator.py +0 -25
- reflex_google_auth-0.0.7/custom_components/reflex_google_auth/google_auth.py +0 -37
- {reflex_google_auth-0.0.7 → reflex_google_auth-0.0.9a1}/custom_components/reflex_google_auth.egg-info/dependency_links.txt +0 -0
- {reflex_google_auth-0.0.7 → reflex_google_auth-0.0.9a1}/custom_components/reflex_google_auth.egg-info/top_level.txt +0 -0
- {reflex_google_auth-0.0.7 → reflex_google_auth-0.0.9a1}/setup.cfg +0 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
name: Publish Component to PyPI
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
tags:
|
6
|
+
- "*"
|
7
|
+
|
8
|
+
jobs:
|
9
|
+
publish:
|
10
|
+
name: Publish Component to PyPI
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
steps:
|
13
|
+
- uses: actions/checkout@master
|
14
|
+
- name: Set up Python 3.12
|
15
|
+
uses: actions/setup-python@v3
|
16
|
+
with:
|
17
|
+
python-version: "3.12"
|
18
|
+
- name: Install package
|
19
|
+
run: pip install .
|
20
|
+
- name: Publish to PyPI
|
21
|
+
run: reflex component publish -t ${{ secrets.PYPI_TOKEN }} --no-share --no-validate-project-info
|
22
|
+
deploy:
|
23
|
+
name: Deploy Demo App to Reflex Cloud
|
24
|
+
runs-on: ubuntu-latest
|
25
|
+
steps:
|
26
|
+
- uses: actions/checkout@master
|
27
|
+
- name: Set up Python 3.12
|
28
|
+
uses: actions/setup-python@v3
|
29
|
+
with:
|
30
|
+
python-version: "3.12"
|
31
|
+
- name: Set reflex version for deploy
|
32
|
+
run: sed -e "s/^reflex-google-auth[ >=].*$/reflex-google-auth==${{ github.event.release.tag_name }}/" -i google_auth_demo/requirements.txt
|
33
|
+
- name: Deploy to ReflexCloud
|
34
|
+
uses: reflex-dev/reflex-deploy-action@v2
|
35
|
+
with:
|
36
|
+
auth_token: ${{ secrets.REFLEX_AUTH_TOKEN }}
|
37
|
+
project_id: ${{ secrets.REFLEX_PROJECT_ID }}
|
38
|
+
app_directory: google_auth_demo
|
39
|
+
extra_args: ${{ env.EXTRA_ARGS }}
|
40
|
+
dry_run: ${{ vars.DRY_RUN }}
|
41
|
+
skip_checkout: "true"
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.2
|
2
2
|
Name: reflex-google-auth
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.9a1
|
4
4
|
Summary: Sign in with Google
|
5
5
|
Author-email: Masen Furer <m_github@0x26.net>
|
6
6
|
License: Apache-2.0
|
@@ -9,7 +9,7 @@ Keywords: reflex,reflex-custom-components
|
|
9
9
|
Classifier: Development Status :: 4 - Beta
|
10
10
|
Requires-Python: >=3.8
|
11
11
|
Description-Content-Type: text/markdown
|
12
|
-
Requires-Dist: reflex>=0.
|
12
|
+
Requires-Dist: reflex>=0.6.6
|
13
13
|
Requires-Dist: google-auth[requests]
|
14
14
|
Provides-Extra: dev
|
15
15
|
Requires-Dist: build; extra == "dev"
|
@@ -42,11 +42,22 @@ Head over to https://console.developers.google.com/apis/credentials and sign in
|
|
42
42
|
- From the "Credentials" page, click "+ Create Credentials", then "OAuth client ID"
|
43
43
|
- Select Application Type: "Web Application"
|
44
44
|
- Add Authorized Javascript Origins: http://localhost, http://localhost:3000, https://example.com (prod domain must be HTTPS)
|
45
|
+
- If using custom button, add the same origins to "Authorized redirect URIs"
|
45
46
|
- Click "Save"
|
46
|
-
- Copy the OAuth "Client ID" and save it for later. Mine looks like 309209880368-4uqd9e44h7t4alhhdqn48pvvr63cc5j5.apps.googleusercontent.com
|
47
|
+
- Copy the OAuth "Client ID" and "Client Secret" and save it for later. Mine looks like 309209880368-4uqd9e44h7t4alhhdqn48pvvr63cc5j5.apps.googleusercontent.com
|
47
48
|
|
48
49
|
https://github.com/reflex-dev/reflex-examples/assets/1524005/af2499a6-0bda-4d60-b52b-4f51b7322fd5
|
49
50
|
|
51
|
+
### Configure Environment Variables
|
52
|
+
|
53
|
+
Set the following environment variables based on your deployment.
|
54
|
+
|
55
|
+
```bash
|
56
|
+
export GOOGLE_CLIENT_ID="309209880368-4uqd9e44h7t4alhhdqn48pvvr63cc5j5.apps.googleusercontent.com"
|
57
|
+
export GOOGLE_CLIENT_SECRET="your_client_secret"
|
58
|
+
export GOOGLE_REDIRECT_URI="http://localhost:3000"
|
59
|
+
```
|
60
|
+
|
50
61
|
### Integrate with Reflex app
|
51
62
|
|
52
63
|
The `GoogleAuthState` provided by this component has a `token_is_valid` var that
|
@@ -89,3 +100,30 @@ See the example in
|
|
89
100
|
[masenf/rx_shout](https://github.com/masenf/rx_shout/blob/main/rx_shout/state.py)
|
90
101
|
for how to integrate an authenticated Google user with other app-specific user
|
91
102
|
data.
|
103
|
+
|
104
|
+
### Customizing the Button
|
105
|
+
|
106
|
+
If you want to use your own login button, you may use whatever component you
|
107
|
+
like, as long as it is wrapped in a `reflex_google_auth.google_oauth_provider`
|
108
|
+
component and the `on_click` triggers
|
109
|
+
`reflex_google_auth.handle_google_login()`. Note that this cannot be combined
|
110
|
+
with other event handlers.
|
111
|
+
|
112
|
+
This functionality is also exposed in the `require_google_auth` decorator, which
|
113
|
+
accepts a `button` keyword argument.
|
114
|
+
|
115
|
+
When using a custom button, the returned auth-code _must be validated on the
|
116
|
+
backend_, which is handled by this library, but **requires additionally setting
|
117
|
+
`GOOGLE_CLIENT_SECRET` and `GOOGLE_REDIRECT_URI` environment variables**. These
|
118
|
+
can be configured in the Google Cloud Console as described above.
|
119
|
+
|
120
|
+
```python
|
121
|
+
from reflex_google_auth import handle_google_login, require_google_login, GoogleAuthState
|
122
|
+
|
123
|
+
|
124
|
+
@require_google_login(button=rx.button("Google Login 🚀", on_click=handle_google_login()))
|
125
|
+
def custom_button() -> rx.Component:
|
126
|
+
return rx.vstack(
|
127
|
+
f"{GoogleAuthState.tokeninfo['email']} clicked a custom button to login, nice",
|
128
|
+
)
|
129
|
+
```
|
@@ -25,11 +25,22 @@ Head over to https://console.developers.google.com/apis/credentials and sign in
|
|
25
25
|
- From the "Credentials" page, click "+ Create Credentials", then "OAuth client ID"
|
26
26
|
- Select Application Type: "Web Application"
|
27
27
|
- Add Authorized Javascript Origins: http://localhost, http://localhost:3000, https://example.com (prod domain must be HTTPS)
|
28
|
+
- If using custom button, add the same origins to "Authorized redirect URIs"
|
28
29
|
- Click "Save"
|
29
|
-
- Copy the OAuth "Client ID" and save it for later. Mine looks like 309209880368-4uqd9e44h7t4alhhdqn48pvvr63cc5j5.apps.googleusercontent.com
|
30
|
+
- Copy the OAuth "Client ID" and "Client Secret" and save it for later. Mine looks like 309209880368-4uqd9e44h7t4alhhdqn48pvvr63cc5j5.apps.googleusercontent.com
|
30
31
|
|
31
32
|
https://github.com/reflex-dev/reflex-examples/assets/1524005/af2499a6-0bda-4d60-b52b-4f51b7322fd5
|
32
33
|
|
34
|
+
### Configure Environment Variables
|
35
|
+
|
36
|
+
Set the following environment variables based on your deployment.
|
37
|
+
|
38
|
+
```bash
|
39
|
+
export GOOGLE_CLIENT_ID="309209880368-4uqd9e44h7t4alhhdqn48pvvr63cc5j5.apps.googleusercontent.com"
|
40
|
+
export GOOGLE_CLIENT_SECRET="your_client_secret"
|
41
|
+
export GOOGLE_REDIRECT_URI="http://localhost:3000"
|
42
|
+
```
|
43
|
+
|
33
44
|
### Integrate with Reflex app
|
34
45
|
|
35
46
|
The `GoogleAuthState` provided by this component has a `token_is_valid` var that
|
@@ -71,4 +82,31 @@ To uniquely identify a user, the `GoogleAuthState.tokeninfo['sub']` field can be
|
|
71
82
|
See the example in
|
72
83
|
[masenf/rx_shout](https://github.com/masenf/rx_shout/blob/main/rx_shout/state.py)
|
73
84
|
for how to integrate an authenticated Google user with other app-specific user
|
74
|
-
data.
|
85
|
+
data.
|
86
|
+
|
87
|
+
### Customizing the Button
|
88
|
+
|
89
|
+
If you want to use your own login button, you may use whatever component you
|
90
|
+
like, as long as it is wrapped in a `reflex_google_auth.google_oauth_provider`
|
91
|
+
component and the `on_click` triggers
|
92
|
+
`reflex_google_auth.handle_google_login()`. Note that this cannot be combined
|
93
|
+
with other event handlers.
|
94
|
+
|
95
|
+
This functionality is also exposed in the `require_google_auth` decorator, which
|
96
|
+
accepts a `button` keyword argument.
|
97
|
+
|
98
|
+
When using a custom button, the returned auth-code _must be validated on the
|
99
|
+
backend_, which is handled by this library, but **requires additionally setting
|
100
|
+
`GOOGLE_CLIENT_SECRET` and `GOOGLE_REDIRECT_URI` environment variables**. These
|
101
|
+
can be configured in the Google Cloud Console as described above.
|
102
|
+
|
103
|
+
```python
|
104
|
+
from reflex_google_auth import handle_google_login, require_google_login, GoogleAuthState
|
105
|
+
|
106
|
+
|
107
|
+
@require_google_login(button=rx.button("Google Login 🚀", on_click=handle_google_login()))
|
108
|
+
def custom_button() -> rx.Component:
|
109
|
+
return rx.vstack(
|
110
|
+
f"{GoogleAuthState.tokeninfo['email']} clicked a custom button to login, nice",
|
111
|
+
)
|
112
|
+
```
|
@@ -1,11 +1,12 @@
|
|
1
1
|
from .decorator import require_google_login
|
2
|
-
from .google_auth import google_login, google_oauth_provider
|
2
|
+
from .google_auth import google_login, google_oauth_provider, handle_google_login
|
3
3
|
from .state import GoogleAuthState, set_client_id
|
4
4
|
|
5
5
|
__all__ = [
|
6
6
|
"GoogleAuthState",
|
7
7
|
"google_oauth_provider",
|
8
8
|
"google_login",
|
9
|
+
"handle_google_login",
|
9
10
|
"set_client_id",
|
10
11
|
"require_google_login",
|
11
12
|
]
|
@@ -0,0 +1,70 @@
|
|
1
|
+
import functools
|
2
|
+
from typing import Callable, overload
|
3
|
+
|
4
|
+
import reflex as rx
|
5
|
+
|
6
|
+
from . import google_auth
|
7
|
+
from .state import GoogleAuthState
|
8
|
+
|
9
|
+
|
10
|
+
ComponentCallable = Callable[[], rx.Component]
|
11
|
+
|
12
|
+
|
13
|
+
@overload
|
14
|
+
def require_google_login(
|
15
|
+
page: ComponentCallable,
|
16
|
+
) -> ComponentCallable: ...
|
17
|
+
|
18
|
+
|
19
|
+
@overload
|
20
|
+
def require_google_login() -> Callable[[ComponentCallable], ComponentCallable]: ...
|
21
|
+
|
22
|
+
|
23
|
+
@overload
|
24
|
+
def require_google_login(
|
25
|
+
*,
|
26
|
+
button: rx.Component | None,
|
27
|
+
) -> Callable[[ComponentCallable], ComponentCallable]: ...
|
28
|
+
|
29
|
+
|
30
|
+
def require_google_login(
|
31
|
+
page: ComponentCallable | None = None,
|
32
|
+
*,
|
33
|
+
button: rx.Component | None = None,
|
34
|
+
) -> ComponentCallable | Callable[[ComponentCallable], ComponentCallable]:
|
35
|
+
"""Decorator to require Google login before rendering a page.
|
36
|
+
|
37
|
+
The login button should have on_click set to `reflex_google_auth.handle_google_login`.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
page: Page to render after Google login.
|
41
|
+
button: Button to render if Google login is required.
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
A decorator function or the decorated page.
|
45
|
+
"""
|
46
|
+
|
47
|
+
if button is None:
|
48
|
+
button = google_auth.google_login()
|
49
|
+
|
50
|
+
def _inner(page: Callable[[], rx.Component]) -> Callable[[], rx.Component]:
|
51
|
+
@functools.wraps(page)
|
52
|
+
def _auth_wrapper() -> rx.Component:
|
53
|
+
return google_auth.google_oauth_provider(
|
54
|
+
rx.cond(
|
55
|
+
rx.State.is_hydrated,
|
56
|
+
rx.cond(
|
57
|
+
GoogleAuthState.token_is_valid,
|
58
|
+
page(),
|
59
|
+
button,
|
60
|
+
),
|
61
|
+
),
|
62
|
+
)
|
63
|
+
|
64
|
+
_auth_wrapper.__name__ = page.__name__
|
65
|
+
|
66
|
+
return _auth_wrapper
|
67
|
+
|
68
|
+
if page is None:
|
69
|
+
return _inner
|
70
|
+
return _inner(page=page)
|
@@ -0,0 +1,70 @@
|
|
1
|
+
from typing import cast
|
2
|
+
import reflex as rx
|
3
|
+
from reflex.event import EventType, BASE_STATE
|
4
|
+
|
5
|
+
from .state import GoogleAuthState
|
6
|
+
|
7
|
+
|
8
|
+
LIBRARY = "@react-oauth/google"
|
9
|
+
|
10
|
+
|
11
|
+
class GoogleOAuthProvider(rx.Component):
|
12
|
+
library = LIBRARY
|
13
|
+
tag = "GoogleOAuthProvider"
|
14
|
+
|
15
|
+
client_id: rx.Var[str]
|
16
|
+
|
17
|
+
@classmethod
|
18
|
+
def create(cls, *children, **props) -> rx.Component:
|
19
|
+
props.setdefault("client_id", GoogleAuthState.client_id)
|
20
|
+
return super().create(*children, **props)
|
21
|
+
|
22
|
+
|
23
|
+
google_oauth_provider = GoogleOAuthProvider.create
|
24
|
+
|
25
|
+
|
26
|
+
def _on_success_signature(data: rx.Var[dict]) -> tuple[rx.Var[dict]]:
|
27
|
+
return (data,)
|
28
|
+
|
29
|
+
|
30
|
+
class GoogleLogin(rx.Component):
|
31
|
+
library = LIBRARY
|
32
|
+
tag = "GoogleLogin"
|
33
|
+
|
34
|
+
on_success: rx.EventHandler[_on_success_signature]
|
35
|
+
|
36
|
+
@classmethod
|
37
|
+
def create(cls, **props) -> "GoogleLogin":
|
38
|
+
props.setdefault("on_success", GoogleAuthState.on_success)
|
39
|
+
return cast("GoogleLogin", super().create(**props))
|
40
|
+
|
41
|
+
|
42
|
+
google_login = GoogleLogin.create
|
43
|
+
|
44
|
+
|
45
|
+
def handle_google_login(
|
46
|
+
on_success: EventType[[dict], BASE_STATE] = GoogleAuthState.on_success,
|
47
|
+
) -> rx.Var[rx.EventChain]:
|
48
|
+
on_success_event_chain = rx.Var.create(
|
49
|
+
# TODO: rx.EventChain.create(
|
50
|
+
rx.Component()._create_event_chain(
|
51
|
+
value=on_success, # type: ignore
|
52
|
+
args_spec=_on_success_signature,
|
53
|
+
key="on_success",
|
54
|
+
)
|
55
|
+
)
|
56
|
+
return rx.Var(
|
57
|
+
"() => login()",
|
58
|
+
_var_type=rx.EventChain,
|
59
|
+
_var_data=rx.vars.VarData(
|
60
|
+
hooks={
|
61
|
+
"""
|
62
|
+
const login = useGoogleLogin({
|
63
|
+
onSuccess: %s,
|
64
|
+
flow: 'auth-code',
|
65
|
+
});"""
|
66
|
+
% on_success_event_chain: None,
|
67
|
+
},
|
68
|
+
imports={LIBRARY: "useGoogleLogin"},
|
69
|
+
).merge(on_success_event_chain._get_all_var_data()),
|
70
|
+
)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from typing import Any, Dict, Literal, Optional, Union, overload
|
2
|
+
from reflex.vars import Var, BaseVar, ComputedVar
|
3
|
+
from reflex.event import EventChain, EventHandler, EventSpec
|
4
|
+
from reflex.style import Style
|
5
|
+
import reflex as rx
|
6
|
+
from .state import GoogleAuthState
|
7
|
+
|
8
|
+
class GoogleOAuthProvider(rx.Component):
|
9
|
+
|
10
|
+
@overload
|
11
|
+
@classmethod
|
12
|
+
def create(cls, *children, client_id: Optional[Union[Var[str], str]]=None, style: Optional[Style]=None, key: Optional[Any]=None, id: Optional[Any]=None, class_name: Optional[Any]=None, autofocus: Optional[bool]=None, custom_attrs: Optional[Dict[str, Union[Var, str]]]=None, on_blur: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_click: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_context_menu: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_double_click: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_focus: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mount: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mouse_down: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mouse_enter: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mouse_leave: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mouse_move: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mouse_out: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mouse_over: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mouse_up: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_scroll: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_unmount: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, **props) -> 'GoogleOAuthProvider':
|
13
|
+
""""""
|
14
|
+
...
|
15
|
+
google_oauth_provider = GoogleOAuthProvider.create
|
16
|
+
|
17
|
+
class GoogleLogin(rx.Component):
|
18
|
+
|
19
|
+
@overload
|
20
|
+
@classmethod
|
21
|
+
def create(cls, *children, on_success: Optional[EventHandler[Any]]=None, style: Optional[Style]=None, key: Optional[Any]=None, id: Optional[Any]=None, class_name: Optional[Any]=None, autofocus: Optional[bool]=None, custom_attrs: Optional[Dict[str, Union[Var, str]]]=None, on_blur: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_click: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_context_menu: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_double_click: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_focus: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mount: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mouse_down: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mouse_enter: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mouse_leave: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mouse_move: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mouse_out: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mouse_over: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_mouse_up: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_scroll: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_success: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, on_unmount: Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]=None, **props) -> 'GoogleLogin':
|
22
|
+
""""""
|
23
|
+
...
|
24
|
+
google_login = GoogleLogin.create
|
@@ -6,11 +6,15 @@ import time
|
|
6
6
|
|
7
7
|
from google.auth.transport import requests
|
8
8
|
from google.oauth2.id_token import verify_oauth2_token
|
9
|
+
from httpx import AsyncClient
|
9
10
|
|
10
11
|
import reflex as rx
|
11
12
|
|
12
13
|
|
13
|
-
|
14
|
+
TOKEN_URI = "https://oauth2.googleapis.com/token"
|
15
|
+
CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "")
|
16
|
+
CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", "")
|
17
|
+
REDIRECT_URI = os.environ.get("GOOGLE_REDIRECT_URI", "")
|
14
18
|
|
15
19
|
|
16
20
|
def set_client_id(client_id: str):
|
@@ -19,10 +23,39 @@ def set_client_id(client_id: str):
|
|
19
23
|
CLIENT_ID = client_id
|
20
24
|
|
21
25
|
|
26
|
+
async def get_id_token(auth_code) -> str:
|
27
|
+
"""Get the id token credential from an auth code.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
auth_code: Returned from an 'auth-code' flow.
|
31
|
+
|
32
|
+
Returns:
|
33
|
+
The id token credential.
|
34
|
+
"""
|
35
|
+
async with AsyncClient() as client:
|
36
|
+
response = await client.post(
|
37
|
+
TOKEN_URI,
|
38
|
+
data={
|
39
|
+
"code": auth_code,
|
40
|
+
"client_id": CLIENT_ID,
|
41
|
+
"client_secret": CLIENT_SECRET,
|
42
|
+
"redirect_uri": REDIRECT_URI,
|
43
|
+
"grant_type": "authorization_code",
|
44
|
+
},
|
45
|
+
)
|
46
|
+
response.raise_for_status()
|
47
|
+
response_data = response.json()
|
48
|
+
return response_data.get("id_token")
|
49
|
+
|
50
|
+
|
22
51
|
class GoogleAuthState(rx.State):
|
23
52
|
id_token_json: str = rx.LocalStorage()
|
24
53
|
|
25
|
-
|
54
|
+
@rx.event
|
55
|
+
async def on_success(self, id_token: dict):
|
56
|
+
if "code" in id_token:
|
57
|
+
# Handle auth-code flow
|
58
|
+
id_token["credential"] = await get_id_token(id_token["code"])
|
26
59
|
self.id_token_json = json.dumps(id_token)
|
27
60
|
|
28
61
|
@rx.var(cache=True)
|
@@ -39,9 +72,11 @@ class GoogleAuthState(rx.State):
|
|
39
72
|
)
|
40
73
|
except Exception as exc:
|
41
74
|
if self.id_token_json:
|
42
|
-
print(f"Error verifying token: {exc}")
|
75
|
+
print(f"Error verifying token: {exc!r}")
|
76
|
+
self.id_token_json = ""
|
43
77
|
return {}
|
44
78
|
|
79
|
+
@rx.event
|
45
80
|
def logout(self):
|
46
81
|
self.id_token_json = ""
|
47
82
|
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.2
|
2
2
|
Name: reflex-google-auth
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.9a1
|
4
4
|
Summary: Sign in with Google
|
5
5
|
Author-email: Masen Furer <m_github@0x26.net>
|
6
6
|
License: Apache-2.0
|
@@ -9,7 +9,7 @@ Keywords: reflex,reflex-custom-components
|
|
9
9
|
Classifier: Development Status :: 4 - Beta
|
10
10
|
Requires-Python: >=3.8
|
11
11
|
Description-Content-Type: text/markdown
|
12
|
-
Requires-Dist: reflex>=0.
|
12
|
+
Requires-Dist: reflex>=0.6.6
|
13
13
|
Requires-Dist: google-auth[requests]
|
14
14
|
Provides-Extra: dev
|
15
15
|
Requires-Dist: build; extra == "dev"
|
@@ -42,11 +42,22 @@ Head over to https://console.developers.google.com/apis/credentials and sign in
|
|
42
42
|
- From the "Credentials" page, click "+ Create Credentials", then "OAuth client ID"
|
43
43
|
- Select Application Type: "Web Application"
|
44
44
|
- Add Authorized Javascript Origins: http://localhost, http://localhost:3000, https://example.com (prod domain must be HTTPS)
|
45
|
+
- If using custom button, add the same origins to "Authorized redirect URIs"
|
45
46
|
- Click "Save"
|
46
|
-
- Copy the OAuth "Client ID" and save it for later. Mine looks like 309209880368-4uqd9e44h7t4alhhdqn48pvvr63cc5j5.apps.googleusercontent.com
|
47
|
+
- Copy the OAuth "Client ID" and "Client Secret" and save it for later. Mine looks like 309209880368-4uqd9e44h7t4alhhdqn48pvvr63cc5j5.apps.googleusercontent.com
|
47
48
|
|
48
49
|
https://github.com/reflex-dev/reflex-examples/assets/1524005/af2499a6-0bda-4d60-b52b-4f51b7322fd5
|
49
50
|
|
51
|
+
### Configure Environment Variables
|
52
|
+
|
53
|
+
Set the following environment variables based on your deployment.
|
54
|
+
|
55
|
+
```bash
|
56
|
+
export GOOGLE_CLIENT_ID="309209880368-4uqd9e44h7t4alhhdqn48pvvr63cc5j5.apps.googleusercontent.com"
|
57
|
+
export GOOGLE_CLIENT_SECRET="your_client_secret"
|
58
|
+
export GOOGLE_REDIRECT_URI="http://localhost:3000"
|
59
|
+
```
|
60
|
+
|
50
61
|
### Integrate with Reflex app
|
51
62
|
|
52
63
|
The `GoogleAuthState` provided by this component has a `token_is_valid` var that
|
@@ -89,3 +100,30 @@ See the example in
|
|
89
100
|
[masenf/rx_shout](https://github.com/masenf/rx_shout/blob/main/rx_shout/state.py)
|
90
101
|
for how to integrate an authenticated Google user with other app-specific user
|
91
102
|
data.
|
103
|
+
|
104
|
+
### Customizing the Button
|
105
|
+
|
106
|
+
If you want to use your own login button, you may use whatever component you
|
107
|
+
like, as long as it is wrapped in a `reflex_google_auth.google_oauth_provider`
|
108
|
+
component and the `on_click` triggers
|
109
|
+
`reflex_google_auth.handle_google_login()`. Note that this cannot be combined
|
110
|
+
with other event handlers.
|
111
|
+
|
112
|
+
This functionality is also exposed in the `require_google_auth` decorator, which
|
113
|
+
accepts a `button` keyword argument.
|
114
|
+
|
115
|
+
When using a custom button, the returned auth-code _must be validated on the
|
116
|
+
backend_, which is handled by this library, but **requires additionally setting
|
117
|
+
`GOOGLE_CLIENT_SECRET` and `GOOGLE_REDIRECT_URI` environment variables**. These
|
118
|
+
can be configured in the Google Cloud Console as described above.
|
119
|
+
|
120
|
+
```python
|
121
|
+
from reflex_google_auth import handle_google_login, require_google_login, GoogleAuthState
|
122
|
+
|
123
|
+
|
124
|
+
@require_google_login(button=rx.button("Google Login 🚀", on_click=handle_google_login()))
|
125
|
+
def custom_button() -> rx.Component:
|
126
|
+
return rx.vstack(
|
127
|
+
f"{GoogleAuthState.tokeninfo['email']} clicked a custom button to login, nice",
|
128
|
+
)
|
129
|
+
```
|
@@ -1,11 +1,20 @@
|
|
1
|
+
.gitignore
|
1
2
|
README.md
|
2
3
|
pyproject.toml
|
4
|
+
.github/workflows/publish.yml
|
3
5
|
custom_components/reflex_google_auth/__init__.py
|
4
6
|
custom_components/reflex_google_auth/decorator.py
|
5
7
|
custom_components/reflex_google_auth/google_auth.py
|
8
|
+
custom_components/reflex_google_auth/google_auth.pyi
|
6
9
|
custom_components/reflex_google_auth/state.py
|
7
10
|
custom_components/reflex_google_auth.egg-info/PKG-INFO
|
8
11
|
custom_components/reflex_google_auth.egg-info/SOURCES.txt
|
9
12
|
custom_components/reflex_google_auth.egg-info/dependency_links.txt
|
10
13
|
custom_components/reflex_google_auth.egg-info/requires.txt
|
11
|
-
custom_components/reflex_google_auth.egg-info/top_level.txt
|
14
|
+
custom_components/reflex_google_auth.egg-info/top_level.txt
|
15
|
+
google_auth_demo/.gitignore
|
16
|
+
google_auth_demo/requirements.txt
|
17
|
+
google_auth_demo/rxconfig.py
|
18
|
+
google_auth_demo/assets/favicon.ico
|
19
|
+
google_auth_demo/google_auth_demo/__init__.py
|
20
|
+
google_auth_demo/google_auth_demo/google_auth_demo.py
|
Binary file
|
File without changes
|
@@ -0,0 +1,84 @@
|
|
1
|
+
import reflex as rx
|
2
|
+
|
3
|
+
from reflex_google_auth import (
|
4
|
+
GoogleAuthState,
|
5
|
+
handle_google_login,
|
6
|
+
require_google_login,
|
7
|
+
)
|
8
|
+
|
9
|
+
|
10
|
+
class State(GoogleAuthState):
|
11
|
+
@rx.var(cache=True)
|
12
|
+
def protected_content(self) -> str:
|
13
|
+
if self.token_is_valid:
|
14
|
+
return f"This content can only be viewed by a logged in User. Nice to see you {self.tokeninfo['name']}"
|
15
|
+
return "Not logged in."
|
16
|
+
|
17
|
+
|
18
|
+
def user_info(tokeninfo: rx.vars.ObjectVar) -> rx.Component:
|
19
|
+
return rx.hstack(
|
20
|
+
rx.avatar(
|
21
|
+
src=tokeninfo["picture"],
|
22
|
+
fallback=tokeninfo["name"],
|
23
|
+
size="5",
|
24
|
+
),
|
25
|
+
rx.vstack(
|
26
|
+
rx.heading(tokeninfo["name"], size="6"),
|
27
|
+
rx.text(tokeninfo["email"]),
|
28
|
+
align_items="flex-start",
|
29
|
+
),
|
30
|
+
rx.button("Logout", on_click=GoogleAuthState.logout),
|
31
|
+
padding="10px",
|
32
|
+
)
|
33
|
+
|
34
|
+
|
35
|
+
def index():
|
36
|
+
return rx.vstack(
|
37
|
+
rx.heading("Google OAuth", size="8"),
|
38
|
+
rx.link("Protected Page", href="/protected"),
|
39
|
+
rx.link("Partially Protected Page", href="/partially-protected"),
|
40
|
+
rx.link("Custom Login Button", href="/custom-button"),
|
41
|
+
align="center",
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
@rx.page(route="/protected")
|
46
|
+
@require_google_login
|
47
|
+
def protected() -> rx.Component:
|
48
|
+
return rx.vstack(
|
49
|
+
user_info(GoogleAuthState.tokeninfo),
|
50
|
+
rx.text(State.protected_content),
|
51
|
+
rx.link("Home", href="/"),
|
52
|
+
)
|
53
|
+
|
54
|
+
|
55
|
+
@require_google_login
|
56
|
+
def user_name_or_sign_in() -> rx.Component:
|
57
|
+
return rx.heading(GoogleAuthState.tokeninfo["name"], size="6")
|
58
|
+
|
59
|
+
|
60
|
+
@rx.page(route="/partially-protected")
|
61
|
+
def partially_protected() -> rx.Component:
|
62
|
+
return rx.vstack(
|
63
|
+
rx.heading("This page is partially protected."),
|
64
|
+
rx.text(
|
65
|
+
"If you are signed in with google, you should see your name below, otherwise "
|
66
|
+
"you will see a sign in button",
|
67
|
+
),
|
68
|
+
user_name_or_sign_in(),
|
69
|
+
)
|
70
|
+
|
71
|
+
|
72
|
+
@rx.page(route="/custom-button")
|
73
|
+
@require_google_login(
|
74
|
+
button=rx.button("Google Login 🚀", on_click=handle_google_login())
|
75
|
+
)
|
76
|
+
def custom_button() -> rx.Component:
|
77
|
+
return rx.vstack(
|
78
|
+
user_info(GoogleAuthState.tokeninfo),
|
79
|
+
"You clicked a custom button to login, nice",
|
80
|
+
)
|
81
|
+
|
82
|
+
|
83
|
+
app = rx.App()
|
84
|
+
app.add_page(index)
|
@@ -1,13 +1,13 @@
|
|
1
1
|
[build-system]
|
2
2
|
requires = [
|
3
|
-
"setuptools",
|
3
|
+
"setuptools>=64",
|
4
|
+
"setuptools_scm>=8",
|
4
5
|
"wheel",
|
5
6
|
]
|
6
7
|
build-backend = "setuptools.build_meta"
|
7
8
|
|
8
9
|
[project]
|
9
10
|
name = "reflex-google-auth"
|
10
|
-
version = "0.0.7"
|
11
11
|
description = "Sign in with Google"
|
12
12
|
readme = "README.md"
|
13
13
|
license = { text = "Apache-2.0" }
|
@@ -16,13 +16,14 @@ authors = [{ name = "Masen Furer", email = "m_github@0x26.net" }]
|
|
16
16
|
keywords = ["reflex", "reflex-custom-components"]
|
17
17
|
|
18
18
|
dependencies = [
|
19
|
-
"reflex>=0.
|
19
|
+
"reflex>=0.6.6",
|
20
20
|
"google-auth[requests]",
|
21
21
|
]
|
22
22
|
|
23
23
|
classifiers = [
|
24
24
|
"Development Status :: 4 - Beta",
|
25
25
|
]
|
26
|
+
dynamic = ["version"]
|
26
27
|
|
27
28
|
[project.urls]
|
28
29
|
homepage = "https://github.com/masenf/reflex-google-auth"
|
@@ -32,3 +33,5 @@ dev = ["build", "twine"]
|
|
32
33
|
|
33
34
|
[tool.setuptools.packages.find]
|
34
35
|
where = ["custom_components"]
|
36
|
+
|
37
|
+
[tool.setuptools_scm]
|
@@ -1,25 +0,0 @@
|
|
1
|
-
import functools
|
2
|
-
|
3
|
-
import reflex as rx
|
4
|
-
|
5
|
-
from . import google_auth
|
6
|
-
from .state import GoogleAuthState
|
7
|
-
|
8
|
-
|
9
|
-
def require_google_login(page) -> rx.Component:
|
10
|
-
@functools.wraps(page)
|
11
|
-
def _auth_wrapper() -> rx.Component:
|
12
|
-
return google_auth.google_oauth_provider(
|
13
|
-
rx.cond(
|
14
|
-
rx.State.is_hydrated,
|
15
|
-
rx.cond(
|
16
|
-
GoogleAuthState.token_is_valid,
|
17
|
-
page(),
|
18
|
-
google_auth.google_login(),
|
19
|
-
),
|
20
|
-
),
|
21
|
-
)
|
22
|
-
|
23
|
-
_auth_wrapper.__name__ = page.__name__
|
24
|
-
|
25
|
-
return _auth_wrapper
|
@@ -1,37 +0,0 @@
|
|
1
|
-
import reflex as rx
|
2
|
-
|
3
|
-
from .state import GoogleAuthState
|
4
|
-
|
5
|
-
|
6
|
-
class GoogleOAuthProvider(rx.Component):
|
7
|
-
library = "@react-oauth/google"
|
8
|
-
tag = "GoogleOAuthProvider"
|
9
|
-
|
10
|
-
client_id: rx.Var[str]
|
11
|
-
|
12
|
-
@classmethod
|
13
|
-
def create(cls, *children, **props) -> rx.Component:
|
14
|
-
props.setdefault("client_id", GoogleAuthState.client_id)
|
15
|
-
return super().create(*children, **props)
|
16
|
-
|
17
|
-
|
18
|
-
google_oauth_provider = GoogleOAuthProvider.create
|
19
|
-
|
20
|
-
|
21
|
-
def _on_success_signature(data: rx.Var[dict]) -> tuple[rx.Var[dict]]:
|
22
|
-
return data,
|
23
|
-
|
24
|
-
|
25
|
-
class GoogleLogin(rx.Component):
|
26
|
-
library = "@react-oauth/google"
|
27
|
-
tag = "GoogleLogin"
|
28
|
-
|
29
|
-
on_success: rx.EventHandler[_on_success_signature]
|
30
|
-
|
31
|
-
@classmethod
|
32
|
-
def create(cls, **props) -> "GoogleLogin":
|
33
|
-
props.setdefault("on_success", GoogleAuthState.on_success)
|
34
|
-
return super().create(**props)
|
35
|
-
|
36
|
-
|
37
|
-
google_login = GoogleLogin.create
|
File without changes
|
File without changes
|
File without changes
|