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

Flexible jwt #2131

Merged
merged 3 commits into from Mar 25, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/installation/system/inifile.rst
Expand Up @@ -180,6 +180,10 @@ This entry means, that the private key, that corresponds to the given
public key can sign a JWT, that can impersonate as the *userA* in resolver
*resolverX* in *realmA*.

.. note:: The ``username`` can be a regular expression like ".*".
This way you could allow a private signing key to impersonate every
user in a realm. (Starting with version 3.3)

A JWT can be created like this::

auth_token = jwt.encode(payload={"role": "user",
Expand Down
15 changes: 11 additions & 4 deletions privacyidea/api/lib/utils.py
Expand Up @@ -291,6 +291,7 @@ def verify_auth_token(auth_token, required_role=None):

headers = jwt.get_unverified_header(auth_token)
algorithm = headers.get("alg")
wrong_username = None
if algorithm in TRUSTED_JWT_ALGOS:
# The trusted JWTs are RSA, PSS or eliptic curve signed
trusted_jwts = current_app.config.get("PI_TRUSTED_JWT", [])
Expand All @@ -300,10 +301,13 @@ def verify_auth_token(auth_token, required_role=None):
j = jwt.decode(auth_token,
trusted_jwt.get("public_key"),
algorithms=TRUSTED_JWT_ALGOS)
if dict((k, j.get(k)) for k in ("role", "username", "resolver", "realm")) == \
dict((k, trusted_jwt.get(k)) for k in ("role", "username", "resolver", "realm")):
r = j
break
if dict((k, j.get(k)) for k in ("role", "resolver", "realm")) == \
dict((k, trusted_jwt.get(k)) for k in ("role", "resolver", "realm")):
if re.match(trusted_jwt.get("username") + "$", j.get("username")):
r = j
break
else:
r = wrong_username = j.get("username")
else:
log.warning(u"Unsupported JWT algorithm in PI_TRUSTED_JWT.")
except jwt.DecodeError as err:
Expand All @@ -322,6 +326,9 @@ def verify_auth_token(auth_token, required_role=None):
except jwt.ExpiredSignature as err:
raise AuthError(_("Authentication failure. Your token has expired: {0!s}").format(err),
id=ERROR.AUTHENTICATE_TOKEN_EXPIRED)
if wrong_username:
raise AuthError(_("Authentication failure. The username {0!s} is not allowed to "
"impersonate via JWT.".format(wrong_username)))
if required_role and r.get("role") not in required_role:
# If we require a certain role like "admin", but the users role does
# not match
Expand Down
6 changes: 6 additions & 0 deletions privacyidea/config.py
Expand Up @@ -86,6 +86,12 @@ class TestingConfig(Config):
"realm": "realm1",
"username": "userA",
"resolver": "resolverX"},
{"public_key": pubtest_key,
"algorithm": "RS256",
"role": "user",
"realm": "realmX",
"resolver": "resolverX",
"username": "h.*s"},
{"public_key": pubtest_key,
"algorithm": "RS256",
"role": "user",
Expand Down
37 changes: 37 additions & 0 deletions tests/test_api_lib_utils.py
Expand Up @@ -53,6 +53,42 @@ def test_03_verify_auth_token(self):
with open("tests/testdata/jwt_sign.key", "r") as f:
key = f.read()

# successful authentication with wildcard user, starting with an "h" and ending with "s"
auth_token = jwt.encode(payload={"role": "user",
"username": "hans",
"realm": "realmX",
"resolver": "resolverX"},
key=key,
algorithm="RS256")
r = verify_auth_token(auth_token=auth_token,
required_role="user")
self.assertEqual(r.get("realm"), "realmX")
self.assertEqual(r.get("username"), "hans")
self.assertEqual(r.get("resolver"), "resolverX", )
self.assertEqual(r.get("role"), "user")

# A user starting with hans and ending with "t" is not allowed
auth_token = jwt.encode(payload={"role": "user",
"username": "hanswurst",
"realm": "realmX",
"resolver": "resolverX"},
key=key,
algorithm="RS256")
self.assertRaisesRegexp(AuthError, "The username hanswurst is not allowed to impersonate via JWT.",
verify_auth_token, auth_token=auth_token, required_role="user")

# A user ending with hans is not allowed
# A user starting with hans and ending with "t" is not allowed
auth_token = jwt.encode(payload={"role": "user",
"username": "kleinerhans",
"realm": "realmX",
"resolver": "resolverX"},
key=key,
algorithm="RS256")
self.assertRaisesRegexp(AuthError, "The username kleinerhans is not allowed to impersonate via JWT.",
verify_auth_token, auth_token=auth_token, required_role="user")

# Successful authentication with dedicated user
with mock.patch("logging.Logger.warning") as mock_log:
auth_token = jwt.encode(payload={"role": "user",
"username": "userA",
Expand All @@ -66,6 +102,7 @@ def test_03_verify_auth_token(self):
self.assertEqual(r.get("username"), "userA")
self.assertEqual(r.get("resolver"), "resolverX",)
self.assertEqual(r.get("role"), "user")
# ...but there is an unsupported configuration
mock_log.assert_called_once_with("Unsupported JWT algorithm in PI_TRUSTED_JWT.")

# The signature has expired
Expand Down