Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding optional API-Key requirement #18

Merged
merged 7 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ Private key and certificate chain can be also provided as ENVs as given below. I
* Private key: `AS_CLIENT_KEY`
* Certificate chain: `AS_CLIENT_CRT`

When enabling the requirement of an API-Key for the different
endpoints ([config/as.yml](./config/as.yml#L30))), the actual API-Key can be also provided as ENVs:
* iSHARE flow: `AS_APIKEY_ISHARE`
* Trusted-Issuers-Lists flow: `AS_APIKEY_ISSUER`

In case of very large JWTs in the Authorization header, one needs to increase the max. HTTP header size of
gunicorn. This can be done by setting the following ENV (here: max. 32kb):

Expand All @@ -46,8 +51,10 @@ Further ENVs control the execution of the activation service. Below is a list of
| AS_MAX_HEADER_SIZE | 32768 | Maximum header size in bytes |
| AS_LOG_LEVEL | 'info' | Log level |
| AS_DATABASE_URI | | Database URI to use instead of config from configuration file |
| AS_CLIENT_KEY | | iSHARE private key provided as ENV (compare to [config/as.yml](./config/as.yml#L8)) |
| AS_CLIENT_CERTS | | iSHARE certificate chain provided as ENV (compare to [config/as.yml](./config/as.yml#L10)) |
| AS_CLIENT_KEY | | iSHARE private key provided as ENV (compare to [config/as.yml](./config/as.yml#L8)) |
| AS_CLIENT_CERTS | | iSHARE certificate chain provided as ENV (compare to [config/as.yml](./config/as.yml#L10)) |
| AS_APIKEY_ISHARE | | API-Key for iSHARE flow provided as ENV (compare to [config/as.yml](./config/as.yml#L36)) |
| AS_APIKEY_ISSUER | | API-Key for Trusted-Issuers-List flow provided as ENV (compare to [config/as.yml](./config/as.yml#L46)) |


## Usage
Expand Down
14 changes: 14 additions & 0 deletions api/createpolicy.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from flask import Blueprint, Response, current_app, abort, request

from api.util.createpolicy_handler import extract_access_token, get_ar_token, check_create_delegation_evidence, create_delegation_evidence
from api.util.apikey_handler import check_api_key

from api.exceptions.create_policy_exception import CreatePolicyException
from api.exceptions.database_exception import DatabaseException
from api.exceptions.apikey_exception import ApiKeyException

# Blueprint
createpolicy_endpoint = Blueprint("createpolicy_endpoint", __name__)
Expand All @@ -16,6 +18,18 @@ def index():
# Load config
conf = current_app.config['as']

# Check for API-Key
if 'apikeys' in conf:
apikey_conf = conf['apikeys']
if 'ishare' in apikey_conf and apikey_conf['ishare']['enabledCreatePolicy']:
try:
current_app.logger.debug("Checking API-Key...")
check_api_key(request, apikey_conf['ishare']['headerName'], apikey_conf['ishare']['apiKey'])
except ApiKeyException as ake:
current_app.logger.debug("Checking API-Key not successful: {}. Returning status {}.".format(ake.internal_msg, ake.status_code))
abort(ake.status_code, ake.public_msg)
current_app.logger.debug("... API-Key accepted")

# Get access token from request header
token = None
try:
Expand Down
5 changes: 5 additions & 0 deletions api/exceptions/apikey_exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from api.exceptions.as_exception import ActivationServiceException

class ApiKeyException(ActivationServiceException):

pass
22 changes: 18 additions & 4 deletions api/issuer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from api.util.issuer_handler import extract_access_token, get_samedevice_redirect_url
from api.util.issuer_handler import decode_token_with_jwk, forward_til_request
from api.util.issuer_handler import check_create_role, check_update_role, check_delete_role
from api.util.apikey_handler import check_api_key
import time

from api.exceptions.issuer_exception import IssuerException
from api.exceptions.database_exception import DatabaseException
from api.exceptions.apikey_exception import ApiKeyException

# Blueprint
issuer_endpoint = Blueprint("issuer_endpoint", __name__)
Expand All @@ -17,7 +19,19 @@ def index():

# Load config
conf = current_app.config['as']


# Check for API-Key
if 'apikeys' in conf:
apikey_conf = conf['apikeys']
if 'ishare' in apikey_conf and apikey_conf['issuer']['enabledIssuer']:
try:
current_app.logger.debug("Checking API-Key...")
check_api_key(request, apikey_conf['issuer']['headerName'], apikey_conf['issuer']['apiKey'])
except ApiKeyException as ake:
current_app.logger.debug("Checking API-Key not successful: {}. Returning status {}.".format(ake.internal_msg, ake.status_code))
abort(ake.status_code, ake.public_msg)
current_app.logger.debug("... API-Key accepted")

# Check for access token JWT in request header
request_token = None
try:
Expand All @@ -42,7 +56,7 @@ def index():

# Received JWT in Authorization header
current_app.logger.debug("...received access token JWT in incoming request: {}".format(request_token))

# Validate JWT with verifier JWKS
payload = None
try:
Expand All @@ -53,7 +67,7 @@ def index():
current_app.logger.debug("Error when validating/decoding: {}. Returning status {}.".format(die.internal_msg, die.status_code))
abort(die.status_code, die.public_msg)
current_app.logger.debug("... decoded token payload: {}".format(payload))

# Check TIL access depending on HTTP method
if request.method == 'POST':
# POST: Create issuer flow
Expand Down Expand Up @@ -100,7 +114,7 @@ def index():
else:
# should not happen
abort(500, "Invalid HTTP method")

# Forward request to TIL
current_app.logger.debug("... access granted!")
try:
Expand Down
14 changes: 14 additions & 0 deletions api/token.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from flask import Blueprint, Response, current_app, abort, request
from api.util.token_handler import forward_token
from api.util.apikey_handler import check_api_key
import time

from api.exceptions.token_exception import TokenException
from api.exceptions.database_exception import DatabaseException
from api.exceptions.apikey_exception import ApiKeyException

# Blueprint
token_endpoint = Blueprint("token_endpoint", __name__)
Expand All @@ -15,6 +17,18 @@ def index():

# Load config
conf = current_app.config['as']

# Check for API-Key
if 'apikeys' in conf:
apikey_conf = conf['apikeys']
if 'ishare' in apikey_conf and apikey_conf['ishare']['enabledToken']:
try:
current_app.logger.debug("Checking API-Key...")
check_api_key(request, apikey_conf['ishare']['headerName'], apikey_conf['ishare']['apiKey'])
except ApiKeyException as ake:
current_app.logger.debug("Checking API-Key not successful: {}. Returning status {}.".format(ake.internal_msg, ake.status_code))
abort(ake.status_code, ake.public_msg)
current_app.logger.debug("... API-Key accepted")

# Forward token
auth_data = None
Expand Down
19 changes: 19 additions & 0 deletions api/util/apikey_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from api.exceptions.apikey_exception import ApiKeyException

# Check API-Key in request
def check_api_key(request, header_name, api_key):

# Get header
auth_header = request.headers.get(header_name)
if not auth_header:
message = "Missing API-Key header"
internal_msg = message + " ('{}')".format(header_name)
raise ApiKeyException(message, internal_msg, 400)

# Check API-Keys
if auth_header != api_key:
msg = "Invalid API-Key"
int_msg = msg + " (provided '{}' != expected '{}')".format(auth_header, api_key)
raise ApiKeyException(msg, int_msg, 400)

return True
2 changes: 1 addition & 1 deletion api/util/issuer_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def forward_til_request(request, conf):
headers = {k:v for k,v in request.headers if k != "Authorization" and k.lower() != 'host'}
url = request.url.replace(request.host_url, f'{til_uri}/')
data = request.get_data()

# Forward request
response = requests.request(
method = request.method,
Expand Down
21 changes: 21 additions & 0 deletions config/as.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ db:
# Enable SQL logging to stderr
echo: true

# Configuration for additional API keys to protect certain endpoints
apikeys:
# Config for iSHARE flow
ishare:
# Header name
headerName: "AS-API-KEY"
# API key (auto-generated if left empty)
apiKey: ""
# Enable for /token endpoint (API key will be required)
enabledToken: true
# Enable for /createpolicy endpoint (API key will be required)
enabledCreatePolicy: false
# Config for Trusted-Issuers-List flow
issuer:
# Header name
headerName: "AS-API-KEY"
# API key (auto-generated if left empty)
apiKey: ""
# Enable for /issuer endpoint (API key will be required)
enabledIssuer: true

# Configuration of iSHARE authorisation registry
ar:
# Endpoint for token request
Expand Down
46 changes: 46 additions & 0 deletions tests/config/as.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,27 @@ db:
# Enable SQL logging to stderr
echo: false

# Configuration for additional API keys to protect certain endpoints
apikeys:
# Config for iSHARE flow
ishare:
# Header name
headerName: "AS-API-KEY"
# API key (auto-generated if left empty)
apiKey: "31f5247c-17e5-4969-95f0-928c8ab16504"
# Enable for /token endpoint (API key will be required)
enabledToken: true
# Enable for /createpolicy endpoint (API key will be required)
enabledCreatePolicy: false
# Config for Trusted-Issuers-List flow
issuer:
# Header name
headerName: "AS-API-KEY"
# API key (auto-generated if left empty)
apiKey: "eb4675ed-860e-4de1-a9a7-3e2e4356d08d"
# Enable for /issuer endpoint (API key will be required)
enabledIssuer: true

# Configuration of authorisation registry
ar:
# Endpoint for token request
Expand All @@ -192,3 +213,28 @@ ar:
# EORI of AR
id: "EU.EORI.DEPROVIDER"

# Configuration specific to Trusted Issuer List /issuer endpoint
issuer:
# clientId parameter
clientId: "some-id"
# Provider DID
providerId: "did:web:packetdelivery.dsba.fiware.dev:did"
# URI of Trusted Issuers List service
tilUri: "http://til.internal"
# URI of verifier
verifierUri: "https://verifier.packetdelivery.net"
# samedevice flow path
samedevicePath: "/api/v1/samedevice"
# JWKS path
jwksPath: "/.well-known/jwks"
# Allowed algorithms for JWT signatures
algorithms:
- "ES256"
# Roles config
roles:
# Role for creating trusted issuer
createRole: "CREATE_ISSUER"
# Role for updating trusted issuer
updateRole: "UPDATE_ISSUER"
# Role for deleting trusted issuer
deleteRole: "DELETE_ISSUER"
81 changes: 81 additions & 0 deletions tests/pytest/test_apikey_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import pytest
from api import app
from tests.pytest.util.config_handler import load_config
from api.util.apikey_handler import check_api_key

from api.exceptions.apikey_exception import ApiKeyException

# Get AS config
as_config = load_config("tests/config/as.yml", app)
app.config['as'] = as_config

@pytest.fixture
def mock_request_apikey_ok_ishare(mocker):
def headers_get(attr):
if attr == "AS-API-KEY": return "31f5247c-17e5-4969-95f0-928c8ab16504"
else: return None
request = mocker.Mock()
request.headers.get.side_effect = headers_get
return request

@pytest.fixture
def mock_request_apikey_ok_issuer(mocker):
def headers_get(attr):
if attr == "AS-API-KEY": return "eb4675ed-860e-4de1-a9a7-3e2e4356d08d"
else: return None
request = mocker.Mock()
request.headers.get.side_effect = headers_get
return request

@pytest.fixture
def mock_request_apikey_invalid_header(mocker):
def headers_get(attr):
if attr == "AS-API-KEY": return "abc"
else: return None
request = mocker.Mock()
request.headers.get.side_effect = headers_get
return request

@pytest.fixture
def mock_request_apikey_no_headers(mocker):
def headers_get(attr):
return None
request = mocker.Mock()
request.headers.get.side_effect = headers_get
return request

@pytest.mark.ok
@pytest.mark.it('should successfully check API-Key for iSHARE flow')
def test_apikey_ok_ishare(mock_request_apikey_ok_ishare):

# Call function with request mock
try:
check_api_key(mock_request_apikey_ok_ishare, "AS-API-KEY", "31f5247c-17e5-4969-95f0-928c8ab16504")
except Exception as ex:
pytest.fail("should throw no exception: {}".format(ex))

@pytest.mark.ok
@pytest.mark.it('should successfully check API-Key for TIL flow')
def test_apikey_ok_issuer(mock_request_apikey_ok_issuer):

# Call function with request mock
try:
check_api_key(mock_request_apikey_ok_issuer, "AS-API-KEY", "eb4675ed-860e-4de1-a9a7-3e2e4356d08d")
except Exception as ex:
pytest.fail("should throw no exception: {}".format(ex))

@pytest.mark.failure
@pytest.mark.it('should throw exception about missing API-Key header')
def test_check_missing_header(mock_request_apikey_no_headers):

# Call function
with pytest.raises(ApiKeyException, match=r'Missing API-Key header') as ex:
check_api_key(mock_request_apikey_no_headers, "AS-API-KEY", "eb4675ed-860e-4de1-a9a7-3e2e4356d08d")

@pytest.mark.failure
@pytest.mark.it('should throw exception about invalid API-Key')
def test_check_invalid_header(mock_request_apikey_invalid_header):

# Call function
with pytest.raises(ApiKeyException, match=r'Invalid API-Key') as ex:
check_api_key(mock_request_apikey_invalid_header, "AS-API-KEY", "eb4675ed-860e-4de1-a9a7-3e2e4356d08d")
Loading