-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2511 from stveit/jwt-amend-library-with-pip-fix
Support JWT token authentication
- Loading branch information
Showing
7 changed files
with
261 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# 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 | ||
# 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. | ||
|
||
# Each section must contain an _aud_ option. NAV will only accept | ||
# tokens from this issuer where the 'aud' claim matches the value of _aud_. | ||
|
||
# The _keytype_ option defines what kind of key the _key_ option contains. | ||
# The value can be either JWKS or PEM. | ||
|
||
# The _key_ option contains the key matching the _keytype_ option. | ||
# If _keytype_ is JWKS, then _key_ must contain a URL to an endpoint | ||
# exposing a JWKS object. If _keytype_ is PEM, then _key_ must contain | ||
# a path to a file that contains a public key in PEM format. | ||
|
||
# Example 1 (Issuer with _keytype_ JWKS) | ||
|
||
# [https://issuer-with-url-name.no] | ||
# aud=nav-instance-url | ||
# keytype=JWKS | ||
# key=https://some-url/jwks | ||
|
||
# Example 2 (Issuer with _keytype_ PEM) | ||
|
||
# [issuer-with-simpler-name] | ||
# aud=nav-instance-url | ||
# keytype=PEM | ||
# key=/some/path/public_key.pem |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import logging | ||
from functools import partial | ||
import configparser | ||
|
||
from nav.config import ConfigurationError, NAVConfigParser | ||
|
||
_logger = logging.getLogger('nav.jwtconf') | ||
|
||
|
||
class JWTConf(NAVConfigParser): | ||
"""jwt.conf config parser""" | ||
|
||
DEFAULT_CONFIG_FILES = ('jwt.conf',) | ||
|
||
def get_issuers_setting(self): | ||
issuers_settings = dict() | ||
for section in self.sections(): | ||
try: | ||
get = partial(self.get, 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) | ||
claims_options = { | ||
'aud': {'values': [aud], 'essential': True}, | ||
} | ||
issuers_settings[section] = { | ||
'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 | ||
|
||
def _read_file(self, file): | ||
with open(file, "r") as f: | ||
return f.read() | ||
|
||
def _validate_key(self, key): | ||
if not key: | ||
raise ConfigurationError("Invalid 'key': 'key' must not be empty") | ||
return key | ||
|
||
def _validate_type(self, key_type): | ||
if key_type not in ['JWKS', 'PEM']: | ||
raise ConfigurationError( | ||
"Invalid 'keytype': 'keytype' must be either 'JWKS' or 'PEM'" | ||
) | ||
return key_type | ||
|
||
def _validate_audience(self, audience): | ||
if not audience: | ||
raise ConfigurationError("Invalid 'aud': 'aud' must not be empty") | ||
return audience |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,3 +35,5 @@ pynetsnmp-2>=0.1.8,<0.2.0 | |
libsass==0.15.1 | ||
|
||
napalm==3.4.1 | ||
|
||
git+https://github.com/Uninett/[email protected]#egg=drf-oidc-auth |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
from mock import patch | ||
from unittest import TestCase | ||
from nav.jwtconf import JWTConf | ||
from nav.config import ConfigurationError | ||
|
||
|
||
class TestJWTConf(TestCase): | ||
def setUp(self): | ||
pass | ||
|
||
def test_valid_jwks_config_should_pass(self): | ||
config = u""" | ||
[jwks-issuer] | ||
keytype=JWKS | ||
aud=nav | ||
key=www.example.com | ||
""" | ||
expected_settings = { | ||
'jwks-issuer': { | ||
'key': 'www.example.com', | ||
'type': 'JWKS', | ||
'claims_options': { | ||
'aud': {'values': ['nav'], 'essential': True}, | ||
}, | ||
} | ||
} | ||
with patch.object(JWTConf, 'DEFAULT_CONFIG', config): | ||
jwtconf = JWTConf() | ||
settings = jwtconf.get_issuers_setting() | ||
self.assertEqual(settings, expected_settings) | ||
|
||
def test_valid_pem_config_should_pass(self): | ||
config = u""" | ||
[pem-issuer] | ||
keytype=PEM | ||
aud=nav | ||
key=key_path | ||
""" | ||
pem_key = "PEM KEY" | ||
expected_settings = { | ||
'pem-issuer': { | ||
'key': pem_key, | ||
'type': 'PEM', | ||
'claims_options': { | ||
'aud': {'values': ['nav'], 'essential': True}, | ||
}, | ||
} | ||
} | ||
|
||
def read_file_patch(self, file): | ||
return pem_key | ||
|
||
with patch.object(JWTConf, 'DEFAULT_CONFIG', config): | ||
with patch.object(JWTConf, '_read_file', read_file_patch): | ||
jwtconf = JWTConf() | ||
settings = jwtconf.get_issuers_setting() | ||
self.assertEqual(settings, expected_settings) | ||
|
||
def test_invalid_ketype_should_fail(self): | ||
config = u""" | ||
[pem-issuer] | ||
keytype=Fake | ||
aud=nav | ||
key=key | ||
""" | ||
with patch.object(JWTConf, 'DEFAULT_CONFIG', config): | ||
jwtconf = JWTConf() | ||
settings = jwtconf.get_issuers_setting() | ||
self.assertEqual(settings, dict()) | ||
|
||
def test_empty_key_should_fail(self): | ||
config = u""" | ||
[pem-issuer] | ||
keytype=JWKS | ||
aud=nav | ||
key= | ||
""" | ||
with patch.object(JWTConf, 'DEFAULT_CONFIG', config): | ||
jwtconf = JWTConf() | ||
settings = jwtconf.get_issuers_setting() | ||
self.assertEqual(settings, dict()) | ||
|
||
def test_empty_aud_should_fail(self): | ||
config = u""" | ||
[pem-issuer] | ||
keytype=JWKS | ||
aud= | ||
key=key | ||
""" | ||
with patch.object(JWTConf, 'DEFAULT_CONFIG', config): | ||
jwtconf = JWTConf() | ||
settings = jwtconf.get_issuers_setting() | ||
self.assertEqual(settings, dict()) | ||
|
||
def test_validate_key_should_raise_error_if_key_is_empty(self): | ||
jwtconf = JWTConf() | ||
with self.assertRaises(ConfigurationError): | ||
jwtconf._validate_key("") | ||
|
||
def test_validate_key_should_allow_non_empty_string(self): | ||
key = "key" | ||
jwtconf = JWTConf() | ||
validated_key = jwtconf._validate_key(key) | ||
self.assertEqual(validated_key, key) | ||
|
||
def test_validate_audience_should_raise_error_if_audience_is_empty(self): | ||
jwtconf = JWTConf() | ||
with self.assertRaises(ConfigurationError): | ||
jwtconf._validate_audience("") | ||
|
||
def test_validate_audience_should_allow_non_empty_string(self): | ||
aud = "key" | ||
jwtconf = JWTConf() | ||
validated_aud = jwtconf._validate_key(aud) | ||
self.assertEqual(validated_aud, aud) | ||
|
||
def test_validate_type_should_raise_error_if_type_is_invalid(self): | ||
jwtconf = JWTConf() | ||
with self.assertRaises(ConfigurationError): | ||
jwtconf._validate_type("invalid") | ||
|
||
def test_JWKS_should_be_a_valid_type(self): | ||
type = "JWKS" | ||
jwtconf = JWTConf() | ||
validated_type = jwtconf._validate_type(type) | ||
self.assertEqual(validated_type, type) | ||
|
||
def test_PEM_should_be_a_valid_type(self): | ||
type = "PEM" | ||
jwtconf = JWTConf() | ||
validated_type = jwtconf._validate_type(type) | ||
self.assertEqual(validated_type, type) |