yhttp-auth 3.9.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yhttp/ext/auth/__init__.py +5 -0
- yhttp/ext/auth/authentication.py +397 -0
- yhttp/ext/auth/cli.py +37 -0
- yhttp/ext/auth/install.py +16 -0
- yhttp_auth-3.9.2.dist-info/LICENSE +21 -0
- yhttp_auth-3.9.2.dist-info/METADATA +119 -0
- yhttp_auth-3.9.2.dist-info/RECORD +9 -0
- yhttp_auth-3.9.2.dist-info/WHEEL +5 -0
- yhttp_auth-3.9.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import jwt
|
|
3
|
+
import hashlib
|
|
4
|
+
import functools
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
6
|
+
|
|
7
|
+
import redis
|
|
8
|
+
from pymlconf import MergableDict
|
|
9
|
+
from yhttp import statuses
|
|
10
|
+
from yhttp.lazyattribute import lazyattribute
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
FORBIDDEN_REDIS_KEY = 'yhttp-auth-forbidden'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Identity:
|
|
17
|
+
def __init__(self, payload):
|
|
18
|
+
assert payload['id'] is not None
|
|
19
|
+
self.payload = payload
|
|
20
|
+
|
|
21
|
+
def __getattr__(self, attr):
|
|
22
|
+
try:
|
|
23
|
+
return self.payload[attr]
|
|
24
|
+
except KeyError:
|
|
25
|
+
raise AttributeError()
|
|
26
|
+
|
|
27
|
+
def authorize(self, *roles):
|
|
28
|
+
if 'roles' not in self.payload:
|
|
29
|
+
raise statuses.forbidden()
|
|
30
|
+
|
|
31
|
+
for r in roles:
|
|
32
|
+
if r in self.roles:
|
|
33
|
+
return r
|
|
34
|
+
|
|
35
|
+
raise statuses.forbidden()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Authenticator:
|
|
39
|
+
redis = None
|
|
40
|
+
default_settings = MergableDict('''
|
|
41
|
+
redis:
|
|
42
|
+
host: localhost
|
|
43
|
+
port: 6379
|
|
44
|
+
db: 0
|
|
45
|
+
|
|
46
|
+
token:
|
|
47
|
+
algorithm: HS256
|
|
48
|
+
secret: foobar
|
|
49
|
+
maxage: 3600 # seconds
|
|
50
|
+
leeway: 10 # seconds
|
|
51
|
+
|
|
52
|
+
refresh:
|
|
53
|
+
key: yhttp-refresh-token
|
|
54
|
+
algorithm: HS256
|
|
55
|
+
secret: quxquux
|
|
56
|
+
secure: true
|
|
57
|
+
httponly: true
|
|
58
|
+
maxage: 2592000 # 1 Month
|
|
59
|
+
leeway: 10 # seconds
|
|
60
|
+
domain:
|
|
61
|
+
path:
|
|
62
|
+
samesite: Strict
|
|
63
|
+
|
|
64
|
+
csrf:
|
|
65
|
+
key: yhttp-csrf-token
|
|
66
|
+
secure: true
|
|
67
|
+
httponly: true
|
|
68
|
+
maxage: 60 # 1 Minute
|
|
69
|
+
samesite: Strict
|
|
70
|
+
domain:
|
|
71
|
+
path:
|
|
72
|
+
|
|
73
|
+
oauth2:
|
|
74
|
+
state:
|
|
75
|
+
algorithm: HS256
|
|
76
|
+
secret: quxquux
|
|
77
|
+
maxage: 60 # 1 Minute
|
|
78
|
+
leeway: 10 # seconds
|
|
79
|
+
|
|
80
|
+
''')
|
|
81
|
+
|
|
82
|
+
def __init__(self, settings=None):
|
|
83
|
+
self.settings = settings if settings else self.default_settings
|
|
84
|
+
self.redis = redis.Redis(**self.settings.redis)
|
|
85
|
+
|
|
86
|
+
##########
|
|
87
|
+
# OAuth2 #
|
|
88
|
+
##########
|
|
89
|
+
|
|
90
|
+
@lazyattribute
|
|
91
|
+
def oauth2_state_maxage(self):
|
|
92
|
+
return self.settings.oauth2.state.maxage
|
|
93
|
+
|
|
94
|
+
@lazyattribute
|
|
95
|
+
def oauth2_state_secret(self):
|
|
96
|
+
return self.settings.oauth2.state.secret
|
|
97
|
+
|
|
98
|
+
@lazyattribute
|
|
99
|
+
def oauth2_state_algorithm(self):
|
|
100
|
+
return self.settings.oauth2.state.algorithm
|
|
101
|
+
|
|
102
|
+
@lazyattribute
|
|
103
|
+
def oauth2_state_leeway(self):
|
|
104
|
+
return self.settings.oauth2.state.leeway
|
|
105
|
+
|
|
106
|
+
def dump_oauth2_state(self, req, redirect_url, attrs=None):
|
|
107
|
+
payload = {
|
|
108
|
+
'exp': self._exp(self.oauth2_state_maxage),
|
|
109
|
+
'redurl': redirect_url,
|
|
110
|
+
'id': self.create_csrftoken(req)
|
|
111
|
+
}
|
|
112
|
+
if attrs:
|
|
113
|
+
payload.update(attrs)
|
|
114
|
+
|
|
115
|
+
return jwt.encode(
|
|
116
|
+
payload,
|
|
117
|
+
self.oauth2_state_secret,
|
|
118
|
+
algorithm=self.oauth2_state_algorithm
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def decode_oauth2_state(self, state):
|
|
122
|
+
return jwt.decode(
|
|
123
|
+
state,
|
|
124
|
+
self.oauth2_state_secret,
|
|
125
|
+
leeway=self.oauth2_state_leeway,
|
|
126
|
+
algorithms=[self.oauth2_state_algorithm]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def verify_oauth2_state(self, req, state):
|
|
130
|
+
if state is None:
|
|
131
|
+
raise statuses.unauthorized()
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
identity = Identity(self.decode_oauth2_state(state))
|
|
135
|
+
except (KeyError, jwt.DecodeError, jwt.ExpiredSignatureError):
|
|
136
|
+
raise statuses.unauthorized()
|
|
137
|
+
|
|
138
|
+
self.verify_csrftoken(req, identity.id)
|
|
139
|
+
return identity
|
|
140
|
+
|
|
141
|
+
########
|
|
142
|
+
# CSRF #
|
|
143
|
+
########
|
|
144
|
+
|
|
145
|
+
@lazyattribute
|
|
146
|
+
def csrf_cookiekey(self):
|
|
147
|
+
return self.settings.csrf.key
|
|
148
|
+
|
|
149
|
+
def set_csrfcookie(self, req, token):
|
|
150
|
+
settings = self.settings.csrf
|
|
151
|
+
|
|
152
|
+
# Set cookie
|
|
153
|
+
entry = req.response.setcookie(self.csrf_cookiekey, token)
|
|
154
|
+
|
|
155
|
+
if settings.secure:
|
|
156
|
+
entry['secure'] = settings.secure
|
|
157
|
+
|
|
158
|
+
if settings.httponly:
|
|
159
|
+
entry['httponly'] = settings.httponly
|
|
160
|
+
|
|
161
|
+
if settings.domain:
|
|
162
|
+
entry['domain'] = settings.domain
|
|
163
|
+
|
|
164
|
+
if settings.samesite:
|
|
165
|
+
entry['samesite'] = settings.samesite
|
|
166
|
+
|
|
167
|
+
if settings.maxage:
|
|
168
|
+
entry['max-age'] = settings.maxage
|
|
169
|
+
|
|
170
|
+
entry['path'] = settings.path if settings.path else req.path
|
|
171
|
+
return entry
|
|
172
|
+
|
|
173
|
+
def create_csrftoken(self, req):
|
|
174
|
+
# Create a state token to prevent request forgery.
|
|
175
|
+
token = hashlib.sha256(os.urandom(1024)).hexdigest()
|
|
176
|
+
|
|
177
|
+
self.set_csrfcookie(req, token)
|
|
178
|
+
return token
|
|
179
|
+
|
|
180
|
+
def verify_csrftoken(self, req, token):
|
|
181
|
+
ctoken = req.cookies.get(self.csrf_cookiekey)
|
|
182
|
+
if ctoken is None:
|
|
183
|
+
raise statuses.unauthorized()
|
|
184
|
+
|
|
185
|
+
if ctoken.value != token:
|
|
186
|
+
raise statuses.unauthorized()
|
|
187
|
+
|
|
188
|
+
##########
|
|
189
|
+
# Refresh
|
|
190
|
+
##########
|
|
191
|
+
|
|
192
|
+
@lazyattribute
|
|
193
|
+
def refresh_cookiekey(self):
|
|
194
|
+
return self.settings.refresh.key
|
|
195
|
+
|
|
196
|
+
@lazyattribute
|
|
197
|
+
def refresh_secret(self):
|
|
198
|
+
return self.settings.refresh.secret
|
|
199
|
+
|
|
200
|
+
@lazyattribute
|
|
201
|
+
def refresh_maxage(self):
|
|
202
|
+
return self.settings.refresh.maxage
|
|
203
|
+
|
|
204
|
+
@lazyattribute
|
|
205
|
+
def refresh_leeway(self):
|
|
206
|
+
return self.settings.refresh.leeway
|
|
207
|
+
|
|
208
|
+
@lazyattribute
|
|
209
|
+
def refresh_algorithm(self):
|
|
210
|
+
return self.settings.refresh.algorithm
|
|
211
|
+
|
|
212
|
+
def _set_refreshtoken(self, req, token):
|
|
213
|
+
settings = self.settings.refresh
|
|
214
|
+
|
|
215
|
+
# Set cookie
|
|
216
|
+
entry = req.response.setcookie(self.refresh_cookiekey, token)
|
|
217
|
+
|
|
218
|
+
if settings.secure:
|
|
219
|
+
entry['secure'] = settings.secure
|
|
220
|
+
|
|
221
|
+
if settings.httponly:
|
|
222
|
+
entry['httponly'] = settings.httponly
|
|
223
|
+
|
|
224
|
+
if settings.domain:
|
|
225
|
+
entry['domain'] = settings.domain
|
|
226
|
+
|
|
227
|
+
if settings.samesite:
|
|
228
|
+
entry['samesite'] = settings.samesite
|
|
229
|
+
|
|
230
|
+
entry['path'] = settings.path if settings.path else req.path
|
|
231
|
+
return entry
|
|
232
|
+
|
|
233
|
+
def delete_refreshtoken(self, req):
|
|
234
|
+
entry = self._set_refreshtoken(req, '')
|
|
235
|
+
entry['expires'] = 'Thu, 01 Jan 1970 00:00:00 GMT'
|
|
236
|
+
return entry
|
|
237
|
+
|
|
238
|
+
def set_refreshtoken(self, req, id, attrs=None):
|
|
239
|
+
settings = self.settings.refresh
|
|
240
|
+
token = self.dump_refreshtoken(id, attrs)
|
|
241
|
+
|
|
242
|
+
# Set cookie
|
|
243
|
+
entry = self._set_refreshtoken(req, token)
|
|
244
|
+
entry['max-age'] = settings.maxage
|
|
245
|
+
return entry
|
|
246
|
+
|
|
247
|
+
def _exp(self, seconds):
|
|
248
|
+
return datetime.now(tz=timezone.utc) + timedelta(seconds=seconds)
|
|
249
|
+
|
|
250
|
+
def dump_refreshtoken(self, id, attrs=None):
|
|
251
|
+
payload = {
|
|
252
|
+
'id': id,
|
|
253
|
+
'refresh': True,
|
|
254
|
+
'exp': self._exp(self.refresh_maxage)
|
|
255
|
+
}
|
|
256
|
+
if attrs:
|
|
257
|
+
payload.update(attrs)
|
|
258
|
+
|
|
259
|
+
return jwt.encode(
|
|
260
|
+
payload,
|
|
261
|
+
self.refresh_secret,
|
|
262
|
+
algorithm=self.refresh_algorithm
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def verify_refreshtoken(self, req):
|
|
266
|
+
if self.refresh_cookiekey not in req.cookies:
|
|
267
|
+
raise statuses.unauthorized()
|
|
268
|
+
|
|
269
|
+
token = req.cookies[self.refresh_cookiekey].value
|
|
270
|
+
try:
|
|
271
|
+
identity = Identity(jwt.decode(
|
|
272
|
+
token,
|
|
273
|
+
self.refresh_secret,
|
|
274
|
+
leeway=self.refresh_leeway,
|
|
275
|
+
algorithms=[self.refresh_algorithm]
|
|
276
|
+
))
|
|
277
|
+
|
|
278
|
+
except (KeyError, jwt.DecodeError, jwt.ExpiredSignatureError):
|
|
279
|
+
raise statuses.unauthorized()
|
|
280
|
+
|
|
281
|
+
self.check_blacklist(identity.id)
|
|
282
|
+
return identity
|
|
283
|
+
|
|
284
|
+
def read_refreshtoken(self, req):
|
|
285
|
+
if self.refresh_cookiekey not in req.cookies:
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
token = req.cookies[self.refresh_cookiekey].value
|
|
289
|
+
try:
|
|
290
|
+
identity = Identity(jwt.decode(
|
|
291
|
+
token,
|
|
292
|
+
options={"verify_signature": False},
|
|
293
|
+
))
|
|
294
|
+
|
|
295
|
+
except (KeyError, jwt.DecodeError):
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
return identity
|
|
299
|
+
|
|
300
|
+
#########
|
|
301
|
+
# Token #
|
|
302
|
+
#########
|
|
303
|
+
|
|
304
|
+
@lazyattribute
|
|
305
|
+
def token_secret(self):
|
|
306
|
+
return self.settings.token.secret
|
|
307
|
+
|
|
308
|
+
@lazyattribute
|
|
309
|
+
def token_maxage(self):
|
|
310
|
+
return self.settings.token.maxage
|
|
311
|
+
|
|
312
|
+
@lazyattribute
|
|
313
|
+
def token_leeway(self):
|
|
314
|
+
return self.settings.token.leeway
|
|
315
|
+
|
|
316
|
+
@lazyattribute
|
|
317
|
+
def token_algorithm(self):
|
|
318
|
+
return self.settings.token.algorithm
|
|
319
|
+
|
|
320
|
+
def dump(self, id, attrs=None, maxage=None):
|
|
321
|
+
payload = {
|
|
322
|
+
'id': id,
|
|
323
|
+
'exp': self._exp(maxage or self.token_maxage)
|
|
324
|
+
}
|
|
325
|
+
if attrs:
|
|
326
|
+
payload.update(attrs)
|
|
327
|
+
|
|
328
|
+
return jwt.encode(
|
|
329
|
+
payload,
|
|
330
|
+
self.token_secret,
|
|
331
|
+
algorithm=self.token_algorithm
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def dump_from_refreshtoken(self, refresh, attrs=None):
|
|
335
|
+
payload = refresh.payload.copy()
|
|
336
|
+
del payload['refresh']
|
|
337
|
+
|
|
338
|
+
if attrs:
|
|
339
|
+
payload.update(attrs)
|
|
340
|
+
|
|
341
|
+
payload['exp'] = self._exp(self.token_maxage)
|
|
342
|
+
return jwt.encode(
|
|
343
|
+
payload,
|
|
344
|
+
self.token_secret,
|
|
345
|
+
algorithm=self.token_algorithm
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def check_blacklist(self, userid):
|
|
349
|
+
# FIXME: use redis hash, hset, hget
|
|
350
|
+
if self.redis is not None and \
|
|
351
|
+
self.redis.sismember(FORBIDDEN_REDIS_KEY, userid):
|
|
352
|
+
raise statuses.unauthorized()
|
|
353
|
+
|
|
354
|
+
def decode_token(self, token):
|
|
355
|
+
return jwt.decode(
|
|
356
|
+
token,
|
|
357
|
+
self.token_secret,
|
|
358
|
+
leeway=self.token_leeway,
|
|
359
|
+
algorithms=[self.token_algorithm]
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
def verify_token(self, req):
|
|
363
|
+
token = req.headers.get('Authorization')
|
|
364
|
+
|
|
365
|
+
if token is None or not token.startswith('Bearer '):
|
|
366
|
+
raise statuses.unauthorized()
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
identity = Identity(self.decode_token(token[7:]))
|
|
370
|
+
except (KeyError, jwt.DecodeError, jwt.ExpiredSignatureError):
|
|
371
|
+
raise statuses.unauthorized()
|
|
372
|
+
|
|
373
|
+
self.check_blacklist(identity.id)
|
|
374
|
+
return identity
|
|
375
|
+
|
|
376
|
+
def preventlogin(self, id):
|
|
377
|
+
self.redis.sadd(FORBIDDEN_REDIS_KEY, id)
|
|
378
|
+
|
|
379
|
+
def permitlogin(self, id):
|
|
380
|
+
self.redis.srem(FORBIDDEN_REDIS_KEY, id)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def authenticate(app, roles=None):
|
|
384
|
+
if isinstance(roles, str):
|
|
385
|
+
roles = [i.strip() for i in roles.split(',')]
|
|
386
|
+
|
|
387
|
+
def decorator(handler):
|
|
388
|
+
@functools.wraps(handler)
|
|
389
|
+
def wrapper(req, *args, **kw):
|
|
390
|
+
req.identity = app.auth.verify_token(req)
|
|
391
|
+
if roles is not None:
|
|
392
|
+
req.identity.authorize(*roles)
|
|
393
|
+
|
|
394
|
+
return handler(req, *args, **kw)
|
|
395
|
+
|
|
396
|
+
return wrapper
|
|
397
|
+
return decorator
|
yhttp/ext/auth/cli.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from easycli import SubCommand, Argument
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from .authentication import Authenticator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Create(SubCommand):
|
|
8
|
+
__command__ = 'create'
|
|
9
|
+
__aliases__ = ['c']
|
|
10
|
+
__arguments__ = [
|
|
11
|
+
Argument(
|
|
12
|
+
'id', help='example: alice'
|
|
13
|
+
),
|
|
14
|
+
Argument(
|
|
15
|
+
'payload', default='', nargs='?', help='example: {"foo": "bar"}'
|
|
16
|
+
),
|
|
17
|
+
Argument(
|
|
18
|
+
'--maxage', type=int, help='Token maxage in seconds.'
|
|
19
|
+
),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
def __call__(self, args):
|
|
23
|
+
settings = args.application.settings.auth
|
|
24
|
+
jwt = Authenticator(settings)
|
|
25
|
+
if args.payload:
|
|
26
|
+
payload = json.loads(args.payload)
|
|
27
|
+
else:
|
|
28
|
+
payload = ''
|
|
29
|
+
|
|
30
|
+
print(jwt.dump(args.id, payload, maxage=args.maxage))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AuthenticatorCLI(SubCommand):
|
|
34
|
+
__command__ = 'auth'
|
|
35
|
+
__arguments__ = [
|
|
36
|
+
Create,
|
|
37
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
from .authentication import Authenticator, authenticate
|
|
4
|
+
from .cli import AuthenticatorCLI
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def install(app):
|
|
8
|
+
app.cliarguments.append(AuthenticatorCLI)
|
|
9
|
+
app.settings.merge('auth: {}')
|
|
10
|
+
app.settings['auth'].merge(Authenticator.default_settings)
|
|
11
|
+
|
|
12
|
+
@app.when
|
|
13
|
+
def ready(app):
|
|
14
|
+
app.auth = Authenticator(app.settings.auth)
|
|
15
|
+
|
|
16
|
+
return functools.partial(authenticate, app)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 yhttp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: yhttp-auth
|
|
3
|
+
Version: 3.9.2
|
|
4
|
+
Summary: A very micro http framework.
|
|
5
|
+
Home-page: http://github.com/yhttp/yhttp-auth
|
|
6
|
+
Author: Vahid Mardani
|
|
7
|
+
Author-email: vahid.mardani@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: License :: Other/Proprietary License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Topic :: Software Development
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: pyjwt
|
|
26
|
+
Requires-Dist: hiredis
|
|
27
|
+
Requires-Dist: redis
|
|
28
|
+
Requires-Dist: yhttp >=4.1.4
|
|
29
|
+
|
|
30
|
+
# yhttp-auth
|
|
31
|
+
|
|
32
|
+
[](https://pypi.python.org/pypi/yhttp-auth)
|
|
33
|
+
[](https://github.com/yhttp/yhttp-auth/actions/workflows/build.yml)
|
|
34
|
+
[](https://coveralls.io/github/yhttp/yhttp-auth?branch=master)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
Authentication extension for [yhttp](https://github.com/yhttp/yhttp).
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install yhttp-pony
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
### Usage
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from yhttp import Application
|
|
51
|
+
from yhttp.ext.auth import install as auth_install
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
app = Application()
|
|
55
|
+
auth = auth_install(app)
|
|
56
|
+
app.settings.merge(f'''
|
|
57
|
+
auth:
|
|
58
|
+
redis:
|
|
59
|
+
host: localhost
|
|
60
|
+
port: 6379
|
|
61
|
+
db: 0
|
|
62
|
+
|
|
63
|
+
token:
|
|
64
|
+
algorithm: HS256
|
|
65
|
+
secret: foobar
|
|
66
|
+
|
|
67
|
+
refresh:
|
|
68
|
+
key: yhttp-refresh-token
|
|
69
|
+
algorithm: HS256
|
|
70
|
+
secret: quxquux
|
|
71
|
+
secure: true
|
|
72
|
+
httponly: true
|
|
73
|
+
maxage: 2592000 # 1 Month
|
|
74
|
+
domain: example.com
|
|
75
|
+
''')
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.route('/reftokens')
|
|
79
|
+
@yhttp.statuscode(yhttp.statuses.created)
|
|
80
|
+
def create(req):
|
|
81
|
+
app.auth.set_refreshtoken(req, 'alice', dict(baz='qux'))
|
|
82
|
+
|
|
83
|
+
@app.route('/tokens')
|
|
84
|
+
@yhttp.statuscode(yhttp.statuses.created)
|
|
85
|
+
@yhttp.text
|
|
86
|
+
def refresh(req):
|
|
87
|
+
reftoken = app.auth.verify_refreshtoken(req)
|
|
88
|
+
return app.auth.dump_from_refreshtoken(reftoken, dict(foo='bar'))
|
|
89
|
+
|
|
90
|
+
@app.route('/admin')
|
|
91
|
+
@auth(roles='admin, god')
|
|
92
|
+
@yhttp.text
|
|
93
|
+
def get(req):
|
|
94
|
+
return req.identity.roles
|
|
95
|
+
|
|
96
|
+
app.ready()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Command line interface
|
|
100
|
+
|
|
101
|
+
setup.py
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
|
|
105
|
+
setup(
|
|
106
|
+
...
|
|
107
|
+
entry_points={
|
|
108
|
+
'console_scripts': [
|
|
109
|
+
'myapp = myapp:app.climain'
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
...
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
myapp auth --help
|
|
119
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
yhttp/ext/auth/__init__.py,sha256=B84lEOXQReXYwKmMkVjDAAZIeFcPSJk9F-u2DaBa_Qs,128
|
|
2
|
+
yhttp/ext/auth/authentication.py,sha256=35x999VqUtbrnRLX3FV9CZ-qHmlgGeAwWmF1mBUXMqY,10229
|
|
3
|
+
yhttp/ext/auth/cli.py,sha256=EX0pgDLRIjm-ulPwZ7lbEqYpFXoCmaozzK6HFJQC9sQ,873
|
|
4
|
+
yhttp/ext/auth/install.py,sha256=EIDVHYHlvvqmU99nsxuRjSEUNprHyzbv4ybyt_RWXNw,408
|
|
5
|
+
yhttp_auth-3.9.2.dist-info/LICENSE,sha256=1lZ7neh4Es6earhYMwYaqsuS1Alohsrf9GZRVdl7l5o,1062
|
|
6
|
+
yhttp_auth-3.9.2.dist-info/METADATA,sha256=G3fecKi0_xXcr9vI-feBJJn2OcPIBMN2-QhWijbgBNQ,2785
|
|
7
|
+
yhttp_auth-3.9.2.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
|
|
8
|
+
yhttp_auth-3.9.2.dist-info/top_level.txt,sha256=efaJ4DfUOs7MO3pjIEyRhnQASGVyD6vH-SL3ogQxkeU,6
|
|
9
|
+
yhttp_auth-3.9.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
yhttp
|