Skip to content

Commit 25b5f8e

Browse files
committed
Add service token authentication mechanism
1 parent b61c652 commit 25b5f8e

File tree

5 files changed

+105
-12
lines changed

5 files changed

+105
-12
lines changed

openshift/template.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ objects:
9595
configMapKeyRef:
9696
name: bayesian-config
9797
key: keycloak-url
98+
- name: BAYESIAN_AUTH_PUBLIC_KEYS_URL
99+
valueFrom:
100+
configMapKeyRef:
101+
name: bayesian-config
102+
key: auth-url
98103
- name: BAYESIAN_JWT_AUDIENCE
99104
value: "fabric8-online-platform,openshiftio-public"
100105
image: "${DOCKER_REGISTRY}/${DOCKER_IMAGE}:${IMAGE_TAG}"

src/auth.py

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55
import jwt
66
from os import getenv
77

8-
98
from exceptions import HTTPError
10-
from utils import fetch_public_key
9+
from utils import fetch_public_key, fetch_service_public_keys
1110

1211

13-
def decode_token(token):
12+
def decode_user_token(token):
1413
"""Decode the authorization token read from the request header."""
1514
if token is None:
1615
return {}
@@ -38,6 +37,40 @@ def decode_token(token):
3837
return decoded_token
3938

4039

40+
def decode_service_token(token): # pragma: no cover
41+
"""Decode OSIO service token."""
42+
# TODO: Merge this function and user token function once audience is removed from user tokens.
43+
if token is None:
44+
return {}
45+
46+
if token.startswith('Bearer '):
47+
_, token = token.split(' ', 1)
48+
49+
pub_keys = fetch_service_public_keys(current_app)
50+
decoded_token = None
51+
52+
# Since we have multiple public keys, we need to verify against every public key.
53+
# Token can be decoded by any one of the available public keys.
54+
for pub_key in pub_keys:
55+
try:
56+
pub_key = pub_key.get("key", "")
57+
pub_key = '-----BEGIN PUBLIC KEY-----\n{pkey}\n-----END PUBLIC KEY-----'\
58+
.format(pkey=pub_key)
59+
decoded_token = jwt.decode(token, pub_key, algorithms=['RS256'])
60+
except jwt.InvalidTokenError:
61+
current_app.logger.error("Auth token couldn't be decoded for public key: {}"
62+
.format(pub_key))
63+
decoded_token = None
64+
65+
if decoded_token:
66+
break
67+
68+
if not decoded_token:
69+
raise jwt.InvalidTokenError('Auth token cannot be verified.')
70+
71+
return decoded_token
72+
73+
4174
def get_token_from_auth_header():
4275
"""Get the authorization token read from the request header."""
4376
return request.headers.get('Authorization')
@@ -62,7 +95,37 @@ def wrapper(*args, **kwargs):
6295
lgr = current_app.logger
6396

6497
try:
65-
decoded = decode_token(get_token_from_auth_header())
98+
decoded = decode_user_token(get_token_from_auth_header())
99+
if not decoded:
100+
lgr.exception('Provide an Authorization token with the API request')
101+
raise HTTPError(401, 'Authentication failed - token missing')
102+
103+
lgr.info('Successfuly authenticated user {e} using JWT'.
104+
format(e=decoded.get('email')))
105+
except jwt.ExpiredSignatureError as exc:
106+
lgr.exception('Expired JWT token')
107+
raise HTTPError(401, 'Authentication failed - token has expired') from exc
108+
except Exception as exc:
109+
lgr.exception('Failed decoding JWT token')
110+
raise HTTPError(401, 'Authentication failed - could not decode JWT token') from exc
111+
112+
return view(*args, **kwargs)
113+
114+
return wrapper
115+
116+
117+
def service_token_required(view): # pragma: no cover
118+
"""Check if the request contains a valid service token."""
119+
@wraps(view)
120+
def wrapper(*args, **kwargs):
121+
# Disable authentication for local setup
122+
if getenv('DISABLE_AUTHENTICATION') in ('1', 'True', 'true'):
123+
return view(*args, **kwargs)
124+
125+
lgr = current_app.logger
126+
127+
try:
128+
decoded = decode_service_token(get_token_from_auth_header())
66129
if not decoded:
67130
lgr.exception('Provide an Authorization token with the API request')
68131
raise HTTPError(401, 'Authentication failed - token missing')

src/rest_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from flask_cors import CORS
55
from utils import DatabaseIngestion, scan_repo, validate_request_data, retrieve_worker_result
66
from f8a_worker.setup_celery import init_selinon
7-
from auth import login_required
7+
from auth import login_required, service_token_required
88
from exceptions import HTTPError
99

1010
app = Flask(__name__)
@@ -160,7 +160,7 @@ def user_repo_scan():
160160

161161

162162
@app.route('/api/v1/user-repo/notify', methods=['POST'])
163-
@login_required
163+
@service_token_required
164164
def notify_user():
165165
"""
166166
Endpoint for notifying security vulnerability in a repository.

src/utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,28 @@ def fetch_public_key(app):
292292
app.public_key = None
293293

294294
return app.public_key
295+
296+
297+
def fetch_service_public_keys(app): # pragma: no cover
298+
"""Get public keys for OSIO service account. Currently, there are three public keys."""
299+
if not getattr(app, "service_public_keys", []):
300+
auth_url = os.getenv('BAYESIAN_AUTH_PUBLIC_KEYS_URL', '')
301+
if auth_url:
302+
try:
303+
auth_url = auth_url.strip('/') + '/api/token/keys?format=pem'
304+
result = requests.get(auth_url, timeout=0.5)
305+
app.logger.info('Fetching public key from %s, status %d, result: %s',
306+
auth_url, result.status_code, result.text)
307+
except requests.exceptions.Timeout:
308+
app.logger.error('Timeout fetching public key from %s', auth_url)
309+
return ''
310+
if result.status_code != 200:
311+
return ''
312+
313+
keys = result.json().get('keys', [])
314+
app.service_public_keys = keys
315+
316+
else:
317+
app.service_public_keys = None
318+
319+
return app.service_public_keys

tests/test_auth.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,39 +68,39 @@ def mocked_get_audiences_3():
6868
@patch("auth.fetch_public_key", side_effect=mocked_fetch_public_key_1)
6969
def test_decode_token_invalid_input_1(mocked_fetch_public_key, mocked_get_audiences):
7070
"""Test the invalid input handling during token decoding."""
71-
assert decode_token(None) == {}
71+
assert decode_user_token(None) == {}
7272

7373

7474
@patch("auth.get_audiences", side_effect=mocked_get_audiences)
7575
@patch("auth.fetch_public_key", side_effect=mocked_fetch_public_key_1)
7676
def test_decode_token_invalid_input_2(mocked_fetch_public_key, mocked_get_audiences):
7777
"""Test the invalid input handling during token decoding."""
7878
with pytest.raises(Exception):
79-
assert decode_token("Foobar") is None
79+
assert decode_user_token("Foobar") is None
8080

8181

8282
@patch("auth.get_audiences", side_effect=mocked_get_audiences)
8383
@patch("auth.fetch_public_key", side_effect=mocked_fetch_public_key_1)
8484
def test_decode_token_invalid_input_3(mocked_fetch_public_key, mocked_get_audiences):
8585
"""Test the invalid input handling during token decoding."""
8686
with pytest.raises(Exception):
87-
assert decode_token("Bearer ") is None
87+
assert decode_user_token("Bearer ") is None
8888

8989

9090
@patch("auth.get_audiences", side_effect=mocked_get_audiences)
9191
@patch("auth.fetch_public_key", side_effect=mocked_fetch_public_key_2)
9292
def test_decode_token_invalid_input_4(mocked_fetch_public_key, mocked_get_audiences):
9393
"""Test the invalid input handling during token decoding."""
9494
with pytest.raises(Exception):
95-
assert decode_token("Bearer ") is None
95+
assert decode_user_token("Bearer ") is None
9696

9797

9898
@patch("auth.get_audiences", side_effect=mocked_get_audiences_2)
9999
@patch("auth.fetch_public_key", side_effect=mocked_fetch_public_key_2)
100100
def test_decode_token_invalid_input_5(mocked_fetch_public_key, mocked_get_audiences):
101101
"""Test the handling wrong JWT tokens."""
102102
with pytest.raises(Exception):
103-
assert decode_token("Bearer something") is None
103+
assert decode_user_token("Bearer something") is None
104104

105105

106106
@patch("auth.get_audiences", side_effect=mocked_get_audiences_3)
@@ -112,7 +112,7 @@ def test_decode_token_invalid_input_6(mocked_fetch_public_key, mocked_get_audien
112112
'aud': 'foo:bar'
113113
}
114114
token = jwt.encode(payload, PRIVATE_KEY, algorithm='RS256').decode("utf-8")
115-
assert decode_token(token) is not None
115+
assert decode_user_token(token) is not None
116116

117117

118118
def test_audiences():

0 commit comments

Comments
 (0)