def cancel_session_token(request, context): authenticated_user = authenticate_user(request, context) utils.validate_not_empty(request, 'session_token') # Retrieve the session details from the database try: session = get_session_from_token(request.session_token) except DatabaseObjectNotFound: abort(grpc.StatusCode.PERMISSION_DENIED, 'Session token is invalid or has expired') # Verify that the caller is the authorized renewer for the token # (The renewer can also cancel the token) if session.renewer != authenticated_user: abort(grpc.StatusCode.PERMISSION_DENIED, f'Unauthorized renewer: {authenticated_user}') # Cancel the token session.delete() # Create and return RPC response response = broker_pb2.CancelSessionTokenResponse() return response, { 'owner': session.owner, 'renewer': session.renewer, 'session-id': session.id }
def validate_scope(scope): # Check if the given scope is whitelisted scope_set = set(scope.split(',')) whitelist = set(settings.SCOPE_WHITELIST.split(',')) if not scope_set.issubset(whitelist): abort(grpc.StatusCode.PERMISSION_DENIED, f'`{scope}` is not a whitelisted scope')
def get_access_token(self, owner, scope): google_identity = self.get_google_identity(owner) try: refresh_token = RefreshToken.get(google_identity) except DatabaseObjectNotFound: # The user has not authorized the broker yet abort(grpc.StatusCode.PERMISSION_DENIED, self.AUTHZ_ERROR_MESSAGE.format(owner)) oauthsession, client_config = session_from_client_secrets_file( settings.CLIENT_SECRET_PATH, scopes=scope.split(',')) decrypted_value = encryption.decrypt( settings.ENCRYPTION_REFRESH_TOKEN_CRYPTO_KEY, refresh_token.value) try: access_token = oauthsession.refresh_token( token_url='https://oauth2.googleapis.com/token', client_id=client_config['web']['client_id'], client_secret=client_config['web']['client_secret'], refresh_token=decrypted_value) except InvalidGrantError: # The refresh token has expired or has been revoked abort(grpc.StatusCode.PERMISSION_DENIED, self.AUTHZ_ERROR_MESSAGE.format(owner)) return { 'access_token': access_token['access_token'], 'expires_at': self.calculate_expiry_time(access_token['expires_in']) }
def renew_session_token(request, context): authenticated_user = authenticate_user(request, context) utils.validate_not_empty(request, 'session_token') # Retrieve the session details from the database try: session = get_session_from_token(request.session_token) except DatabaseObjectNotFound: abort(grpc.StatusCode.PERMISSION_DENIED, 'Session token is invalid or has expired') # Verify that the caller is the authorized renewer for the token if session.renewer != authenticated_user: abort(grpc.StatusCode.PERMISSION_DENIED, f'Unauthorized renewer: {authenticated_user}') # Extends session's lifetime session.extend_lifetime() session.save() # Create and return RPC response response = broker_pb2.RenewSessionTokenResponse() response.expires_at = session.expires_at return response, { 'owner': session.owner, 'renewer': session.renewer, 'session-id': session.id }
def read_session_token(token: str) -> (str, bytes): try: header, encrypted_password = token.encode('ascii').split( TOKEN_SEPARATOR) except (ValueError): abort(grpc.StatusCode.UNAUTHENTICATED, f'Invalid session token') header = base64.urlsafe_b64decode(header).decode('ascii') session_id = json.loads(header)['session_id'] encrypted_password = base64.urlsafe_b64decode(encrypted_password) return session_id, encrypted_password
def _get_spnego_token(self, context): # Extract the client-supplied SPNEGO token from the "Authorization" header # provided in the gRPC request metadata metadata = context.invocation_metadata() header = dict(metadata).get("authorization") if header is None or not header.startswith('Negotiate '): abort( grpc.StatusCode.UNAUTHENTICATED, 'Use "authorization: Negotiate <token>" metadata to authenticate' ) spnego_token = header.split()[1] return spnego_token
def authenticate_session(context) -> None: metadata = context.invocation_metadata() header = dict(metadata).get("authorization") if header is None or not header.startswith('BrokerSession '): return None else: token = header.split()[1] session = get_session_from_token(token) if session.is_expired(): abort(grpc.StatusCode.UNIMPLEMENTED, f'Expired session ID: {session_id}') return session
def authenticate(self, request, context): spnego_token = self._get_spnego_token(context) gcssapi_context = None try: # Initialize GSSAPI context result, gcssapi_context = kerberos.authGSSServerInit( f'{settings.BROKER_SERVICE_NAME}@{settings.BROKER_SERVICE_HOSTNAME}' ) if result != kerberos.AUTH_GSS_COMPLETE: # The GSSAPI context initialization failed abort(grpc.StatusCode.PERMISSION_DENIED) # Process the client-supplied SPNEGO token result = kerberos.authGSSServerStep(gcssapi_context, spnego_token) if result == kerberos.AUTH_GSS_COMPLETE: # Authentication succeded. Return the authenticated principal's username. principal = kerberos.authGSSServerUserName(gcssapi_context) return principal else: # Authentication failed abort(grpc.StatusCode.PERMISSION_DENIED) except kerberos.GSSError: abort(grpc.StatusCode.PERMISSION_DENIED) finally: # Destroy the GSSAPI context if gcssapi_context: kerberos.authGSSServerClean(gcssapi_context)
def validate_impersonator(impersonator, impersonated): whitelist = set(settings.PROXY_USER_WHITELIST.split(',')) if impersonator != impersonated and impersonator not in whitelist: abort(grpc.StatusCode.PERMISSION_DENIED, f'`{impersonator}` is not a whitelisted impersonator')
def validate_domain(user): try: common_utils.validate_domain(user) except ValueError: abort(grpc.StatusCode.PERMISSION_DENIED, f'Invalid domain for user: {user}')
def validate_not_empty(request, param): if getattr(request, param) == '': abort(grpc.StatusCode.INVALID_ARGUMENT, f'Request must provide the `{param}` parameter')
def get_access_token(request, context): session = authenticate_session(context) if session is not None: utils.validate_not_empty(request, 'owner') utils.validate_not_empty(request, 'scope') if request.target != session.target: abort(grpc.StatusCode.PERMISSION_DENIED, "Target mismatch") if request.owner not in [session.owner, session.owner.split('@')[0]]: abort(grpc.StatusCode.PERMISSION_DENIED, "Owner mismatch") if request.scope != session.scope: abort(grpc.StatusCode.PERMISSION_DENIED, "Scope mismatch") else: authenticated_user = authenticate_user(request, context) utils.validate_not_empty(request, 'owner') utils.validate_not_empty(request, 'scope') utils.validate_impersonator(authenticated_user, request.owner) utils.validate_scope(request.scope) # Create cache key to look up access token from cache cache_key = f'access-token-{request.owner}-{request.scope}' # First check in local cache local_cache = get_local_cache() access_token = local_cache.get(cache_key) if access_token is None: # Not found in local cache, so look in remote cache. cache = get_cache() encrypted_access_token = cache.get(cache_key) if encrypted_access_token is not None: # Cache hit... Let's load the value. access_token_json = encryption.decrypt( settings.ENCRYPTION_ACCESS_TOKEN_CACHE_CRYPTO_KEY, encrypted_access_token) access_token = json.loads(access_token_json) else: # Cache miss... Let's generate a new access token. # Start by acquiring a lock to avoid cache stampede lock = cache.acquire_lock(f'{cache_key}_lock') try: # Check again if there's still no value encrypted_access_token = cache.get(cache_key) if encrypted_access_token is not None: # This time it's a cache hit. The token must have been generated # by a competing thread. So we just load the value. access_token_json = encryption.decrypt( settings.ENCRYPTION_ACCESS_TOKEN_CACHE_CRYPTO_KEY, encrypted_access_token) access_token = json.loads(access_token_json) else: # Again a cache miss, so we must really generate a new token now. access_token = get_provider().get_access_token( request.owner, request.scope) # Cache token for possible future requests access_token_json = json.dumps(access_token) encrypted_value = encryption.encrypt( settings.ENCRYPTION_ACCESS_TOKEN_CACHE_CRYPTO_KEY, access_token_json) cache.set( cache_key, encrypted_value, timedelta( seconds=settings.ACCESS_TOKEN_REMOTE_CACHE_TIME)) finally: # Release the lock cache.release_lock(lock) # Add unencrypted token to local cache local_cache.set(cache_key, access_token, settings.ACCESS_TOKEN_LOCAL_CACHE_TIME) # Create and return RPC response response = broker_pb2.GetAccessTokenResponse() response.access_token = access_token['access_token'] response.expires_at = access_token['expires_at'] return response, {'owner': request.owner, 'scope': request.scope}
def get_session_from_token(token: str) -> Session: session_id, encrypted_password = read_session_token(token) session = Session.get(session_id) if not password_match(session, encrypted_password): abort(grpc.StatusCode.UNAUTHENTICATED, 'Invalid session token') return session