diff --git a/app/config/development.default.py b/app/config/development.default.py index 38778168461bb8604be279c5418aa875cb45839a..d0dea0f686c359cd7f3581f15d2c7efa7c74f746 100644 --- a/app/config/development.default.py +++ b/app/config/development.default.py @@ -17,16 +17,17 @@ SERVICE_CONFIG = { 'app_secret': 'x-blocpower-app-secret'}, 'urls': { 'app': 'http://dev.appservice.blocpower.io/', - 'user': 'http://dev.userservice.blocpower.io/', } } # AppService APP_CACHE_EXPIRY = 60 * 60 # One hour. -# UserService -GOOGLE_AUTH_HEADER = 'x-blocpower-google-token' -USER_CACHE_EXPIRY = 60 * 60 # One Hour. +# Auth0 Authentication +AUTH0_AUTH_HEADER = 'x-blocpower-auth0-token' +AUTH0_DOMAIN = os.environ['AUTH0_DOMAIN'] +AUTH0_AUDIENCE = os.environ['AUTH0_AUDIENCE'] +AUTH0_CLAIMS_NAMESPACE = os.environ['AUTH0_CLAIMS_NAMESPACE'] # Blocpower auth information. HEADER_AUTH_KEY = 'x-blocpower-auth-key' diff --git a/app/config/local.default.py b/app/config/local.default.py index fa44d084dbb6462900b087ecb5345b31deee1112..5bb202a605cd2c08ca79f23e2add7f7a06315b29 100644 --- a/app/config/local.default.py +++ b/app/config/local.default.py @@ -17,15 +17,17 @@ SERVICE_CONFIG = { 'app_secret': 'x-blocpower-app-secret'}, 'urls': { 'app': os.environ['APP_SERVICE'], - 'user': os.environ['USER_SERVICE']} + } } # AppService APP_CACHE_EXPIRY = 60 * 60 # One hour. -# UserService -GOOGLE_AUTH_HEADER = 'x-blocpower-google-token' -USER_CACHE_EXPIRY = 60 * 60 # One Hour. +# Auth0 Authentication +AUTH0_AUTH_HEADER = 'x-blocpower-auth0-token' +AUTH0_DOMAIN = '$AUTH0_DOMAIN' +AUTH0_AUDIENCE = '$AUTH0_AUDIENCE' +AUTH0_CLAIMS_NAMESPACE = '$AUTH0_CLAIMS_NAMESPACE' # Blocpower auth information. HEADER_AUTH_KEY = 'x-blocpower-auth-key' 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/production.default.py b/app/config/production.default.py index 0e23120a29bce9e4d71621f269a052450fd0df9e..6f333164a6f203dcbf89a52e9f6be0f94ee1ff98 100644 --- a/app/config/production.default.py +++ b/app/config/production.default.py @@ -17,16 +17,17 @@ SERVICE_CONFIG = { 'app_secret': 'x-blocpower-app-secret'}, 'urls': { 'app': 'http://app.s.blocpower.us', - 'user': 'http://user.s.blocpower.us', } } # AppService APP_CACHE_EXPIRY = 60 * 60 # One hour. -# UserService -GOOGLE_AUTH_HEADER = 'x-blocpower-google-token' -USER_CACHE_EXPIRY = 60 * 60 # One Hour. +# Auth0 Authentication +AUTH0_AUTH_HEADER = 'x-blocpower-auth0-token' +AUTH0_DOMAIN = os.environ['AUTH0_DOMAIN'] +AUTH0_AUDIENCE = os.environ['AUTH0_AUDIENCE'] +AUTH0_CLAIMS_NAMESPACE = os.environ['AUTH0_CLAIMS_NAMESPACE'] # Blocpower auth information. HEADER_AUTH_KEY = 'x-blocpower-auth-key' diff --git a/app/config/staging.default.py b/app/config/staging.default.py index 04bbebf0f410510f053887e1b65de27e2e772e4b..e3e3fc845ef15c3e4b83e6d78f493f80cd48e941 100644 --- a/app/config/staging.default.py +++ b/app/config/staging.default.py @@ -17,16 +17,17 @@ SERVICE_CONFIG = { 'app_secret': 'x-blocpower-app-secret'}, 'urls': { 'app': 'http://staging.app.s.blocpower.us/', - 'user': 'http://staging.user.s.blocpower.us/', } } # AppService APP_CACHE_EXPIRY = 60 * 60 # One hour. -# UserService -GOOGLE_AUTH_HEADER = 'x-blocpower-google-token' -USER_CACHE_EXPIRY = 60 * 60 # One Hour. +# Auth0 Authentication +AUTH0_AUTH_HEADER = 'x-blocpower-auth0-token' +AUTH0_DOMAIN = os.environ['AUTH0_DOMAIN'] +AUTH0_AUDIENCE = os.environ['AUTH0_AUDIENCE'] +AUTH0_CLAIMS_NAMESPACE = os.environ['AUTH0_CLAIMS_NAMESPACE'] # Blocpower auth information. HEADER_AUTH_KEY = 'x-blocpower-auth-key' diff --git a/app/config/test.default.py b/app/config/test.default.py index 6a75e784933d834ef7b70f9f7a47cd136599a01b..2a2795c4dcab65b27a3f7b22f02b32038df7c742 100644 --- a/app/config/test.default.py +++ b/app/config/test.default.py @@ -19,14 +19,18 @@ SERVICE_CONFIG = { 'urls': { 'app': os.environ['APP_SERVICE'], 'user': os.environ['USER_SERVICE']} + 'app': 'http://127.0.0.1:5400', + } } # App APP_CACHE_EXPIRY = 60 * 60 # One hour. -# Auth -GOOGLE_AUTH_HEADER = 'x-blocpower-google-token' -USER_CACHE_EXPIRY = 60 * 60 # One Hour. +# Auth0 Authentication +AUTH0_AUTH_HEADER = 'x-blocpower-auth0-token' +AUTH0_DOMAIN = os.environ['AUTH0_DOMAIN'] +AUTH0_AUDIENCE = os.environ['AUTH0_AUDIENCE'] +AUTH0_CLAIMS_NAMESPACE = os.environ['AUTH0_CLAIMS_NAMESPACE'] # Blocpower auth information. HEADER_AUTH_KEY = 'x-blocpower-auth-key' 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 414dea9fc2eec7e0e93cb2f8e550a9284263f1b5..1d372b124cd347d5d8c7b9db5a9559179068560e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,6 +29,7 @@ pyasn1==0.1.9 pyasn1-modules==0.0.8 pycparser==2.14 python-dateutil==2.4.2 +python-jose==1.3.2 PyYAML==3.11 redis==2.10.5 requests==2.7