Skip to content

Commit 8c56a2b

Browse files
author
Aaron Suarez
authored
Add JWT in auth header as an authN option (#325)
1 parent 0112911 commit 8c56a2b

File tree

7 files changed

+457
-36
lines changed

7 files changed

+457
-36
lines changed

.dev/dev-jwt-key

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIJKAIBAAKCAgEAp8DAocd7+LjrW0NucSPCcBnO7Inu7soRVCmaOjt1HcQHdCV4
3+
8WzPKAWxz/FQyVqHbUf+UZkw1ryi7CASf9n35Aia/JrYnW5hG1ti455GhgEUcItv
4+
B7dpscK9N3DeeyNv4tk3FokdhiG/92LvujhMxFPjO60jex0H2yieR7Osx/AwCEHN
5+
h6opct+EYNkoD1G2cXfCOCdZxpzBttU6jsvOfX3lDykWrZHSC0yfYpZU+9M4qtnj
6+
ZbpKK/Vpw4Ic5qTpYm9rBkF3rDbQeY2O5nw+3S996ckMR5jKXb4aRxnX2LawQ2Mm
7+
KmmYTRKMc7KG/vXPOH2qDcr/+caP5ZP+epTo5Rz4t88tuhTlj+KRefs1LM3dbq5r
8+
LnIbe7zmTuyzTSlA+qTMkmt42dZ4mAH0huEHNd931owRDlLvl/Py3by4D+RZo+er
9+
8D+wrkUQk4O+s6SYNqfdYphSIXgIbTeKny48E3Ph87fXQz4vgbJPFk1dGi9xm1ds
10+
NLrkMoapvZwdN7bSJ5zqjro71M4HnFRUAGdYnM353W5uKwEHSmR6TcOUastQ7qJD
11+
Y6DYNTKCte/XXQmgcResBtWRl2LVz7KepXHJrXjXcLv5OaJMRe9PklrWM3SOGpKf
12+
Q2CP3XNvDvu2x1kb3sikzjVdtl4glcEI4Ow68Ani73dDyAIVcXPu2CtGyksCAwEA
13+
AQKCAgEAo6JqRWUJkP0Q191XBhYTvLXwGtwRrex+KtLKFrOY8ogdnTZQW3AAQtIL
14+
OQP0AfXE1Ny9P2tnMJChfCNs6Dn+jPm39WA2nJrnLoBeXhouQNkczwu0KprHBxcm
15+
68W1v/g5U9b+3YSyv/x7/R0NK2FvwLLznWquiZEv8KAWhWrGx+GLeQJ3MjbSZ7OQ
16+
tcgeQ5M5nEVttsjr0clnTKmCjXhQ3CjKH5e8/2KWuV7suoZaL6tCQ6Z3IuwtHeQu
17+
Xv+0oWeMIPD+PQPvcJWnlmp3Um0wBSImeL4ctFpeTEL77w9OdZ7/ITy+JfELF2NY
18+
jiM/e8TbdgdeskWqnEMMaq2KNpi68+2BvLa2aBOS4G0KZQirke9/e54giOuiFbN9
19+
CstT7w6qNLb1bVuMKNaX2Fe/JMP/Ex8eXJLY4dViTmodMASFP1pckvP1fcgzcNdJ
20+
A8kxUujmS6mNZZSI91McWv4j1pAFWA4FzTgVnzyPoh2XZUbiR89QivJQkM0BLIVh
21+
AwkEIX2M6IjHzEOM3+ZtYassG+vYCjz526vq4tsfbycuBaOlyxFpJhKOMTK6qsM9
22+
GHvXcOGJIkB8VSQLocEx22Lfg1h/U/zDGZeExbJMwsnOFfOAmzz8l3bviPTKyUK0
23+
SNbQfa76pTJ1pg3qMNpetTTpGudf7CS+CliS/GvTd+MBdnvhMwECggEBAN8bFfAv
24+
S2U1R5X2OmbM3Q71zalL8WtogLoKt19j1y1WSzEE/0dun+xPz2CyUqAqQog21BNF
25+
QZyDS5OoGApVvDWjQqtpLTlaPd0vqBCiPZFNkQ7YNOpPH2d0fh/wx66sn/AtMffL
26+
ReFzvWM8rvl3ASr75fTIIoH6PuiI4J/IlEhIaa7iwp4nmtpmo+G23xQ/rbLdGiQu
27+
M73QmMM/Qy61bmrCJ9OXlSu5hgk3Leu6zDJs2ygM7Joe+KzFNFq1WFw/ef4K6F0r
28+
fbwBdAz5wOgitLgC69EmvL87mqLEQRTd/vgTjONj1+j3yVmhxzAzMaVZ/TWpuCkE
29+
sDjiSpNr5b6+85ECggEBAMB8bRLpa+xaYLYGl8M94qaVca4o2l7ZrLmMX63+BAV9
30+
jpxUIbJ/hk9DJ1SAl53ZLIAYDYT637eHGOiXTD3IqqE91sYDlGvrRjxFJfBJnZlT
31+
V6eXppN1rn+ZnKdJimd0H8pzQx7EGoGciXhDoawYhxY9BxkKQPDK93fCUr31tKDf
32+
gpOG/gIoRGHX+drmZnVkYvXOVgYloxyiEc67rcQ04S6IVuP9c4kYSXy/3w3iBFvS
33+
mPDgZsPKP1IQ4HVPDgFQfHkzaDIWf71XIiPgoZykYQx5araM7uwWFjsh/whZ5ulY
34+
M3kOgNcMlQ90E5bEpGorzX0DPSx9vEODjCYnB5QQehsCggEAV7UqNrYhCbScY9Pc
35+
ubUn4k23gCqeyf7XPEwiMpnpaaVXAfpY8RgIPrpRaE4yNUznwuzrCnhbhtAG0hFv
36+
AgEacGuyNfivErDrSR0HESL22TyJHjDY/JQGYIFnY98gYQb0CVN7JVMAMdVySqT8
37+
lI24I9HLYSOcjUR3nqrQw3/y60esZFg48jvXoKxhGMbvg+JUwtAxCrAvHxv2MiuY
38+
mbAxrD6PsZsRxZK1osHSh61zwQ8SSPhru1sZn7IXFuHbzsgViU14c8g5McPQf5lf
39+
wOKD8SMU2bBE21jvPbWxcCalqZjl9i62HpvqyBXVXJmDluF9ra7++wEg1fwAHVx5
40+
gTdIQQKCAQBnEHiKvsdVt5K/BEqwdOtuDOjguukqDl2IwFve2vsmQXNhyz57yAKP
41+
YEKn4W7NSyKjt71NbdLp/wFcUN622kJasbTVM8d9/W0PCmtk/NXQ6iouB2pe3I1B
42+
r2uMuzjLagc3rH3M9G3I5ptI9NWVQ1DZnHW3d6EMDXFyA2+wXOaJmQPeoFJTr2Hm
43+
DfGvvtwvkT/Xo9K12eM7iqAEVMOXIkVMWB5GV0hMqN94V3hEg7eXvuy7VTxRK3K6
44+
K2U0Cs9R7tmnP9pTr25YYFZcZYPDTtTUDBMSieXILY9bvDlFLHYSjXKKKDTecNND
45+
ggCXItVyL+AIRvqzXuO2NrKNHyrUofnvAoIBADXUidZCHzGPwK5uCfmNm0DMz5S/
46+
iNN/qKAsAn87EeRPCg+LJa/vRp4SqJzogbeYfCeEtwJx5Y2+EJ+zVnXAs/k7WFPA
47+
S94WfNlh9eRfsaVRDHdVSaB+Fhk8tQ3ZujwxtvfWQWy4aZBDMncWYzHJr5InI2jb
48+
FMDs3cxLanMMRo5wOzmD2OI7Jdb5DE9eZCWBeu03kmVcAP0zpb5ouIhV1WdPJH2W
49+
XSb7oyammHbQEMVeCYAULV1PcZ7RLI1ySdI9BpjIPlxMxAwqxUQXaMoYXfEftoGQ
50+
Elp0Mkin32RzA1JqdtAXLX/3ikpjgVa6pxJ58WDqPypa8RtdAaJwrmxEt9M=
51+
-----END RSA PRIVATE KEY-----

.dev/dev-jwt-key.pub

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8DAocd7+LjrW0NucSPC
3+
cBnO7Inu7soRVCmaOjt1HcQHdCV48WzPKAWxz/FQyVqHbUf+UZkw1ryi7CASf9n3
4+
5Aia/JrYnW5hG1ti455GhgEUcItvB7dpscK9N3DeeyNv4tk3FokdhiG/92LvujhM
5+
xFPjO60jex0H2yieR7Osx/AwCEHNh6opct+EYNkoD1G2cXfCOCdZxpzBttU6jsvO
6+
fX3lDykWrZHSC0yfYpZU+9M4qtnjZbpKK/Vpw4Ic5qTpYm9rBkF3rDbQeY2O5nw+
7+
3S996ckMR5jKXb4aRxnX2LawQ2MmKmmYTRKMc7KG/vXPOH2qDcr/+caP5ZP+epTo
8+
5Rz4t88tuhTlj+KRefs1LM3dbq5rLnIbe7zmTuyzTSlA+qTMkmt42dZ4mAH0huEH
9+
Nd931owRDlLvl/Py3by4D+RZo+er8D+wrkUQk4O+s6SYNqfdYphSIXgIbTeKny48
10+
E3Ph87fXQz4vgbJPFk1dGi9xm1dsNLrkMoapvZwdN7bSJ5zqjro71M4HnFRUAGdY
11+
nM353W5uKwEHSmR6TcOUastQ7qJDY6DYNTKCte/XXQmgcResBtWRl2LVz7KepXHJ
12+
rXjXcLv5OaJMRe9PklrWM3SOGpKfQ2CP3XNvDvu2x1kb3sikzjVdtl4glcEI4Ow6
13+
8Ani73dDyAIVcXPu2CtGyksCAwEAAQ==
14+
-----END PUBLIC KEY-----

Makefile

+5
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ test: build
7474
test-coverage:
7575
${DOCKER_COMPOSE} run ${RESOURCES_CONTAINER} py.test --cov-report html --cov=app/ tests/
7676

77+
# Usage: make test-single tests/unit/test_api_key.py::test_get_api_key
78+
.PHONY: test-single
79+
test-single: build
80+
${DOCKER_COMPOSE} run ${RESOURCES_CONTAINER} /bin/bash -c "py.test --cov=app/ $(shell echo ${ARGS})"
81+
7782
.PHONY: lint
7883
lint:
7984
${DOCKER_COMPOSE} run ${RESOURCES_CONTAINER} flake8 . --exclude migrations --statistics --count

app/api/auth.py

+47-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import uuid
2+
import os
23
from enum import Enum
34

45
import requests
6+
from app import db
57
from app.models import Key
68
from app.utils import setup_logger, standardize_response
79
from flask import g, request
810

11+
from jwt import decode, InvalidSignatureError, ExpiredSignatureError
12+
13+
PUBLIC_KEY = os.environ.get('JWT_PUBLIC_KEY', open(".dev/dev-jwt-key.pub").read())
14+
915
auth_logger = setup_logger('auth_logger')
1016
create_logger = setup_logger('create_auth_logger')
1117
update_logger = setup_logger('update_auth_logger')
@@ -85,10 +91,50 @@ def rotate_key(key, session):
8591
return None
8692

8793

94+
def jwt_to_key():
95+
auth_header = request.headers.get("authorization")
96+
if not auth_header:
97+
return None
98+
auth_parts = auth_header.split(" ")
99+
if len(auth_parts) != 2:
100+
return None
101+
_, token = auth_parts
102+
try:
103+
decoded_token = decode(
104+
token, PUBLIC_KEY, algorithms="RS256"
105+
)
106+
request.decoded_token = decoded_token
107+
except InvalidSignatureError:
108+
return None
109+
except ExpiredSignatureError:
110+
return None
111+
if 'exp' not in decoded_token:
112+
return None
113+
return get_api_key_from_authenticated_email(decoded_token['email'])
114+
115+
116+
# NOTE: this function assumes the email has already been authenticated
117+
def get_api_key_from_authenticated_email(email):
118+
apikey = Key.query.filter_by(email=email).first()
119+
120+
if apikey and apikey.blacklisted:
121+
return None
122+
123+
if not apikey:
124+
apikey = create_new_apikey(email, db.session)
125+
if not apikey:
126+
raise Exception
127+
return apikey
128+
129+
88130
def authenticate(func):
89131
def wrapper(*args, **kwargs):
90132
apikey = request.headers.get('x-apikey')
91-
key = Key.query.filter_by(apikey=apikey, blacklisted=False).first()
133+
try:
134+
filters = {'apikey': apikey, 'blacklisted': False}
135+
key = Key.query.filter_by(**filters).first() if apikey else jwt_to_key()
136+
except Exception:
137+
return standardize_response(status_code=500)
92138

93139
if not key:
94140
return standardize_response(status_code=401)

0 commit comments

Comments
 (0)