Skip to content

Commit 86cab28

Browse files
authored
Add Flask basic authentication decorator and improve descope_validate_auth decorator to support roles (#172)
1 parent 14f8e88 commit 86cab28

File tree

2 files changed

+116
-2
lines changed

2 files changed

+116
-2
lines changed

samples/decorators/flask_decorators.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import datetime
22
import os
33
import sys
4+
import uuid
45
from functools import wraps
56

67
from flask import Response, _request_ctx_stack, redirect, request
78

89
from descope.descope_client import DescopeClient
10+
from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT
911

1012
dir_name = os.path.dirname(__file__)
1113
sys.path.insert(0, os.path.join(dir_name, "../"))
@@ -92,7 +94,7 @@ def decorated(*args, **kwargs):
9294
return decorator
9395

9496

95-
def descope_validate_auth(descope_client, permissions=[], tenant=""):
97+
def descope_validate_auth(descope_client, permissions=[], roles=[], tenant=""):
9698
"""
9799
Test if Access Token is valid
98100
"""
@@ -124,6 +126,17 @@ def decorated(*args, **kwargs):
124126
if not valid_permissions:
125127
return Response("Access denied", 401)
126128

129+
if roles:
130+
if tenant:
131+
valid_roles = descope_client.validate_tenant_roles(
132+
jwt_response, roles
133+
)
134+
else:
135+
valid_roles = descope_client.validate_roles(jwt_response, roles)
136+
137+
if not valid_roles:
138+
return Response("Access denied", 401)
139+
127140
# Save the claims on the context execute the original API
128141
_request_ctx_stack.top.claims = jwt_response
129142
response = f(*args, **kwargs)
@@ -369,6 +382,7 @@ def decorator(f):
369382
def decorated(*args, **kwargs):
370383
cookies = request.cookies.copy()
371384
refresh_token = cookies.get(REFRESH_SESSION_COOKIE_NAME)
385+
cookie_domain = request.headers.get("Host", "")
372386
try:
373387
descope_client.logout(refresh_token)
374388
except AuthException as e:
@@ -378,7 +392,9 @@ def decorated(*args, **kwargs):
378392
response = f(*args, **kwargs)
379393

380394
# Invalidate all cookies
381-
cookies.clear()
395+
if cookie_domain:
396+
response.delete_cookie(SESSION_COOKIE_NAME, "/", cookie_domain)
397+
response.delete_cookie(REFRESH_SESSION_COOKIE_NAME, "/", cookie_domain)
382398
return response
383399

384400
return decorated
@@ -410,3 +426,49 @@ def decorated(*args, **kwargs):
410426
return decorated
411427

412428
return decorator
429+
430+
431+
def descope_full_login(project_id, flow_id, success_redirect_url):
432+
"""
433+
Descope Flow login
434+
"""
435+
436+
def decorator(f):
437+
@wraps(f)
438+
def decorated(*args, **kwargs):
439+
id = f"descope-{uuid.uuid4()}"
440+
if not success_redirect_url:
441+
raise AuthException(
442+
500,
443+
ERROR_TYPE_INVALID_ARGUMENT,
444+
"Missing success_redirect_url parameter",
445+
)
446+
447+
html = f"""<!DOCTYPE html>
448+
<html lang="en">
449+
<head>
450+
<script src="https://unpkg.com/@descope/web-component/dist/index.js"></script>
451+
</head>
452+
453+
<body>
454+
<descope-wc id="{id}" project-id="{project_id}" flow-id="{flow_id}"></descope-wc>
455+
<script>
456+
const setCookie = (cookieName, cookieValue, maxAge, path, domain) => {{
457+
document.cookie = cookieName + '=' + cookieValue + ';max-age=' + maxAge + ';path=' + path + ';domain=' + domain + '; samesite=strict; secure;'
458+
}}
459+
const descopeWcEle = document.getElementById('{id}');
460+
descopeWcEle.addEventListener('success', async (e) => {{
461+
setCookie('{SESSION_COOKIE_NAME}', e.detail.sessionJwt, e.detail.cookieMaxAge, e.detail.cookiePath, e.detail.cookieDomain)
462+
setCookie('{REFRESH_SESSION_COOKIE_NAME}', e.detail.refreshJwt, e.detail.cookieMaxAge, e.detail.cookiePath, e.detail.cookieDomain)
463+
464+
document.location.replace("{success_redirect_url}")
465+
}});
466+
</script>
467+
</body>
468+
</html>"""
469+
f(*args, **kwargs)
470+
return html
471+
472+
return decorated
473+
474+
return decorator

samples/flask_authentication.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from decorators.flask_decorators import ( # noqa: E402;
2+
descope_full_login,
3+
descope_logout,
4+
descope_validate_auth,
5+
)
6+
from flask import Flask, Response
7+
8+
from descope import DescopeClient # noqa: E402
9+
10+
APP = Flask(__name__)
11+
PROJECT_ID = "" # Can be set also by environment variable
12+
13+
# init the DescopeClient
14+
descope_client = DescopeClient(PROJECT_ID)
15+
16+
17+
@APP.route("/login", methods=["GET"])
18+
@descope_full_login(
19+
project_id=PROJECT_ID,
20+
flow_id="sign-up-or-in",
21+
success_redirect_url="http://dev.localhost:9010/private",
22+
)
23+
def login():
24+
# Nothing to do! this is the MAGIC!
25+
pass
26+
27+
28+
# This needs authentication
29+
@APP.route("/private")
30+
@descope_validate_auth(
31+
descope_client
32+
) # Can add permissions=["Perm 1"], roles=["Role 1"], tenant="t1" conditions
33+
def private():
34+
return Response("<h1>Restricted page, authentication needed.</h1>")
35+
36+
37+
@APP.route("/logout")
38+
@descope_logout(descope_client)
39+
def logout():
40+
return Response("<h1>Goodbye, logged out.</h1>")
41+
42+
43+
# This doesn't need authentication
44+
@APP.route("/")
45+
def home():
46+
return Response("<h1>Hello, public page!</h1>")
47+
48+
49+
if __name__ == "__main__":
50+
APP.run(
51+
host="dev.localhost", port=9010
52+
) # cannot run on localhost as cookie will not work (just add it to your /etc/hosts file)

0 commit comments

Comments
 (0)