-
-
Notifications
You must be signed in to change notification settings - Fork 613
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1676 from zauberzeug/descope_auth
Example: Auth and User Management with Descope
- Loading branch information
Showing
5 changed files
with
130 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Descope Auth Example | ||
|
||
Descope is an all-inclusive user authentication and user management platform. | ||
|
||
## Getting Started | ||
|
||
1. Create a [Descope](descope.com) account. | ||
2. Setup a project and configure the "Login Flow" (fist step in the Getting Started Wizard). | ||
3. Instead of following the "Integrate" instructions of the Wizard, use this example. | ||
4. Provide your Descope Project ID as environment variable: `DESCOPE_PROJECT_ID=<your_project_id> python3 main.py` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
#!/usr/bin/env python3 | ||
import json | ||
|
||
import user | ||
|
||
from nicegui import ui | ||
|
||
|
||
@user.login_page | ||
def login(): | ||
user.login_form().on('success', lambda: ui.open('/')) | ||
|
||
|
||
@user.page('/') | ||
def home(): | ||
ui.code(json.dumps(user.about(), indent=2), language='json') | ||
ui.button('Logout', on_click=user.logout) | ||
|
||
|
||
ui.run(storage_secret='THIS_NEEDS_TO_BE_CHANGED') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
descope |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import logging | ||
import os | ||
from typing import Any, Callable, Dict | ||
|
||
from descope import AuthException, DescopeClient | ||
|
||
from nicegui import Client, app, ui | ||
|
||
_descope_id = os.environ.get('DESCOPE_PROJECT_ID', '') | ||
|
||
try: | ||
descope_client = DescopeClient(project_id=_descope_id) | ||
except AuthException as ex: | ||
print(ex.error_message) | ||
|
||
|
||
def login_form() -> ui.element: | ||
"""Places and returns the Descope login form.""" | ||
with ui.card().classes('w-96 mx-auto'): | ||
return ui.element('descope-wc').props(f'project-id="{_descope_id}" flow-id="sign-up-or-in"') \ | ||
.on('success', lambda e: app.storage.user.update({'descope': e.args['detail']['user']})) | ||
|
||
|
||
def about() -> Dict[str, Any]: | ||
"""Returns the user's Descope profile.""" | ||
infos = app.storage.user['descope'] | ||
if not infos: | ||
raise PermissionError('User is not logged in.') | ||
return infos | ||
|
||
|
||
async def logout() -> None: | ||
"""Logs the user out.""" | ||
result = await ui.run_javascript('return await sdk.logout()', respond=True) | ||
if result['code'] == 200: | ||
app.storage.user['descope'] = None | ||
else: | ||
logging.error(f'Logout failed: {result}') | ||
ui.notify('Logout failed', type='negative') | ||
ui.open(page.LOGIN_PATH) | ||
|
||
|
||
class page(ui.page): | ||
"""A page that requires the user to be logged in. | ||
It allows the same parameters as ui.page, but adds a login check. | ||
As recommended by Descope, this is done via JavaScript and allows to use Flows. | ||
But this means that the page has already awaited the client connection. | ||
So `ui.add_head_html` will not work. | ||
""" | ||
SESSION_TOKEN_REFRESH_INTERVAL = 30 | ||
LOGIN_PATH = '/login' | ||
|
||
def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]: | ||
async def content(client: Client): | ||
ui.add_head_html('<script src="https://unpkg.com/@descope/web-component@latest/dist/index.js"></script>') | ||
ui.add_head_html('<script src="https://unpkg.com/@descope/web-js-sdk@latest/dist/index.umd.js"></script>') | ||
ui.add_body_html(f''' | ||
<script> | ||
const sdk = Descope({{ projectId: '{_descope_id}', persistTokens: true, autoRefresh: true }}); | ||
const sessionToken = sdk.getSessionToken() | ||
</script> | ||
''') | ||
await client.connected() | ||
token = await ui.run_javascript('return sessionToken && !sdk.isJwtExpired(sessionToken) ? sessionToken : null;') | ||
if token and self._verify(token): | ||
if self.path == self.LOGIN_PATH: | ||
await self._refresh() | ||
ui.open('/') | ||
else: | ||
func() | ||
else: | ||
if self.path != self.LOGIN_PATH: | ||
ui.open(self.LOGIN_PATH) | ||
else: | ||
ui.timer(self.SESSION_TOKEN_REFRESH_INTERVAL, self._refresh) | ||
func() | ||
|
||
return super().__call__(content) | ||
|
||
@staticmethod | ||
def _verify(token: str) -> bool: | ||
try: | ||
descope_client.validate_session(session_token=token) | ||
return True | ||
except AuthException: | ||
logging.exception('Could not validate user session.') | ||
ui.notify('Wrong username or password', type='negative') | ||
return False | ||
|
||
@staticmethod | ||
async def _refresh() -> None: | ||
await ui.run_javascript('sdk.refresh()', respond=False) | ||
|
||
|
||
def login_page(func: Callable[..., Any]) -> Callable[..., Any]: | ||
"""Marks the special page that will contain the login form.""" | ||
return page(page.LOGIN_PATH)(func) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters