Skip to content

Commit

Permalink
Add auth via JWT providers (#2768)
Browse files Browse the repository at this point in the history
* authentication via JWT providers
* add support for IAP JWT auth
* remove jwt_auth Blueprint and /headers endpoint
* fix pep8: imports
  • Loading branch information
SakuradaJun authored and arikfr committed Sep 27, 2018
1 parent fa92fec commit de0089c
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 9 deletions.
61 changes: 53 additions & 8 deletions redash/authentication/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
import logging

from flask import redirect, request, jsonify, url_for
from werkzeug.exceptions import Unauthorized

from redash import models, settings
from redash.settings.organization import settings as org_settings
from redash.authentication import jwt_auth
from redash.authentication.org_resolving import current_org
from redash.tasks import record_event

Expand Down Expand Up @@ -48,6 +51,21 @@ def load_user(user_id):
return None


def request_loader(request):
user = None
if settings.AUTH_TYPE == 'hmac':
user = hmac_load_user_from_request(request)
elif settings.AUTH_TYPE == 'api_key':
user = api_key_load_user_from_request(request)
else:
logger.warning("Unknown authentication type ({}). Using default (HMAC).".format(settings.AUTH_TYPE))
user = hmac_load_user_from_request(request)

if org_settings['auth_jwt_login_enabled'] and user is None:
user = jwt_token_load_user_from_request(request)
return user


def hmac_load_user_from_request(request):
signature = request.args.get('signature')
expires = float(request.args.get('expires') or 0)
Expand Down Expand Up @@ -116,6 +134,40 @@ def api_key_load_user_from_request(request):
return user


def jwt_token_load_user_from_request(request):
org = current_org._get_current_object()

payload = None

if org_settings['auth_jwt_auth_cookie_name']:
jwt_token = request.cookies.get(org_settings['auth_jwt_auth_cookie_name'], None)
elif org_settings['auth_jwt_auth_header_name']:
jwt_token = request.headers.get(org_settings['auth_jwt_auth_header_name'], None)
else:
return None

if jwt_token:
payload, token_is_valid = jwt_auth.verify_jwt_token(
jwt_token,
expected_issuer=org_settings['auth_jwt_auth_issuer'],
expected_audience=org_settings['auth_jwt_auth_audience'],
algorithms=org_settings['auth_jwt_auth_algorithms'],
public_certs_url=org_settings['auth_jwt_auth_public_certs_url'],
)
if not token_is_valid:
raise Unauthorized('Invalid JWT token')

if not payload:
return

try:
user = models.User.get_by_email_and_org(payload['email'], org)
except models.NoResultFound:
user = create_and_login_user(current_org, payload['email'], payload['email'])

return user


def log_user_logged_in(app, user):
event = {
'org_id': current_org.id,
Expand Down Expand Up @@ -168,14 +220,7 @@ def setup_authentication(app):
app.register_blueprint(ldap_auth.blueprint)

user_logged_in.connect(log_user_logged_in)

if settings.AUTH_TYPE == 'hmac':
login_manager.request_loader(hmac_load_user_from_request)
elif settings.AUTH_TYPE == 'api_key':
login_manager.request_loader(api_key_load_user_from_request)
else:
logger.warning("Unknown authentication type ({}). Using default (HMAC).".format(settings.AUTH_TYPE))
login_manager.request_loader(hmac_load_user_from_request)
login_manager.request_loader(request_loader)


def create_and_login_user(org, name, email, picture=None):
Expand Down
65 changes: 65 additions & 0 deletions redash/authentication/jwt_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import logging
import json
import jwt
import requests

logger = logging.getLogger('jwt_auth')


def get_public_keys(url):
"""
Returns:
List of RSA public keys usable by PyJWT.
"""
key_cache = get_public_keys.key_cache
if url in key_cache:
return key_cache[url]
else:
r = requests.get(url)
r.raise_for_status()
data = r.json()
if 'keys' in data:
public_keys = []
for key_dict in data['keys']:
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(key_dict))
public_keys.append(public_key)

get_public_keys.key_cache[url] = public_keys
return public_keys
else:
get_public_keys.key_cache[url] = data
return data


get_public_keys.key_cache = {}


def verify_jwt_token(jwt_token, expected_issuer, expected_audience, algorithms, public_certs_url):
# https://developers.cloudflare.com/access/setting-up-access/validate-jwt-tokens/
# https://cloud.google.com/iap/docs/signed-headers-howto
# Loop through the keys since we can't pass the key set to the decoder
keys = get_public_keys(public_certs_url)

key_id = jwt.get_unverified_header(jwt_token).get('kid', '')
if key_id and isinstance(keys, dict):
keys = [keys.get(key_id)]

valid_token = False
payload = None
for key in keys:
try:
# decode returns the claims which has the email if you need it
payload = jwt.decode(
jwt_token,
key=key,
audience=expected_audience,
algorithms=algorithms
)
issuer = payload['iss']
if issuer != expected_issuer:
raise Exception('Wrong issuer: {}'.format(issuer))
valid_token = True
break
except Exception as e:
logging.exception(e)
return payload, valid_token
17 changes: 16 additions & 1 deletion redash/settings/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,26 @@

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

JWT_LOGIN_ENABLED = parse_boolean(os.environ.get("REDASH_JWT_LOGIN_ENABLED", "false"))
JWT_AUTH_ISSUER = os.environ.get("REDASH_JWT_AUTH_ISSUER", "")
JWT_AUTH_PUBLIC_CERTS_URL = os.environ.get("REDASH_JWT_AUTH_PUBLIC_CERTS_URL", "")
JWT_AUTH_AUDIENCE = os.environ.get("REDASH_JWT_AUTH_AUDIENCE", "")
JWT_AUTH_ALGORITHMS = os.environ.get("REDASH_JWT_AUTH_ALGORITHMS", "HS256,RS256,ES256").split(',')
JWT_AUTH_COOKIE_NAME = os.environ.get("REDASH_JWT_AUTH_COOKIE_NAME", "")
JWT_AUTH_HEADER_NAME = os.environ.get("REDASH_JWT_AUTH_HEADER_NAME", "")

settings = {
"auth_password_login_enabled": PASSWORD_LOGIN_ENABLED,
"auth_saml_enabled": SAML_LOGIN_ENABLED,
"auth_saml_entity_id": SAML_ENTITY_ID,
"auth_saml_metadata_url": SAML_METADATA_URL,
"auth_saml_nameid_format": SAML_NAMEID_FORMAT,
"date_format": DATE_FORMAT
"date_format": DATE_FORMAT,
"auth_jwt_login_enabled": JWT_LOGIN_ENABLED,
"auth_jwt_auth_issuer": JWT_AUTH_ISSUER,
"auth_jwt_auth_public_certs_url": JWT_AUTH_PUBLIC_CERTS_URL,
"auth_jwt_auth_audience": JWT_AUTH_AUDIENCE,
"auth_jwt_auth_algorithms": JWT_AUTH_ALGORITHMS,
"auth_jwt_auth_cookie_name": JWT_AUTH_COOKIE_NAME,
"auth_jwt_auth_header_name": JWT_AUTH_HEADER_NAME,
}
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ semver==2.2.1
xlsxwriter==0.9.3
pystache==0.5.4
parsedatetime==2.1
PyJWT==1.6.4
cryptography==2.0.2
simplejson==3.10.0
ua-parser==0.7.3
Expand Down

0 comments on commit de0089c

Please sign in to comment.