-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathauth.py
267 lines (202 loc) · 7.63 KB
/
auth.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
import jwt
from urllib.parse import unquote_plus
from contextlib import contextmanager
from functools import wraps
from flask import request
from pymacaron.log import pymlogger
from pymacaron.exceptions import AuthInvalidTokenError
from pymacaron.exceptions import AuthTokenExpiredError
from pymacaron.exceptions import AuthMissingHeaderError
from pymacaron.utils import timenow, to_epoch
from pymacaron.config import get_config
try:
from flask import _app_ctx_stack as stack
except ImportError:
from flask import _request_ctx_stack as stack
log = pymlogger(__name__)
#
# Decorators used to add authentication to endpoints in swagger specs
#
def requires_auth(f):
"""A decorator for flask api methods that validates auth0 tokens, hence ensuring
that the user is authenticated. Code coped from:
https://github.com/auth0/auth0-python/tree/master/examples/flask-api
"""
@wraps(f)
def requires_auth_decorator(*args, **kwargs):
authenticate_http_request()
return f(*args, **kwargs)
return requires_auth_decorator
def add_auth(f):
"""A decorator that adds the authentication header to requests arguments"""
def add_auth_decorator(*args, **kwargs):
token = get_user_token()
if 'headers' not in kwargs:
kwargs['headers'] = {}
kwargs['headers']['Authorization'] = f"Bearer {token}"
return f(*args, **kwargs)
return add_auth_decorator
#
# Get and validate a token
#
def load_auth_token(token, load=True):
"""Validate an auth0 token. Returns the token's payload, or an exception
of the type:"""
conf = get_config()
assert conf.jwt_secret, "No JWT secret configured for pymacaron"
assert conf.jwt_issuer, "No JWT issuer configured for pymacaron"
assert conf.jwt_audience, "No JWT audience configured for pymacaron"
# First extract the issuer
issuer = conf.jwt_issuer
try:
headers = jwt.get_unverified_header(token)
except jwt.exceptions.DecodeError:
raise AuthInvalidTokenError('token signature is invalid')
log.info(f"JWT token has headers '{headers}'")
if 'iss' in headers:
issuer = headers['iss']
# Then validate the token against this issuer
try:
payload = jwt.decode(
token,
conf.jwt_secret,
audience=conf.jwt_audience,
algorithms=["HS256"],
# Allow for a time difference of up to 5min (300sec)
leeway=300,
)
except jwt.exceptions.ExpiredSignatureError:
log.debug('JWT token has expired')
raise AuthTokenExpiredError('JWT token has expired')
except jwt.exceptions.InvalidSignatureError:
log.debug('JWT token signature is invalid')
raise AuthInvalidTokenError('JWT token signature is invalid')
except jwt.exceptions.InvalidAudienceError:
log.debug('JWT has incorrect audience')
raise AuthInvalidTokenError('JWT has incorrect audience')
except jwt.exceptions.InvalidIssuedAtError:
log.debug('JWT token was issued in the future')
raise AuthInvalidTokenError('JWT token was issued in the future')
except jwt.exceptions.InvalidTokenError:
# A catch-all for all other invalid token errors
log.debug('JWT token is invalid')
raise AuthInvalidTokenError('JWT token is invalid')
# Save payload to stack
payload['token'] = token
payload['iss'] = issuer
if load:
stack.top.current_user = payload
return payload
def authenticate_http_request(token=None):
"""Validate auth0 tokens passed in the request's header, hence ensuring
that the user is authenticated. Code copied from:
https://github.com/auth0/auth0-python/tree/master/examples/flask-api
Return a PntCommonException if failed to validate authentication.
Otherwise, return the token's payload (Also stored in stack.top.current_user)
"""
if token:
auth = token
else:
auth = request.headers.get('Authorization', None)
if not auth:
auth = request.cookies.get('token', None)
if auth:
auth = unquote_plus(auth)
log.debug(f"Validating Auth header [{auth}]")
if not auth:
raise AuthMissingHeaderError('There is no Authorization header in the HTTP request')
parts = auth.split()
if parts[0].lower() != 'bearer':
raise AuthInvalidTokenError('Authorization header must start with Bearer')
elif len(parts) == 1:
raise AuthInvalidTokenError('Token not found in Authorization header')
elif len(parts) > 2:
raise AuthInvalidTokenError('Authorization header must be Bearer + \\s + token')
token = parts[1]
return load_auth_token(token)
#
# Generate tokens
#
def generate_token(user_id, expire_in=None, data={}, issuer=None, iat=None):
"""Generate a new JWT token for this user_id. Default expiration date
is 1 year from creation time"""
assert user_id, "No user_id passed to generate_token()"
assert isinstance(data, dict), "generate_token(data=) should be a dictionary"
assert get_config().jwt_secret, "No JWT secret configured in pymacaron"
if not issuer:
issuer = get_config().jwt_issuer
assert issuer, "No JWT issuer configured for pymacaron"
if expire_in is None:
expire_in = get_config().jwt_token_timeout
if iat:
epoch_now = iat
else:
epoch_now = to_epoch(timenow())
epoch_end = epoch_now + expire_in
data['iss'] = issuer
data['sub'] = user_id
data['aud'] = get_config().jwt_audience
data['exp'] = epoch_end
data['iat'] = epoch_now
headers = {
"typ": "JWT",
"alg": "HS256",
"iss": issuer,
}
log.debug("Encoding token with data '{data}' and headers '{headers}'")
t = jwt.encode(
data,
get_config().jwt_secret,
headers=headers,
)
if type(t) is bytes:
t = t.decode("utf-8")
return t
@contextmanager
def backend_token(issuer=None, user_id=None, data={}):
if not issuer:
issuer = get_config().jwt_issuer
if not user_id:
user_id = get_config().default_user_id
assert issuer, "No JWT issuer configured for pymacaron"
assert user_id, "No user_id passed to generate_token()"
cur_token = ''
if stack.top is None:
raise RuntimeError('working outside of request context')
if not hasattr(stack.top, 'current_user'):
stack.top.current_user = {}
else:
cur_token = stack.top.current_user.get('token', '')
tmp_token = generate_token(user_id, issuer=issuer, data=data)
log.debug("Temporarily using custom token for %s and issuer %s: %s" % (user_id, issuer, tmp_token))
stack.top.current_user['token'] = tmp_token
yield tmp_token
log.debug("Restoring token %s" % cur_token)
stack.top.current_user['token'] = cur_token
#
# Access token data during runtime
#
def get_userid():
"""Return the authenticated user's id, i.e. its auth0 id"""
try:
current_user = stack.top.current_user
return current_user.get('sub', '')
except Exception:
return ''
def get_user_token_data():
"""Return the authenticated user's id, i.e. its auth0 id"""
return stack.top.current_user
def get_user_token():
"""Return the authenticated user's auth token"""
if not hasattr(stack.top, 'current_user'):
return ''
current_user = stack.top.current_user
return current_user.get('token', '')
def get_token_issuer():
"""Return the issuer in which this user's token was created"""
try:
current_user = stack.top.current_user
return current_user.get('iss', get_config().jwt_issuer)
except Exception:
pass
return get_config().jwt_issuer