Merge "OAuth2 support"
[pti/o2.git] / o2common / authmw / authprov.py
1 # Copyright (C) 2022 Wind River Systems, Inc.
2 #
3 #  Licensed under the Apache License, Version 2.0 (the "License");
4 #  you may not use this file except in compliance with the License.
5 #  You may obtain a copy of the License at
6 #
7 #      http://www.apache.org/licenses/LICENSE-2.0
8 #
9 #  Unless required by applicable law or agreed to in writing, software
10 #  distributed under the License is distributed on an "AS IS" BASIS,
11 #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 #  See the License for the specific language governing permissions and
13 #  limitations under the License.
14
15 import ssl
16 import urllib.request
17 import urllib.parse
18 import json
19 from http import HTTPStatus
20 from requests import post as requests_post
21 from requests.auth import HTTPBasicAuth
22 from requests.exceptions import HTTPError
23 from jwt import decode as jwt_decode
24 from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
25
26 from o2common.authmw.exceptions import AuthRequiredExp
27 from o2common.authmw.exceptions import AuthFailureExp
28 from o2common.config.config import get_auth_provider, get_review_url
29 from o2common.config.config import get_reviewer_token
30 from o2common.config import conf
31 from o2common.helper import o2logging
32
33 ssl._create_default_https_context = ssl._create_unverified_context
34 logger = o2logging.get_logger(__name__)
35
36
37 class OAuthAuthenticationException(Exception):
38     def __init__(self, value):
39         self.value = value
40
41
42 class K8SAuthenticaException(Exception):
43     def __init__(self, value):
44         self.value = value
45
46
47 class K8SAuthorizationException(Exception):
48     def __init__(self, value):
49         self.value = value
50
51
52 class auth_definer():
53
54     def __init__(self, name):
55         super().__init__()
56         self.name = name
57         # read the conf from config file
58         auth_prv_conf = get_auth_provider()
59         if auth_prv_conf == 'k8s':
60             self.obj = k8s_auth_provider('k8s')
61         else:
62             self.obj = oauth2_auth_provider('oauth2')
63
64     def tokenissue(self):
65         return self.obj.tokenissue()
66
67     def sanity_check(self):
68         return self.obj.sanity_check()
69
70     def authenticate(self, token):
71         return self.obj.authenticate(token)
72
73     def __repr__(self) -> str:
74         return "<auth_definer: name = %s>" % self.name
75
76
77 class k8s_auth_provider(auth_definer):
78
79     def __init__(self, name):
80         self.name = name
81         try:
82             self.token_review_url = get_review_url()
83         except Exception:
84             raise Exception('Failed to get k8s token review url.')
85
86     def tokenissue(self, **args2):
87         pass
88
89     def sanity_check(self):
90         try:
91             self.authenticate('faketoken')
92         except Exception as ex:
93             logger.critical(
94                 'Failed to bootstrap oauth middleware with exp: ' + str(ex))
95             raise Exception(str(ex))
96
97     def authenticate(self, token):
98         ''' Call Kubenetes API to authenticate '''
99         reviewer_token = get_reviewer_token()
100         tokenreview = {
101             "kind": "TokenReview",
102             "apiVersion": "authentication.k8s.io/v1",
103             "metadata": {
104                 "creationTimestamp": None
105             },
106             "spec": {
107                 "token": ""+token
108             },
109             "status": {
110                 "user": {}
111             }
112         }
113         datas = json.dumps(tokenreview)
114         binary_data = datas.encode('utf-8')
115         # 'post' method
116         header = {'Authorization': 'Bearer '+reviewer_token,
117                   'Content-Type': 'application/json'}
118         try:
119             req = urllib.request.Request(
120                 self.token_review_url, data=binary_data, headers=header)
121             response = urllib.request.urlopen(req)
122             data = json.load(response)
123             if data['status']['authenticated'] is True:
124                 logger.debug("Authenticated.")
125                 return True
126         except Exception as ex:
127             strex = str(ex)
128             logger.warning(
129                 "Invoke K8s API Service Exception happened:" + strex)
130             if '403' in strex:
131                 raise K8SAuthorizationException(
132                     'No privilege to perform oauth token check.')
133             elif '401' in strex:
134                 raise K8SAuthenticaException(
135                     'Self Authentication failure.')
136         return False
137
138     def tokenrevoke(self, **args2):
139         return True
140
141
142 class oauth2_auth_provider(auth_definer):
143     def __init__(self, name):
144         self.name = name
145
146     def _format_public_key(self):
147         public_key_string = """-----BEGIN PUBLIC KEY----- \
148         %s \
149         -----END PUBLIC KEY-----""" % conf.OAUTH2.oauth2_public_key
150         return public_key_string
151
152     def _verify_jwt_token_introspect(self, token):
153         introspect_endpoint = conf.OAUTH2.oauth2_introspection_endpoint
154         client_id = conf.OAUTH2.oauth2_client_id
155         client_secret = conf.OAUTH2.oauth2_client_secret
156         try:
157             response = requests_post(
158                 introspect_endpoint,
159                 data={'token': token, 'client_id': client_id},
160                 auth=HTTPBasicAuth(client_id, client_secret)
161             )
162         except HTTPError as e:
163             logger.error('OAuth2 jwt token introspect verify failed.')
164             raise Exception(str(e))
165         if response.status_code == HTTPStatus.OK:
166             introspection_data = response.json()
167             if introspection_data.get('active'):
168                 logger.info('OAuth2 jwt token introspect result active.')
169                 return True
170         logger.info('OAuth2 jwt token introspect verify failed.')
171         return False
172
173     def _verify_jwt_token(self, token):
174         algorithm = conf.OAUTH2.oauth2_algorithm
175         public_key_string = self._format_public_key()
176         try:
177             options = {"verify_signature": True, "verify_aud": False,
178                        "exp": True}
179             decoded_token = jwt_decode(token, public_key_string,
180                                        algorithms=[algorithm], options=options)
181             logger.info(
182                 'Verified Token from client: %s' %
183                 decoded_token.get("clientHost"))
184             return True
185         except (ExpiredSignatureError,
186                 InvalidTokenError) as e:
187             logger.error(f'OAuth2 jwt token validation failed: {e}')
188             raise AuthFailureExp(
189                 'OAuth2 JWT Token Authentication failure.')
190         except Exception as e:
191             raise AuthRequiredExp(str(e))
192
193     def authenticate(self, token):
194         ''' Call the JWT to authenticate
195
196         If the verify type is introspection, call introspection endpoint to
197         verify the token.
198         If the verify type is jwt, call JWT SDK to verify the token.
199         '''
200         oauth2_verify_type = conf.OAUTH2.oauth2_verify_type
201         if oauth2_verify_type == 'introspection':
202             return self._verify_jwt_token_introspect(token)
203         elif oauth2_verify_type == 'jwt':
204             return self._verify_jwt_token(token)
205         return False
206
207     def sanity_check(self):
208         pass