Skip to content

Commit

Permalink
Add Flask basic authentication decorator and improve descope_validate…
Browse files Browse the repository at this point in the history
…_auth decorator to support roles (#172)
  • Loading branch information
guyp-descope committed Apr 20, 2023
1 parent 14f8e88 commit 86cab28
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 2 deletions.
66 changes: 64 additions & 2 deletions samples/decorators/flask_decorators.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import datetime
import os
import sys
import uuid
from functools import wraps

from flask import Response, _request_ctx_stack, redirect, request

from descope.descope_client import DescopeClient
from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT

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


def descope_validate_auth(descope_client, permissions=[], tenant=""):
def descope_validate_auth(descope_client, permissions=[], roles=[], tenant=""):
"""
Test if Access Token is valid
"""
Expand Down Expand Up @@ -124,6 +126,17 @@ def decorated(*args, **kwargs):
if not valid_permissions:
return Response("Access denied", 401)

if roles:
if tenant:
valid_roles = descope_client.validate_tenant_roles(
jwt_response, roles
)
else:
valid_roles = descope_client.validate_roles(jwt_response, roles)

if not valid_roles:
return Response("Access denied", 401)

# Save the claims on the context execute the original API
_request_ctx_stack.top.claims = jwt_response
response = f(*args, **kwargs)
Expand Down Expand Up @@ -369,6 +382,7 @@ def decorator(f):
def decorated(*args, **kwargs):
cookies = request.cookies.copy()
refresh_token = cookies.get(REFRESH_SESSION_COOKIE_NAME)
cookie_domain = request.headers.get("Host", "")
try:
descope_client.logout(refresh_token)
except AuthException as e:
Expand All @@ -378,7 +392,9 @@ def decorated(*args, **kwargs):
response = f(*args, **kwargs)

# Invalidate all cookies
cookies.clear()
if cookie_domain:
response.delete_cookie(SESSION_COOKIE_NAME, "/", cookie_domain)
response.delete_cookie(REFRESH_SESSION_COOKIE_NAME, "/", cookie_domain)
return response

return decorated
Expand Down Expand Up @@ -410,3 +426,49 @@ def decorated(*args, **kwargs):
return decorated

return decorator


def descope_full_login(project_id, flow_id, success_redirect_url):
"""
Descope Flow login
"""

def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
id = f"descope-{uuid.uuid4()}"
if not success_redirect_url:
raise AuthException(
500,
ERROR_TYPE_INVALID_ARGUMENT,
"Missing success_redirect_url parameter",
)

html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://unpkg.com/@descope/web-component/dist/index.js"></script>
</head>
<body>
<descope-wc id="{id}" project-id="{project_id}" flow-id="{flow_id}"></descope-wc>
<script>
const setCookie = (cookieName, cookieValue, maxAge, path, domain) => {{
document.cookie = cookieName + '=' + cookieValue + ';max-age=' + maxAge + ';path=' + path + ';domain=' + domain + '; samesite=strict; secure;'
}}
const descopeWcEle = document.getElementById('{id}');
descopeWcEle.addEventListener('success', async (e) => {{
setCookie('{SESSION_COOKIE_NAME}', e.detail.sessionJwt, e.detail.cookieMaxAge, e.detail.cookiePath, e.detail.cookieDomain)
setCookie('{REFRESH_SESSION_COOKIE_NAME}', e.detail.refreshJwt, e.detail.cookieMaxAge, e.detail.cookiePath, e.detail.cookieDomain)
document.location.replace("{success_redirect_url}")
}});
</script>
</body>
</html>"""
f(*args, **kwargs)
return html

return decorated

return decorator
52 changes: 52 additions & 0 deletions samples/flask_authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from decorators.flask_decorators import ( # noqa: E402;
descope_full_login,
descope_logout,
descope_validate_auth,
)
from flask import Flask, Response

from descope import DescopeClient # noqa: E402

APP = Flask(__name__)
PROJECT_ID = "" # Can be set also by environment variable

# init the DescopeClient
descope_client = DescopeClient(PROJECT_ID)


@APP.route("/login", methods=["GET"])
@descope_full_login(
project_id=PROJECT_ID,
flow_id="sign-up-or-in",
success_redirect_url="http://dev.localhost:9010/private",
)
def login():
# Nothing to do! this is the MAGIC!
pass


# This needs authentication
@APP.route("/private")
@descope_validate_auth(
descope_client
) # Can add permissions=["Perm 1"], roles=["Role 1"], tenant="t1" conditions
def private():
return Response("<h1>Restricted page, authentication needed.</h1>")


@APP.route("/logout")
@descope_logout(descope_client)
def logout():
return Response("<h1>Goodbye, logged out.</h1>")


# This doesn't need authentication
@APP.route("/")
def home():
return Response("<h1>Hello, public page!</h1>")


if __name__ == "__main__":
APP.run(
host="dev.localhost", port=9010
) # cannot run on localhost as cookie will not work (just add it to your /etc/hosts file)

0 comments on commit 86cab28

Please sign in to comment.