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

Feature/SK-721 | Add JWT verification (decoding) #543

Merged
merged 16 commits into from
Mar 19, 2024
90 changes: 90 additions & 0 deletions docs/auth.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
.. _auth-label:

Authentication and Authorization (RBAC)
=============================================
.. warning:: The FEDn RBAC system is an experimental feature and may change in the future.

FEDn supports Role-Based Access Control (RBAC) for controlling access to the FEDn API and gRPC endpoints. The RBAC system is based on JSON Web Tokens (JWT) and is implemented using the `jwt` package. The JWT tokens are used to authenticate users and to control access to the FEDn API.
There are two types of JWT tokens used in the FEDn RBAC system:
- Access tokens: Used to authenticate users and to control access to the FEDn API.
- Refresh tokens: Used to obtain new access tokens when the old ones expire.

.. note:: Please note that the FEDn RBAC system is not enabled by default and does not issue JWT tokens. It is used to integrate with external authentication and authorization systems such as FEDn Studio.

FEDn RBAC system is by default configured with four types of roles:
- `admin`: Has full access to the FEDn API. This role is used to manage the FEDn network using the API client or the FEDn CLI.
- `combiner`: Has access to the /add_combiner endpoint in the API.
- `client`: Has access to the /add_client endpoint in the API and various gRPC endpoint to participate in federated learning sessions.

A full list of the "roles to endpoint" mappings for gRPC can be found in the `fedn/network/grpc/auth.py`. For the API, the mappings are defined using custom decorators defined in `fedn/network/api/auth.py`.

.. note:: The roles are handled by a custom claim in the JWT token called `role`. The claim is used to control access to the FEDn API and gRPC endpoints.

To enable the FEDn RBAC system, you need to set the following environment variables in the controller and combiner:

.. envvar:: FEDN_JWT_SECRET_KEY
:type: str
:required: yes
:default: None
:description: The secret key used for JWT token encryption.

.. envvar:: FEDN_JWT_ALGORITHM
:type: str
:required: no
:default: "HS256"
:description: The algorithm used for JWT token encryption.

.. envvar:: FEDN_AUTH_SCHEME
:type: str
:required: no
:default: "Token"
:description: The authentication scheme used in the FEDn API and gRPC interceptors.

For further fexibility, you can also set the following environment variables:

.. envvar:: FEDN_CUSTOM_URL_PREFIX
:type: str
:required: no
:default: None
:description: Add a custom URL prefix used in the FEDn API, such as /internal or /v1.

.. envvar:: FEDN_AUTH_WHITELIST_URL
:type: str
:required: no
:default: None
:description: A URL patterns to the API that should be excluded from the FEDn RBAC system. For example /internal (to enable internal API calls).

.. envvar:: FEDN_JWT_CUSTOM_CLAIM_KEY
:type: str
:required: no
:default: None
:description: The custom claim key used in the JWT token.

.. envvar:: FEDN_JWT_CUSTOM_CLAIM_VALUE
:type: str
:required: no
:default: None
:description: The custom claim value used in the JWT token.


For the client you need to set the following environment variables:

.. envvar:: FEDN_JWT_ACCESS_TOKEN
:type: str
:required: yes
:default: None
:description: The access token used to authenticate the client to the FEDn API.

.. envvar:: FEDN_JWT_REFRESH_TOKEN
:type: str
:required: no
:default: None
:description: The refresh token used to obtain new access tokens when the old ones expire.

.. envvar:: FEDN_AUTH_SCHEME
:type: str
:required: no
:default: "Token"
:description: The authentication scheme used in the FEDn API and gRPC interceptors.

You can also use `--token` flags in the FEDn CLI to set the access token.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
architecture
aggregators
helpers
auth
faq
modules

Expand Down
11 changes: 11 additions & 0 deletions fedn/fedn/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@
global STATESTORE_CONFIG
global MODELSTORAGE_CONFIG

SECRET_KEY = os.environ.get('FEDN_JWT_SECRET_KEY', False)
FEDN_JWT_CUSTOM_CLAIM_KEY = os.environ.get('FEDN_JWT_CUSTOM_CLAIM_KEY', False)
FEDN_JWT_CUSTOM_CLAIM_VALUE = os.environ.get('FEDN_JWT_CUSTOM_CLAIM_VALUE', False)

FEDN_AUTH_WHITELIST_URL_PREFIX = os.environ.get('FEDN_AUTH_WHITELIST_URL_PREFIX', False)
FEDN_JWT_ALGORITHM = os.environ.get('FEDN_JWT_ALGORITHM', 'HS256')
FEDN_AUTH_SCHEME = os.environ.get('FEDN_AUTH_SCHEME', 'Token')
FEDN_AUTH_REFRESH_TOKEN_URI = os.environ.get('FEDN_AUTH_REFRESH_TOKEN_URI', False)
FEDN_AUTH_REFRESH_TOKEN = os.environ.get('FEDN_AUTH_REFRESH_TOKEN', False)
FEDN_CUSTOM_URL_PREFIX = os.environ.get('FEDN_CUSTOM_URL_PREFIX', '')


def get_environment_config():
""" Get the configuration from environment variables.
Expand Down
70 changes: 70 additions & 0 deletions fedn/fedn/network/api/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from functools import wraps

import jwt
from flask import jsonify, request

from fedn.common.config import (FEDN_AUTH_SCHEME,
FEDN_AUTH_WHITELIST_URL_PREFIX,
FEDN_JWT_ALGORITHM, FEDN_JWT_CUSTOM_CLAIM_KEY,
FEDN_JWT_CUSTOM_CLAIM_VALUE, SECRET_KEY)


def check_role_claims(payload, role):
if 'role' not in payload:
return False
if payload['role'] != role:
return False

return True


def check_custom_claims(payload):
if FEDN_JWT_CUSTOM_CLAIM_KEY and FEDN_JWT_CUSTOM_CLAIM_VALUE:
if payload[FEDN_JWT_CUSTOM_CLAIM_KEY] != FEDN_JWT_CUSTOM_CLAIM_VALUE:
return False
return True


def if_whitelisted_url_prefix(path):
if FEDN_AUTH_WHITELIST_URL_PREFIX and path.startswith(FEDN_AUTH_WHITELIST_URL_PREFIX):
return True
else:
return False


def jwt_auth_required(role=None):
def actual_decorator(func):
if not SECRET_KEY:
return func

@wraps(func)
def decorated(*args, **kwargs):
if if_whitelisted_url_prefix(request.path):
return func(*args, **kwargs)
token = request.headers.get('Authorization')
if not token:
return jsonify({'message': 'Missing token'}), 401
# Get token from the header Bearer
if token.startswith(FEDN_AUTH_SCHEME):
token = token.split(' ')[1]
else:
return jsonify({'message':
f'Invalid token scheme, expected {FEDN_AUTH_SCHEME}'
}), 401
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[FEDN_JWT_ALGORITHM])
if not check_role_claims(payload, role):
return jsonify({'message': 'Invalid token'}), 401
if not check_custom_claims(payload):
return jsonify({'message': 'Invalid token'}), 401

except jwt.ExpiredSignatureError:
return jsonify({'message': 'Token expired'}), 401

except jwt.InvalidTokenError:
return jsonify({'message': 'Invalid token'}), 401

return func(*args, **kwargs)

return decorated
return actual_decorator
2 changes: 1 addition & 1 deletion fedn/fedn/network/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def __init__(self, host, port=None, secure=False, verify=False, token=None, auth
# Auth scheme passed as argument overrides environment variable.
# "Token" is the default auth scheme.
if not auth_scheme:
auth_scheme = os.environ.get("FEDN_AUTH_SCHEME", "Token")
auth_scheme = os.environ.get("FEDN_AUTH_SCHEME", "Bearer")
# Override potential env variable if token is passed as argument.
if not token:
token = os.environ.get("FEDN_AUTH_TOKEN", False)
Expand Down
Loading
Loading