Merge "OAuth2 support"
authorJon Zhang <rong.zhang@windriver.com>
Thu, 30 May 2024 08:44:55 +0000 (08:44 +0000)
committerGerrit Code Review <gerrit@o-ran-sc.org>
Thu, 30 May 2024 08:44:55 +0000 (08:44 +0000)
configs/o2app.conf
o2app/entrypoints/flask_application.py
o2common/authmw/authmiddleware.py
o2common/authmw/authprov.py
o2common/authmw/exceptions.py [new file with mode: 0644]
o2common/config/config.py
requirements.txt

index cf6bdfe..ad224bb 100644 (file)
@@ -5,16 +5,35 @@ ocloud_global_id = 4e24b97c-8c49-4c4f-b53e-3de5235a4e37
 smo_register_url = http://127.0.0.1:8090/register
 smo_token_data = smo_token_payload
 
+auth_provider = oauth2
+
+[OAUTH2]
+# support OAuth2.0
+
+# oauth2 token verify type: jwt or introspection
+oauth2_verify_type =
+# oauth2 public key
+oauth2_public_key =
+# oauth2 encryption asymmetric algorithm
+oauth2_algorithm =
+
+# oauth2 jwt token introspection endpoint, required if oauth2_verify_type = introspection
+oauth2_introspection_endpoint =
+# required if oauth2_verify_type = introspection
+oauth2_client_id =
+# required if oauth2_verify_type = introspection
+oauth2_client_secret =
+
 [OCLOUD]
-OS_AUTH_URL = 
-OS_USERNAME = 
-OS_PASSWORD = 
-API_HOST_EXTERNAL_FLOATING = 
+OS_AUTH_URL =
+OS_USERNAME =
+OS_PASSWORD =
+API_HOST_EXTERNAL_FLOATING =
 
 [API]
 # support native_k8sapi,sol018,sol018_helmcli
 # if the value is black, then native_k8sapi will set by default
-DMS_SUPPORT_PROFILES = 
+DMS_SUPPORT_PROFILES =
 
 [WATCHER]
 
index d55b50a..fb4b6a2 100644 (file)
@@ -22,7 +22,6 @@ from o2common.views.route_exception import configure_exception
 
 from o2common.authmw import authmiddleware
 from o2common.authmw import authprov
-from o2common.config.config import get_review_url
 from o2common.helper import o2logging
 
 AUTH_ENABLED = True
@@ -32,18 +31,8 @@ FLASK_API_VERSION = '1.0.0'
 app = Flask(__name__)
 logger = o2logging.get_logger(__name__)
 
-
-def _get_k8s_url():
-    try:
-        token_review_url = get_review_url()
-        return token_review_url
-    except Exception:
-        raise Exception('Get k8s token review url failed')
-
-
 if AUTH_ENABLED:
     # perform service account identity&privilege check.
-    _get_k8s_url()
     ad = authprov.auth_definer('ad')
     ad.sanity_check()
     app.wsgi_app = authmiddleware.authmiddleware(app.wsgi_app)
index 3141263..a5193fc 100644 (file)
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
+import json
+from flask_restx._http import HTTPStatus
 from werkzeug.wrappers import Request, Response
-from o2common.helper import o2logging
+
 from o2common.authmw.authprov import auth_definer
-from flask_restx._http import HTTPStatus
-import json
+from o2common.authmw.exceptions import AuthRequiredExp
+from o2common.authmw.exceptions import AuthFailureExp
+from o2common.helper import o2logging
 
 logger = o2logging.get_logger(__name__)
 
 
