| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| # |
| import base64 |
| import json |
| import unittest |
| |
| from cryptography.hazmat.primitives.asymmetric import padding, rsa |
| from cryptography.hazmat.primitives import hashes |
| |
| from openserverless.common.oidc_validator import ( |
| OidcForbiddenError, |
| OidcTokenValidator, |
| OidcValidationError, |
| ) |
| |
| |
| def b64url(data): |
| return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") |
| |
| |
| def int_to_b64url(value): |
| length = (value.bit_length() + 7) // 8 |
| return b64url(value.to_bytes(length, byteorder="big")) |
| |
| |
| class OidcValidatorTest(unittest.TestCase): |
| |
| def setUp(self): |
| self.private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) |
| public_numbers = self.private_key.public_key().public_numbers() |
| self.jwks = { |
| "keys": [ |
| { |
| "kty": "RSA", |
| "kid": "test-key", |
| "alg": "RS256", |
| "use": "sig", |
| "n": int_to_b64url(public_numbers.n), |
| "e": int_to_b64url(public_numbers.e), |
| } |
| ] |
| } |
| self.environ = { |
| "OIDC_ISSUER_URL": "http://localhost:8080/realms/openserverless-lab", |
| "OIDC_AUDIENCE": "openserverless-admin-api", |
| "OIDC_REQUIRED_GROUP": "openserverless-users", |
| "OIDC_USERNAME_CLAIM": "preferred_username", |
| "OIDC_GROUPS_CLAIM": "groups", |
| } |
| |
| def token(self, claims, header=None): |
| header = header or {"alg": "RS256", "kid": "test-key", "typ": "JWT"} |
| encoded_header = b64url(json.dumps(header).encode("utf-8")) |
| encoded_claims = b64url(json.dumps(claims).encode("utf-8")) |
| signed_data = f"{encoded_header}.{encoded_claims}".encode("ascii") |
| signature = self.private_key.sign( |
| signed_data, |
| padding.PKCS1v15(), |
| hashes.SHA256(), |
| ) |
| return f"{encoded_header}.{encoded_claims}.{b64url(signature)}" |
| |
| def valid_claims(self): |
| return { |
| "iss": self.environ["OIDC_ISSUER_URL"], |
| "aud": self.environ["OIDC_AUDIENCE"], |
| "exp": 2000, |
| "preferred_username": "developer.lab", |
| "email": "developer.lab@example.test", |
| "groups": ["openserverless-users"], |
| } |
| |
| def validator(self): |
| return OidcTokenValidator(self.environ, jwks=self.jwks, now=1000) |
| |
| def test_accepts_valid_token(self): |
| claims = self.validator().validate(self.token(self.valid_claims())) |
| |
| self.assertEqual("developer.lab", claims["preferred_username"]) |
| |
| def test_rejects_missing_required_group(self): |
| claims = self.valid_claims() |
| claims["groups"] = [] |
| |
| with self.assertRaises(OidcForbiddenError): |
| self.validator().validate(self.token(claims)) |
| |
| def test_rejects_wrong_audience(self): |
| claims = self.valid_claims() |
| claims["aud"] = "other-client" |
| |
| with self.assertRaises(OidcValidationError): |
| self.validator().validate(self.token(claims)) |
| |
| def test_rejects_expired_token(self): |
| claims = self.valid_claims() |
| claims["exp"] = 900 |
| |
| with self.assertRaises(OidcValidationError): |
| self.validator().validate(self.token(claims)) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |
| |