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

Add config for local JWT tokens #2568

Merged
merged 38 commits into from Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
431e92e
Add config for nav issued tokens
stveit Feb 6, 2023
a34c1ba
Validate issuer name
stveit Feb 6, 2023
0763c00
Make log message match the context
stveit Feb 6, 2023
8d38a8b
Add default config
stveit Feb 6, 2023
c2044bb
Shorten error message
stveit Feb 6, 2023
81d0234
Rename function to be more distinct
stveit Feb 6, 2023
25f116f
Return blank config if any error
stveit Feb 6, 2023
b50dcd3
Remove default name value
stveit Feb 6, 2023
cc67a46
Remove redundant tests
stveit Feb 6, 2023
285451b
Add more tests
stveit Feb 6, 2023
57b5f85
Move external settings parsing to new function
stveit Feb 6, 2023
10b9f85
Use string format
stveit Feb 6, 2023
d1a3f19
Wrap errors as ConfigurationError
stveit Feb 8, 2023
fff9a7c
Add new test
stveit Feb 8, 2023
675e099
Test internal/external error separately
stveit Feb 8, 2023
0b83370
Add tests for more coverage
stveit Feb 9, 2023
1655b70
Add test for correctly reading file
stveit Feb 9, 2023
ff9f599
Rename nav internal token config
stveit Feb 22, 2023
913ff72
Fix config comments using incorrent names
stveit Feb 22, 2023
800ee6a
Explain config option directly above the option
stveit Feb 22, 2023
683e24f
Reword comment
stveit Feb 22, 2023
de26c68
Use built in logging message formatting
stveit Feb 22, 2023
d7c784d
Handle permission error for reading pem keys
stveit Feb 22, 2023
17f8471
Add colon to error message
stveit Feb 22, 2023
2a3a0fe
Add test for permissionerror
stveit Feb 22, 2023
714c4af
use %s instead of {}
stveit Feb 22, 2023
12d43ea
Separate exception for permission/notfound
stveit Mar 9, 2023
025a25c
Allow local jwt tokens to not be configured
stveit May 24, 2023
f201c0d
Do not have config for local jwt token by default
stveit May 24, 2023
b2e2f52
Add test for empty config
stveit May 24, 2023
5d5b88e
Pass issuer validation if local nav name is not set
stveit May 24, 2023
5a28a3f
Handle only expected configparser exceptions
stveit May 24, 2023
8739935
Update tests for allowing non-configured local jwt
stveit May 24, 2023
35079b7
Update test names
stveit May 24, 2023
ef3aded
Move config file to webfront directory
stveit Sep 4, 2023
1e99af8
Update docstrings
stveit Sep 4, 2023
b381bba
Add type hints
stveit Sep 4, 2023
8c8e00f
Revise docstring
stveit Sep 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 16 additions & 4 deletions python/nav/etc/jwt.conf
@@ -1,7 +1,19 @@
# This file contains configuration for JWT issuers that this NAV instance
# should accept tokens from.

# Each issuer is defined in a section. The name of the section should be
# This file contains configuration for JWT tokens.
# This includes tokens generated by NAV itself and external issuers.

# The settings in the `nav` section are for tokens issued by NAV itself.
# Comment this in and fill out the values if you want to activate local JWT tokens.
# The section name should NOT be changed.

#[nav]
# Absolute path to private key in PEM format
#private_key=
# Absolute path to public key in PEM format.
#public_key=
# Used for the 'iss' and 'aud' claims of generated tokens.
#name=

# Custom JWT issuers can be defined in a section. The name of the section should be
# the issuers name, the same value as the 'iss' claim will be in tokens
# generated by this issuer. This value is often a URL, but does not
# have to be.
Expand Down
104 changes: 93 additions & 11 deletions python/nav/jwtconf.py
Expand Up @@ -11,32 +11,58 @@ class JWTConf(NAVConfigParser):
"""jwt.conf config parser"""

DEFAULT_CONFIG_FILES = ('jwt.conf',)
NAV_SECTION = "nav"

def get_issuers_setting(self):
issuers_settings = dict()
for section in self.sections():
try:
try:
external_settings = self._get_settings_for_external_tokens()
local_settings = self._get_settings_for_nav_issued_tokens()
external_settings.update(local_settings)
return external_settings
except ConfigurationError as error:
_logger.error('Error reading jwtconfig: %s', error)
return dict()
lunkwill42 marked this conversation as resolved.
Show resolved Hide resolved
lunkwill42 marked this conversation as resolved.
Show resolved Hide resolved

def _get_settings_for_external_tokens(self):
settings = dict()
try:
for section in self.sections():
if section == self.NAV_SECTION:
continue
get = partial(self.get, section)
issuer = self._validate_issuer(section)
key = self._validate_key(get('key'))
aud = self._validate_audience(get('aud'))
key_type = self._validate_type(get('keytype'))
if key_type == 'PEM':
key = self._read_file(key)
key = self._read_key_from_path(key)
claims_options = {
'aud': {'values': [aud], 'essential': True},
}
issuers_settings[section] = {
settings[issuer] = {
'key': key,
'type': key_type,
'claims_options': claims_options,
}
except (configparser.Error, ConfigurationError) as error:
_logger.error('Error collecting stats for %s: %s', section, error)
return issuers_settings
except (
configparser.NoSectionError,
configparser.NoOptionError,
) as error:
raise ConfigurationError(error)
hmpf marked this conversation as resolved.
Show resolved Hide resolved
return settings

def _read_file(self, file):
with open(file, "r") as f:
return f.read()
def _read_key_from_path(self, path):
hmpf marked this conversation as resolved.
Show resolved Hide resolved
try:
with open(path, "r") as f:
return f.read()
except FileNotFoundError:
raise ConfigurationError(
"Could not find file %s to read PEM key from", path
)
except PermissionError:
raise ConfigurationError(
"Could not access file %s to read PEM key from", path
)

def _validate_key(self, key):
if not key:
Expand All @@ -50,7 +76,63 @@ def _validate_type(self, key_type):
)
return key_type

def _validate_issuer(self, section):
if not section:
raise ConfigurationError("Invalid 'issuer': 'issuer' must not be empty")
try:
nav_name = self.get_nav_name()
except ConfigurationError:
pass
else:
if section == nav_name:
raise ConfigurationError(
"Invalid 'issuer': %s collides with internal issuer name %s",
section,
)
return section

def _validate_audience(self, audience):
if not audience:
raise ConfigurationError("Invalid 'aud': 'aud' must not be empty")
return audience

def _get_nav_token_config_option(self, option):
try:
get = partial(self.get, self.NAV_SECTION)
return get(option)
except (
configparser.NoSectionError,
configparser.NoOptionError,
) as error:
raise ConfigurationError(error)

def get_nav_private_key(self):
path = self._get_nav_token_config_option('private_key')
return self._read_key_from_path(path)

def get_nav_public_key(self):
path = self._get_nav_token_config_option('public_key')
return self._read_key_from_path(path)

def get_nav_name(self):
name = self._get_nav_token_config_option('name')
if not name:
raise ConfigurationError("Invalid 'name': 'name' must not be empty")
return name

def _get_settings_for_nav_issued_tokens(self):
if not self.has_section(self.NAV_SECTION):
return {}
name = self.get_nav_name()
claims_options = {
'aud': {'values': [name], 'essential': True},
'token_type': {'values': ['access_token'], 'essential': True},
}
settings = {
name: {
'type': "PEM",
'key': self.get_nav_public_key(),
'claims_options': claims_options,
}
}
return settings