Skip to content

Commit 01404ac

Browse files
committed
2 parents 7aee02d + ea5160a commit 01404ac

File tree

3 files changed

+116
-27
lines changed

3 files changed

+116
-27
lines changed

app/docusign/ds_client.py

+85-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import uuid
2-
from os import path
2+
from os import path, urandom
3+
import hashlib
4+
import base64
5+
import secrets
36

47

58
import requests
6-
from flask import current_app as app, url_for, redirect, render_template, request, session
9+
from flask import current_app as app, url_for, redirect, render_template, request, session, redirect
710
from flask_oauthlib.client import OAuth
11+
from requests_oauthlib import OAuth2Session
812
from docusign_esign import ApiClient
913
from docusign_esign.client.api_exception import ApiException
1014

@@ -48,7 +52,10 @@ class DSClient:
4852
@classmethod
4953
def _init(cls, auth_type, api):
5054
if auth_type == "code_grant":
51-
cls._auth_code_grant(api)
55+
if session.get("pkce_failed", False):
56+
cls._auth_code_grant(api)
57+
else:
58+
cls._pkce_auth(api)
5259
elif auth_type == "jwt":
5360
cls._jwt_auth(api)
5461

@@ -92,6 +99,29 @@ def _auth_code_grant(cls, api):
9299
access_token_method="POST"
93100
)
94101

102+
@classmethod
103+
def _pkce_auth(cls, api):
104+
"""Authorize with the Authorization Code Grant - OAuth 2.0 flow"""
105+
use_scopes = []
106+
107+
if api == "Rooms":
108+
use_scopes.extend(ROOMS_SCOPES)
109+
elif api == "Click":
110+
use_scopes.extend(CLICK_SCOPES)
111+
elif api == "Admin":
112+
use_scopes.extend(ADMIN_SCOPES)
113+
elif api == "Maestro":
114+
use_scopes.extend(MAESTRO_SCOPES)
115+
elif api == "WebForms":
116+
use_scopes.extend(WEBFORMS_SCOPES)
117+
else:
118+
use_scopes.extend(SCOPES)
119+
# remove duplicate scopes
120+
use_scopes = list(set(use_scopes))
121+
122+
redirect_uri = DS_CONFIG["app_url"] + url_for("ds.ds_callback")
123+
cls.ds_app = OAuth2Session(DS_CONFIG["ds_client_id"], redirect_uri=redirect_uri, scope=use_scopes)
124+
95125
@classmethod
96126
def _jwt_auth(cls, api):
97127
"""JSON Web Token authorization"""
@@ -152,15 +182,24 @@ def destroy(cls):
152182
def login(cls, auth_type, api):
153183
cls._init(auth_type, api)
154184
if auth_type == "code_grant":
155-
return cls.get(auth_type, api).authorize(callback=url_for("ds.ds_callback", _external=True))
185+
if session.get("pkce_failed", False):
186+
return cls.get(auth_type, api).authorize(callback=url_for("ds.ds_callback", _external=True))
187+
else:
188+
code_verifier = cls.generate_code_verifier()
189+
code_challenge = cls.generate_code_challenge(code_verifier)
190+
session["code_verifier"] = code_verifier
191+
return redirect(cls.get_auth_url_with_pkce(code_challenge))
156192
elif auth_type == "jwt":
157193
return cls._jwt_auth(api)
158194

159195
@classmethod
160196
def get_token(cls, auth_type):
161197
resp = None
162198
if auth_type == "code_grant":
163-
resp = cls.get(auth_type).authorized_response()
199+
if session.get("pkce_failed", False):
200+
resp = cls.get(auth_type).authorized_response()
201+
else:
202+
return cls.fetch_token_with_pkce(request.url)
164203
elif auth_type == "jwt":
165204
resp = cls.get(auth_type).to_dict()
166205