-class AuthRequiredExp(Exception):
-    def __init__(self, value):
-        self.value = value
-
-    def dictize(self):
-        return {
-            'WWW-Authenticate': '{}'.format(self.value)}
-
-
 class AuthProblemDetails():
     def __init__(self, code: int, detail: str, path: str,
                  title=None, instance=None
@@ -54,15 +48,6 @@ class AuthProblemDetails():
         return json.dumps(details, indent=True)
 
 
-class AuthFailureExp(Exception):
-    def __init__(self, value):
-        self.value = value
-
-    def dictize(self):
-        return {
-            'WWW-Authenticate': '{}'.format(self.value)}
-
-
 def _response_wrapper(environ, start_response, header, detail):
     res = Response(headers=header,
                    mimetype='application/json', status=401, response=detail)
@@ -75,7 +60,6 @@ def _internal_err_response_wrapper(environ, start_response, detail):
 
 
 class authmiddleware():
-
     '''
     Auth WSGI middleware
     '''
index 11243df..87bbc4e 100644 (file)
 #  limitations under the License.
 
 import ssl
-from o2common.helper import o2logging
 import urllib.request
 import urllib.parse
 import json
-
+from http import HTTPStatus
+from requests import post as requests_post
+from requests.auth import HTTPBasicAuth
+from requests.exceptions import HTTPError
+from jwt import decode as jwt_decode
+from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
+
+from o2common.authmw.exceptions import AuthRequiredExp
+from o2common.authmw.exceptions import AuthFailureExp
 from o2common.config.config import get_auth_provider, get_review_url
 from o2common.config.config import get_reviewer_token
+from o2common.config import conf
+from o2common.helper import o2logging
 
 ssl._create_default_https_context = ssl._create_unverified_context
 logger = o2logging.get_logger(__name__)
 
 
+class OAuthAuthenticationException(Exception):
+    def __init__(self, value):
+        self.value = value
+
+
 class K8SAuthenticaException(Exception):
     def __init__(self, value):
         self.value = value
@@ -45,7 +59,7 @@ class auth_definer():
         if auth_prv_conf == 'k8s':
             self.obj = k8s_auth_provider('k8s')
         else:
-            self.obj = keystone_auth_provider('keystone')
+            self.obj = oauth2_auth_provider('oauth2')
 
     def tokenissue(self):
         return self.obj.tokenissue()
@@ -53,7 +67,6 @@ class auth_definer():
     def sanity_check(self):
         return self.obj.sanity_check()
 
-    # call k8s api
     def authenticate(self, token):
         return self.obj.authenticate(token)
 
@@ -82,6 +95,7 @@ class k8s_auth_provider(auth_definer):
             raise Exception(str(ex))
 
     def authenticate(self, token):
+        ''' Call Kubenetes API to authenticate '''
         reviewer_token = get_reviewer_token()
         tokenreview = {
             "kind": "TokenReview",
@@ -125,18 +139,70 @@ class k8s_auth_provider(auth_definer):
         return True
 
 
-class keystone_auth_provider(auth_definer):
+class oauth2_auth_provider(auth_definer):
     def __init__(self, name):
         self.name = name
 
-    def tokenissue(self, *args1, **args2):
-        pass
+    def _format_public_key(self):
+        public_key_string = """-----BEGIN PUBLIC KEY----- \
+        %s \
+        -----END PUBLIC KEY-----""" % conf.OAUTH2.oauth2_public_key
+        return public_key_string
 
-    def authenticate(self, *args1, **args2):
+    def _verify_jwt_token_introspect(self, token):
+        introspect_endpoint = conf.OAUTH2.oauth2_introspection_endpoint
+        client_id = conf.OAUTH2.oauth2_client_id
+        client_secret = conf.OAUTH2.oauth2_client_secret
+        try:
+            response = requests_post(
+                introspect_endpoint,
+                data={'token': token, 'client_id': client_id},
+                auth=HTTPBasicAuth(client_id, client_secret)
+            )
+        except HTTPError as e:
+            logger.error('OAuth2 jwt token introspect verify failed.')
+            raise Exception(str(e))
+        if response.status_code == HTTPStatus.OK:
+            introspection_data = response.json()
+            if introspection_data.get('active'):
+                logger.info('OAuth2 jwt token introspect result active.')
+                return True
+        logger.info('OAuth2 jwt token introspect verify failed.')
         return False
 
-    def sanity_check(self):
-        pass
+    def _verify_jwt_token(self, token):
+        algorithm = conf.OAUTH2.oauth2_algorithm
+        public_key_string = self._format_public_key()
+        try:
+            options = {"verify_signature": True, "verify_aud": False,
+                       "exp": True}
+            decoded_token = jwt_decode(token, public_key_string,
+                                       algorithms=[algorithm], options=options)
+            logger.info(
+                'Verified Token from client: %s' %
+                decoded_token.get("clientHost"))
+            return True
+        except (ExpiredSignatureError,
+                InvalidTokenError) as e:
+            logger.error(f'OAuth2 jwt token validation failed: {e}')
+            raise AuthFailureExp(
+                'OAuth2 JWT Token Authentication failure.')
+        except Exception as e:
+            raise AuthRequiredExp(str(e))
 
-    def tokenrevoke(self, *args1, **args2):
+    def authenticate(self, token):
+        ''' Call the JWT to authenticate
+
+        If the verify type is introspection, call introspection endpoint to
+        verify the token.
+        If the verify type is jwt, call JWT SDK to verify the token.
+        '''
+        oauth2_verify_type = conf.OAUTH2.oauth2_verify_type
+        if oauth2_verify_type == 'introspection':
+            return self._verify_jwt_token_introspect(token)
+        elif oauth2_verify_type == 'jwt':
+            return self._verify_jwt_token(token)
         return False
+
+    def sanity_check(self):
+        pass
diff --git a/o2common/authmw/exceptions.py b/o2common/authmw/exceptions.py
new file mode 100644 (file)
index 0000000..5d1b1a6
--- /dev/null
@@ -0,0 +1,32 @@
+# Copyright (C) 2024 Wind River Systems, Inc.
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+# pylint: disable=too-few-public-methods
+
+
+class AuthGeneralException(Exception):
+    def __init__(self, value):
+        self.value = value
+
+    def dictize(self):
+        return {
+            'WWW-Authenticate': '{}'.format(self.value)}
+
+
+class AuthRequiredExp(AuthGeneralException):
+    pass
+
+
+class AuthFailureExp(AuthGeneralException):
+    pass
index 06c3b56..f8eee94 100644 (file)
@@ -386,7 +386,7 @@ def get_reviewer_token():
 
 
 def get_auth_provider():
-    return 'k8s'
+    return config.conf.auth_provider
 
 
 def get_dms_support_profiles():
index 3462bae..5d34a0c 100644 (file)
@@ -24,3 +24,6 @@ ruamel.yaml==0.17.17
 pyOpenSSL
 
 gunicorn
+
+# Import JWT to support OAuth2
+pyjwt==2.6.0