Skip to content

Commit de0089c

Browse files
SakuradaJunarikfr
authored andcommitted
Add auth via JWT providers (#2768)
* authentication via JWT providers * add support for IAP JWT auth * remove jwt_auth Blueprint and /headers endpoint * fix pep8: imports
1 parent fa92fec commit de0089c

4 files changed

Lines changed: 135 additions & 9 deletions

File tree

redash/authentication/__init__.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
import logging
77

88
from flask import redirect, request, jsonify, url_for
9+
from werkzeug.exceptions import Unauthorized
910

1011
from redash import models, settings
12+
from redash.settings.organization import settings as org_settings
13+
from redash.authentication import jwt_auth
1114
from redash.authentication.org_resolving import current_org
1215
from redash.tasks import record_event
1316

@@ -48,6 +51,21 @@ def load_user(user_id):
4851
return None
4952

5053

54+
def request_loader(request):
55+
user = None
56+
if settings.AUTH_TYPE == 'hmac':
57+
user = hmac_load_user_from_request(request)
58+
elif settings.AUTH_TYPE == 'api_key':
59+
user = api_key_load_user_from_request(request)
60+
else:
61+
logger.warning("Unknown authentication type ({}). Using default (HMAC).".format(settings.AUTH_TYPE))
62+
user = hmac_load_user_from_request(request)
63+
64+
if org_settings['auth_jwt_login_enabled'] and user is None:
65+
user = jwt_token_load_user_from_request(request)
66+
return user
67+
68+
5169
def hmac_load_user_from_request(request):
5270
signature = request.args.get('signature')
5371
expires = float(request.args.get('expires') or 0)
@@ -116,6 +134,40 @@ def api_key_load_user_from_request(request):
116134
return user
117135

118136

137+
def jwt_token_load_user_from_request(request):
138+
org = current_org._get_current_object()
139+
140+
payload = None
141+
142+
if org_settings['auth_jwt_auth_cookie_name']:
143+
jwt_token = request.cookies.get(org_settings['auth_jwt_auth_cookie_name'], None)
144+
elif org_settings['auth_jwt_auth_header_name']:
145+
jwt_token = request.headers.get(org_settings['auth_jwt_auth_header_name'], None)
146+
else:
147+
return None
148+
149+
if jwt_token:
150+
payload, token_is_valid = jwt_auth.verify_jwt_token(
151+
jwt_token,
152+
expected_issuer=org_settings['auth_jwt_auth_issuer'],
153+
expected_audience=org_settings['auth_jwt_auth_audience'],
154+
algorithms=org_settings['auth_jwt_auth_algorithms'],
155+
public_certs_url=org_settings['auth_jwt_auth_public_certs_url'],
156+
)
157+
if not token_is_valid:
158+
raise Unauthorized('Invalid JWT token')
159+
160+
if not payload:
161+
return
162+
163+
try:
164+
user = models.User.get_by_email_and_org(payload['email'], org)
165+
except models.NoResultFound:
166+
user = create_and_login_user(current_org, payload['email'], payload['email'])
167+
168+
return user
169+
170+
119171
def log_user_logged_in(app, user):
120172
event = {
121173
'org_id': current_org.id,
@@ -168,14 +220,7 @@ def setup_authentication(app):
168220
app.register_blueprint(ldap_auth.blueprint)
169221

170222
user_logged_in.connect(log_user_logged_in)
171-
172-
if settings.AUTH_TYPE == 'hmac':
173-
login_manager.request_loader(hmac_load_user_from_request)
174-
elif settings.AUTH_TYPE == 'api_key':
175-
login_manager.request_loader(api_key_load_user_from_request)
176-
else:
177-
logger.warning("Unknown authentication type ({}). Using default (HMAC).".format(settings.AUTH_TYPE))
178-
login_manager.request_loader(hmac_load_user_from_request)
223+
login_manager.request_loader(request_loader)
179224

180225

181226
def create_and_login_user(org, name, email, picture=None):

redash/authentication/jwt_auth.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import logging
2+
import json
3+
import jwt
4+
import requests
5+
6+
logger = logging.getLogger('jwt_auth')
7+
8+
9+
def get_public_keys(url):
10+
"""
11+
Returns:
12+
List of RSA public keys usable by PyJWT.
13+
"""
14+
key_cache = get_public_keys.key_cache
15+
if url in key_cache:
16+
return key_cache[url]
17+
else:
18+
r = requests.get(url)
19+
r.raise_for_status()
20+
data = r.json()
21+
if 'keys' in data:
22+
public_keys = []
23+
for key_dict in data['keys']:
24+
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(key_dict))
25+
public_keys.append(public_key)
26+
27+
get_public_keys.key_cache[url] = public_keys
28+
return public_keys
29+
else:
30+
get_public_keys.key_cache[url] = data
31+
return data
32+
33+
34+
get_public_keys.key_cache = {}
35+
36+
37+
def verify_jwt_token(jwt_token, expected_issuer, expected_audience, algorithms, public_certs_url):
38+
# https://developers.cloudflare.com/access/setting-up-access/validate-jwt-tokens/
39+
# https://cloud.google.com/iap/docs/signed-headers-howto
40+
# Loop through the keys since we can't pass the key set to the decoder
41+
keys = get_public_keys(public_certs_url)
42+
43+
key_id = jwt.get_unverified_header(jwt_token).get('kid', '')
44+
if key_id and isinstance(keys, dict):
45+
keys = [keys.get(key_id)]
46+
47+
valid_token = False
48+
payload = None
49+
for key in keys:
50+
try:
51+
# decode returns the claims which has the email if you need it
52+
payload = jwt.decode(
53+
jwt_token,
54+
key=key,
55+
audience=expected_audience,
56+
algorithms=algorithms
57+
)
58+
issuer = payload['iss']
59+
if issuer != expected_issuer:
60+
raise Exception('Wrong issuer: {}'.format(issuer))
61+
valid_token = True
62+
break
63+
except Exception as e:
64+
logging.exception(e)
65+
return payload, valid_token

redash/settings/organization.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,26 @@
1717

1818
DATE_FORMAT = os.environ.get("REDASH_DATE_FORMAT", "DD/MM/YY")
1919

20+
JWT_LOGIN_ENABLED = parse_boolean(os.environ.get("REDASH_JWT_LOGIN_ENABLED", "false"))
21+
JWT_AUTH_ISSUER = os.environ.get("REDASH_JWT_AUTH_ISSUER", "")
22+
JWT_AUTH_PUBLIC_CERTS_URL = os.environ.get("REDASH_JWT_AUTH_PUBLIC_CERTS_URL", "")
23+
JWT_AUTH_AUDIENCE = os.environ.get("REDASH_JWT_AUTH_AUDIENCE", "")
24+
JWT_AUTH_ALGORITHMS = os.environ.get("REDASH_JWT_AUTH_ALGORITHMS", "HS256,RS256,ES256").split(',')
25+
JWT_AUTH_COOKIE_NAME = os.environ.get("REDASH_JWT_AUTH_COOKIE_NAME", "")
26+
JWT_AUTH_HEADER_NAME = os.environ.get("REDASH_JWT_AUTH_HEADER_NAME", "")
27+
2028
settings = {
2129
"auth_password_login_enabled": PASSWORD_LOGIN_ENABLED,
2230
"auth_saml_enabled": SAML_LOGIN_ENABLED,
2331
"auth_saml_entity_id": SAML_ENTITY_ID,
2432
"auth_saml_metadata_url": SAML_METADATA_URL,
2533
"auth_saml_nameid_format": SAML_NAMEID_FORMAT,
26-
"date_format": DATE_FORMAT
34+
"date_format": DATE_FORMAT,
35+
"auth_jwt_login_enabled": JWT_LOGIN_ENABLED,
36+
"auth_jwt_auth_issuer": JWT_AUTH_ISSUER,
37+
"auth_jwt_auth_public_certs_url": JWT_AUTH_PUBLIC_CERTS_URL,
38+
"auth_jwt_auth_audience": JWT_AUTH_AUDIENCE,
39+
"auth_jwt_auth_algorithms": JWT_AUTH_ALGORITHMS,
40+
"auth_jwt_auth_cookie_name": JWT_AUTH_COOKIE_NAME,
41+
"auth_jwt_auth_header_name": JWT_AUTH_HEADER_NAME,
2742
}

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ semver==2.2.1
4444
xlsxwriter==0.9.3
4545
pystache==0.5.4
4646
parsedatetime==2.1
47+
PyJWT==1.6.4
4748
cryptography==2.0.2
4849
simplejson==3.10.0
4950
ua-parser==0.7.3

0 commit comments

Comments
 (0)