@@ -189,3 +228,44 @@ def get(cls, auth_type, api=API_TYPE["ESIGNATURE"]):
189228
if not cls.ds_app:
190229
cls._init(auth_type, api)
191230
return cls.ds_app
231+
232+
@classmethod
233+
def generate_code_verifier(cls):
234+
# Generate a random 32-byte string and base64-url encode it
235+
return secrets.token_urlsafe(32)
236+
237+
@classmethod
238+
def generate_code_challenge(cls, code_verifier):
239+
# Hash the code verifier using SHA-256
240+
sha256_hash = hashlib.sha256(code_verifier.encode()).digest()
241+
242+
# Base64 encode the hash and make it URL safe
243+
base64_encoded = base64.urlsafe_b64encode(sha256_hash).decode().rstrip('=')
244+
245+
return base64_encoded
246+
247+
@classmethod
248+
def get_auth_url_with_pkce(cls, code_challenge):
249+
authorize_url = DS_CONFIG["authorization_server"] + "/oauth/auth"
250+
auth_url, state = cls.ds_app.authorization_url(
251+
authorize_url,
252+
code_challenge=code_challenge,
253+
code_challenge_method='S256', # PKCE uses SHA-256 hashing,
254+
approval_prompt="auto"
255+
)
256+
257+
return auth_url
258+
259+
@classmethod
260+
def fetch_token_with_pkce(cls, authorization_response):
261+
access_token_url = DS_CONFIG["authorization_server"] + "/oauth/token"
262+
token = cls.get("code_grant", session.get("api")).fetch_token(
263+
access_token_url,
264+
authorization_response=authorization_response,
265+
client_id=DS_CONFIG["ds_client_id"],
266+
client_secret=DS_CONFIG["ds_client_secret"],
267+
code_verifier=session["code_verifier"],
268+
code_challenge_method="S256"
269+
)
270+
271+
return token

app/docusign/views.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,14 @@ def ds_callback():
8686

8787
# Save the redirect eg if present
8888
redirect_url = session.pop("eg", None)
89-
resp = DSClient.get_token(session["auth_type"])
89+
try:
90+
resp = DSClient.get_token(session["auth_type"])
91+
except Exception as err:
92+
if session.get("pkce_failed", False):
93+
raise err
94+
95+
session["pkce_failed"] = True
96+
return redirect(url_for("ds.ds_login"))
9097

9198
# app.logger.info("Authenticated with DocuSign.")
9299
session["ds_access_token"] = resp["access_token"]

run.py

+23-21
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1-
#!flask/bin/python
2-
from app import app
3-
from flask_session import Session
4-
import os
5-
import sys
6-
7-
host = "0.0.0.0" if "--docker" in sys.argv else "localhost"
8-
port = int(os.environ.get("PORT", 3000))
9-
10-
if os.environ.get("DEBUG", False) == "True":
11-
app.config["DEBUG"] = True
12-
app.config['SESSION_TYPE'] = 'filesystem'
13-
sess = Session()
14-
sess.init_app(app)
15-
app.run(host=host, port=port, debug=True)
16-
else:
17-
app.config['SESSION_TYPE'] = 'filesystem'
18-
sess = Session()
19-
sess.init_app(app)
20-
app.run(host=host, port=port, extra_files="api_type.py")
21-
1+
#!flask/bin/python
2+
from app import app
3+
from flask_session import Session
4+
import os
5+
import sys
6+
7+
host = "0.0.0.0" if "--docker" in sys.argv else "localhost"
8+
port = int(os.environ.get("PORT", 3000))
9+
10+
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
11+
12+
if os.environ.get("DEBUG", False) == "True":
13+
app.config["DEBUG"] = True
14+
app.config['SESSION_TYPE'] = 'filesystem'
15+
sess = Session()
16+
sess.init_app(app)
17+
app.run(host=host, port=port, debug=True)
18+
else:
19+
app.config['SESSION_TYPE'] = 'filesystem'
20+
sess = Session()
21+
sess.init_app(app)
22+
app.run(host=host, port=port, extra_files="api_type.py")
23+

0 commit comments

Comments
 (0)