diff --git a/app/config/nginx.conf b/app/config/nginx.conf index 8bbb09a80aeaefed9e04da3fd61f19549bb6d016..27988b5a43f20da194ef6d6d28f064dbbd5603ef 100644 --- a/app/config/nginx.conf +++ b/app/config/nginx.conf @@ -28,7 +28,7 @@ http { add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Methods' 'HEAD, GET, POST, PUT, DELETE, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'x-blocpower-app-key,x-blocpower-app-secret,x-blocpower-app-token,x-blocpower-auth-key,x-blocpower-auth-token,x-blocpower-google-token' always; + add_header 'Access-Control-Allow-Headers' 'x-blocpower-app-key,x-blocpower-app-secret,x-blocpower-app-token,x-blocpower-auth-key,x-blocpower-auth-token,x-blocpower-auth0-token'' always; } } } diff --git a/app/config/test.default.py b/app/config/test.default.py index af250d790fb2af439f3006293fce73d7d23ece02..fb551018d15b15ac204df1429bee292f3dee876f 100644 --- a/app/config/test.default.py +++ b/app/config/test.default.py @@ -6,7 +6,6 @@ TESTING = True HEADER_APP_KEY = 'x-blocpower-app-key' HEADER_APP_SECRET = 'x-blocpower-app-secret' - # HACK Fix issue where raising an exception in a test context creates an extra # request. PRESERVE_CONTEXT_ON_EXCEPTION = False diff --git a/app/permissions/auth.py b/app/permissions/auth.py index 6b4b20c30799643325df1cf8b13bb05067c33db5..d6bbeafd89fb71dbf99e6e0e434b88499a19d1fd 100644 --- a/app/permissions/auth.py +++ b/app/permissions/auth.py @@ -1,5 +1,8 @@ """Permissions to check authentication.""" from werkzeug.exceptions import Unauthorized +from jose import jwt +import json +import requests from ..lib.red import redis from ..lib.service import services @@ -8,63 +11,67 @@ from .application import app_need class AuthNeed(Permission): - """Checks the user service for a valid login.""" - def is_met(self): - """Check for authentication with the user service.""" - from flask import session, request, current_app - # Check the cache. - key = session.get('user_key') - token = session.get('user_token') - cached_token = redis.users.get(key) - if ( - key and token and cached_token and - cached_token.decode('utf-8') == token - ): - return True + """Check if the access token is valid.""" - # Hit the user service. - google_header = current_app.config.get('GOOGLE_AUTH_HEADER') - google_token = request.headers.get(google_header) - response = services.user.get( - '/session/', headers={google_header: google_token}) + def __init__(self, required_permissions=[]): + self.required_permissions = required_permissions + super().__init__() - if not response.status_code == 200: - return False - data = response.json() - if 'data' not in data: - return False - data = data['data'] - if ( - 'user' not in data or - 'token' not in data or - 'key' not in data['user'] - ): - return False - - key = data['user']['key'] - token = data['token'] + def is_met(self): + from flask import session, request, current_app + """Check for authentication with Auth0.""" + auth0_header = current_app.config.get('AUTH0_AUTH_HEADER') + auth0_token = request.headers.get(auth0_header) - # Add to the cache and get the token from the cache. - # - # If there was already a token for this user (e.g from a different - # login), then we can trust that token, the command above did nothing, - # and we should respond with the cached token. - # - # These two are run atomically together to prevent the race condition - # where there is already a token and it expires before we retrieve it. - _, token = redis.users.pipeline()\ - .set(key, token, - ex=current_app.config.get('USER_CACHE_EXPIRY'), - nx=True)\ - .get(key)\ - .execute() + AUTH0_DOMAIN = current_app.config['AUTH0_DOMAIN'] + API_AUDIENCE = current_app.config['AUTH0_AUDIENCE'] + ALGORITHMS = ["RS256"] + r = requests.get("https://"+AUTH0_DOMAIN+"/.well-known/jwks.json") + jwks = json.loads(r.text) + unverified_header = jwt.get_unverified_header(auth0_token) + rsa_key = {} + for key in jwks["keys"]: + if key["kid"] == unverified_header["kid"]: + rsa_key = { + "kty": key["kty"], + "kid": key["kid"], + "use": key["use"], + "n": key["n"], + "e": key["e"] + } + if rsa_key: + try: + payload = jwt.decode( + auth0_token, + rsa_key, + algorithms=ALGORITHMS, + audience=API_AUDIENCE, + issuer="https://"+AUTH0_DOMAIN+"/" + ) + # For now we will print and return unauthorized. In the future + # we will log these errors and the requester + except jwt.ExpiredSignatureError: + print('Token is expired') + return False + except jwt.JWTClaimsError: + print('Incorrect claims. Please check the audience and the issuer') + return False + except Exception: + print('Invalid header. Unable to parse the token') + return False - # Add to the session. - session['user_key'] = key - session['user_token'] = token + # Check permissions + # The self.bool_ variable is a boolean if no value is passed in + CLAIMS_NAMESPACE = current_app.config['AUTH0_CLAIMS_NAMESPACE'] + actual_permissions = payload['{}permissions'.format(CLAIMS_NAMESPACE)] + for permission in self.required_permissions: + if permission not in actual_permissions: + return False + return True + return False - return True auth_need = AuthNeed() - - standard_login_need = app_need & auth_need +# An example permission need decorator. This decorator requires that the user +# has the 'view::developer' permission +developer_view_need = app_need & AuthNeed(['view::developer']) diff --git a/requirements.txt b/requirements.txt index 62c29acf9446d40c99b31357294f3778e9c063a0..dc392034a00f80407a95c931df97691a9418cb3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,7 @@ psycopg2==2.6.1 py==1.4.31 pycparser==2.14 python-dateutil==2.5.3 +python-jose==1.3.2 PyYAML==3.11 redis==2.10.5 requests==2.6.2