├── scripts ├── release.sh ├── build_package.sh ├── setup_dev_env.sh └── format.sh ├── requirements.txt ├── mypy.ini ├── src └── JWTLibrary │ ├── version.py │ ├── keywords │ ├── __init__.py │ ├── token_generation.py │ ├── token_decoding.py │ ├── utilities.py │ └── token_validation.py │ ├── __init__.py │ ├── exceptions.py │ ├── constants.py │ ├── utils.py │ ├── jwt_library.py │ └── cli.py ├── generate.py ├── requirements-dev.txt ├── .pre-commit-config.yaml ├── tox.ini ├── .github └── workflows │ ├── publish-pypi.yml │ └── ci.yml ├── Makefile ├── .gitignore ├── pytest.ini ├── setup.py ├── pyproject.toml ├── README.md ├── example ├── basic_usage │ └── example.robot └── real_world │ └── api_testing_example.robot └── tests ├── robot ├── performance │ └── jwt_performance.robot ├── acceptance │ ├── jwt_basic_robot_test.robot │ └── jwt_error_handling.robot └── integration │ └── jwt_integration.robot └── unit └── test_jwt_library.py /scripts/release.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/build_package.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/setup_dev_env.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyJWT>=2.0.0 2 | robotframework>=4.0.0 -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.11 3 | warn_return_any = True 4 | warn_unused_configs = True 5 | disallow_untyped_defs = True 6 | ignore_missing_imports = True 7 | show_error_codes = True 8 | show_error_context = True 9 | pretty = True 10 | 11 | [mypy-tests.*] 12 | disallow_untyped_defs = False 13 | 14 | [mypy-setup] 15 | ignore_errors = True 16 | -------------------------------------------------------------------------------- /src/JWTLibrary/version.py: -------------------------------------------------------------------------------- 1 | """Version information for JWT Library.""" 2 | 3 | __version__ = "1.0.0" 4 | __author__ = "JWT Robot Framework Library Team" 5 | __email__ = "support@example.com" 6 | __license__ = "Apache 2.0" 7 | __copyright__ = "Copyright 2024 JWT Robot Framework Library Team" 8 | 9 | # Version info tuple 10 | VERSION_INFO = tuple(map(int, __version__.split("."))) 11 | -------------------------------------------------------------------------------- /generate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from os.path import join, dirname 3 | try: 4 | from robot.libdoc import libdoc 5 | except: 6 | def main(): 7 | print("""Robot Framework 2.7 or later required for generating documentation""") 8 | else: 9 | def main(): 10 | libdoc(join(dirname(__file__),'src/JWTLibrary'), 11 | join(dirname(__file__),'docs','JWTLibrary.html')) 12 | 13 | if __name__ == '__main__': 14 | main() -------------------------------------------------------------------------------- /src/JWTLibrary/keywords/__init__.py: -------------------------------------------------------------------------------- 1 | """Keywords package for JWT Library.""" 2 | 3 | from .token_decoding import TokenDecodingKeywords 4 | from .token_generation import TokenGenerationKeywords 5 | from .token_validation import TokenValidationKeywords 6 | from .utilities import UtilityKeywords 7 | 8 | __all__ = [ 9 | "TokenGenerationKeywords", 10 | "TokenDecodingKeywords", 11 | "TokenValidationKeywords", 12 | "UtilityKeywords", 13 | ] 14 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | # Testing 4 | pytest>=6.0.0 5 | pytest-cov>=2.10.0 6 | pytest-mock>=3.6.0 7 | robotframework-lint>=1.1 8 | 9 | # Code Quality 10 | black>=21.0.0 11 | flake8>=3.8.0 12 | mypy>=0.800 13 | isort>=5.9.0 14 | 15 | # Documentation 16 | sphinx>=4.0.0 17 | sphinx-rtd-theme>=0.5.0 18 | myst-parser>=0.15.0 19 | 20 | # Development Tools 21 | pre-commit>=2.15.0 22 | tox>=3.24.0 23 | 24 | # Optional crypto support 25 | cryptography>=3.0.0 -------------------------------------------------------------------------------- /src/JWTLibrary/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | JWT Library for Robot Framework 3 | 4 | This library provides keywords for JSON Web Token (JWT) operations in Robot Framework tests. 5 | 6 | Author: JWT Robot Framework Library 7 | Version: 1.0.0 8 | License: Apache 2.0 9 | """ 10 | 11 | from .jwt_library import JWTLibrary 12 | from .version import __version__ 13 | 14 | __all__ = ["JWTLibrary", "__version__"] 15 | 16 | # Make the library class available at package level 17 | JWTLibrary = JWTLibrary 18 | -------------------------------------------------------------------------------- /scripts/format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # scripts/format.sh 3 | # Code formatting script for JWT Robot Framework Library 4 | 5 | set -e 6 | 7 | # Colors for output 8 | GREEN='\033[0;32m' 9 | BLUE='\033[0;34m' 10 | NC='\033[0m' 11 | 12 | print_status() { 13 | echo -e "${BLUE}[INFO]${NC} $1" 14 | } 15 | 16 | print_success() { 17 | echo -e "${GREEN}[SUCCESS]${NC} $1" 18 | } 19 | 20 | print_status "Formatting code..." 21 | 22 | # Format code with Black 23 | print_status "Running Black formatter..." 24 | black src tests 25 | print_success "Black formatting completed" 26 | 27 | # Sort imports with isort 28 | print_status "Sorting imports with isort..." 29 | isort src tests 30 | print_success "Import sorting completed" 31 | 32 | print_success "Code formatting completed! ✨" 33 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml : -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-added-large-files 9 | - id: check-merge-conflict 10 | 11 | - repo: https://github.com/psf/black 12 | rev: 22.10.0 13 | hooks: 14 | - id: black 15 | language_version: python3 16 | 17 | - repo: https://github.com/pycqa/isort 18 | rev: 5.11.4 19 | hooks: 20 | - id: isort 21 | 22 | - repo: https://github.com/pycqa/flake8 23 | rev: 6.0.0 24 | hooks: 25 | - id: flake8 26 | 27 | - repo: https://github.com/pre-commit/mirrors-mypy 28 | rev: v0.991 29 | hooks: 30 | - id: mypy 31 | additional_dependencies: [types-all] 32 | files: ^src/ 33 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py311,flake8,mypy,robot 3 | isolated_build = true 4 | 5 | [testenv] 6 | deps = -r{toxinidir}/requirements-dev.txt 7 | commands = pytest tests/unit {posargs} 8 | 9 | [testenv:flake8] 10 | deps = flake8 11 | commands = flake8 src tests 12 | 13 | [testenv:mypy] 14 | deps = 15 | mypy 16 | -r{toxinidir}/requirements.txt 17 | commands = mypy src/JWTLibrary 18 | 19 | [testenv:black] 20 | deps = black 21 | commands = black --check src tests 22 | 23 | [testenv:robot] 24 | deps = -r{toxinidir}/requirements-dev.txt 25 | commands = robot --outputdir {envtmpdir}/robot-results tests/robot/acceptance/ 26 | 27 | [testenv:docs] 28 | deps = 29 | sphinx 30 | sphinx-rtd-theme 31 | myst-parser 32 | commands = sphinx-build -b html docs docs/_build/html 33 | 34 | [flake8] 35 | max-line-length = 90 36 | extend-ignore = E203, W503,C901,E501,E722,F541 37 | 38 | 39 | exclude = 40 | .git, 41 | __pycache__, 42 | build, 43 | dist, 44 | .tox, 45 | .eggs 46 | -------------------------------------------------------------------------------- /.github/workflows/publish-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | environment: 11 | name: pypi 12 | url: https://pypi.org/p/robotframework-jwtlibrary 13 | permissions: 14 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.11" 23 | 24 | - name: Install dependencies 25 | run: | 26 | python3 -m pip install --upgrade pip 27 | pip install build 28 | 29 | - name: Build package 30 | run: | 31 | python3 -m pip install --upgrade build 32 | python3 -m build 33 | 34 | - name: Publish package to PyPI 35 | uses: pypa/gh-action-pypi-publish@release/v1 36 | with: 37 | packages-dir: ./dist/ -------------------------------------------------------------------------------- /src/JWTLibrary/exceptions.py: -------------------------------------------------------------------------------- 1 | """Custom exceptions for JWT Library.""" 2 | 3 | 4 | class JWTLibraryError(Exception): 5 | """Base exception for JWT Library errors.""" 6 | 7 | pass 8 | 9 | 10 | class JWTTokenGenerationError(JWTLibraryError): 11 | """Raised when JWT token generation fails.""" 12 | 13 | pass 14 | 15 | 16 | class JWTTokenDecodingError(JWTLibraryError): 17 | """Raised when JWT token decoding fails.""" 18 | 19 | pass 20 | 21 | 22 | class JWTTokenValidationError(JWTLibraryError): 23 | """Raised when JWT token validation fails.""" 24 | 25 | pass 26 | 27 | 28 | class JWTExpiredTokenError(JWTLibraryError): 29 | """Raised when JWT token has expired.""" 30 | 31 | pass 32 | 33 | 34 | class JWTInvalidSignatureError(JWTLibraryError): 35 | """Raised when JWT token signature is invalid.""" 36 | 37 | pass 38 | 39 | 40 | class JWTInvalidTokenError(JWTLibraryError): 41 | """Raised when JWT token format is invalid.""" 42 | 43 | pass 44 | 45 | 46 | class JWTClaimNotFoundError(JWTLibraryError): 47 | """Raised when a requested claim is not found in token.""" 48 | 49 | pass 50 | 51 | 52 | class JWTInvalidAlgorithmError(JWTLibraryError): 53 | """Raised when an unsupported algorithm is specified.""" 54 | 55 | pass 56 | -------------------------------------------------------------------------------- /src/JWTLibrary/constants.py: -------------------------------------------------------------------------------- 1 | """Constants for JWT Library.""" 2 | 3 | # Default JWT algorithms 4 | DEFAULT_ALGORITHM = "HS256" 5 | SUPPORTED_ALGORITHMS = [ 6 | "HS256", 7 | "HS384", 8 | "HS512", 9 | "RS256", 10 | "RS384", 11 | "RS512", 12 | "ES256", 13 | "ES384", 14 | "ES512", 15 | "PS256", 16 | "PS384", 17 | "PS512", 18 | ] 19 | 20 | # Default settings 21 | DEFAULT_EXPIRATION_HOURS = 24 22 | DEFAULT_LEEWAY_SECONDS = 0 23 | 24 | # Standard JWT claims 25 | STANDARD_CLAIMS = { 26 | "iss": "issuer", 27 | "sub": "subject", 28 | "aud": "audience", 29 | "exp": "expiration_time", 30 | "nbf": "not_before", 31 | "iat": "issued_at", 32 | "jti": "jwt_id", 33 | } 34 | 35 | # Error messages 36 | ERROR_MESSAGES = { 37 | "INVALID_TOKEN": "Invalid JWT token format", 38 | "EXPIRED_TOKEN": "JWT token has expired", 39 | "INVALID_SIGNATURE": "JWT token signature verification failed", 40 | "MISSING_SECRET_KEY": "Secret key is required for verification", 41 | "UNSUPPORTED_ALGORITHM": "Unsupported JWT algorithm", 42 | "CLAIM_NOT_FOUND": "Claim not found in token payload", 43 | "INVALID_PAYLOAD": "Invalid payload format", 44 | "GENERATION_FAILED": "JWT token generation failed", 45 | "DECODING_FAILED": "JWT token decoding failed", 46 | } 47 | 48 | # Logging levels 49 | LOG_LEVELS = {"DEBUG": "DEBUG", "INFO": "INFO", "WARN": "WARN", "ERROR": "ERROR"} 50 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.11"] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v3 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements-dev.txt 28 | pip install -e . 29 | 30 | - name: Lint with flake8 31 | run: | 32 | flake8 src tests --count --select=E9,F63,F7,F82 --show-source --statistics 33 | flake8 src tests --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics 34 | 35 | - name: Test 36 | run: | 37 | make test 38 | 39 | - name: Upload coverage to Codecov 40 | uses: codecov/codecov-action@v3 41 | with: 42 | file: ./coverage.xml 43 | 44 | lint: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v3 48 | 49 | - name: Set up Python 50 | uses: actions/setup-python@v3 51 | with: 52 | python-version: 3.11 53 | 54 | - name: Install dependencies 55 | run: | 56 | python -m pip install --upgrade pip 57 | pip install black isort flake8 58 | pip install -r requirements.txt 59 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help install install-dev test test-unit test-robot lint format clean build docs 2 | 3 | help: ## Show this help 4 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' 5 | 6 | install: ## Install production dependencies 7 | pip install -r requirements.txt 8 | 9 | install-dev: ## Install development dependencies 10 | pip install -r requirements-dev.txt 11 | pip install -e . 12 | 13 | test: ## Run all tests 14 | pytest tests/unit 15 | robot --outputdir results tests/robot/acceptance/ 16 | 17 | test-unit: ## Run unit tests 18 | pytest tests/unit --cov=src/JWTLibrary --cov-report=term-missing 19 | 20 | test-robot: ## Run Robot Framework tests 21 | robot --outputdir results tests/robot/acceptance/ 22 | 23 | lint: ## Run linting 24 | flake8 src tests 25 | mypy src/JWTLibrary 26 | black --check src tests 27 | isort --check-only src tests 28 | 29 | format: ## Format code 30 | black src tests 31 | isort src tests 32 | 33 | clean: ## Clean build artifacts 34 | rm -rf build/ 35 | rm -rf dist/ 36 | rm -rf *.egg-info/ 37 | rm -rf .pytest_cache/ 38 | rm -rf .mypy_cache/ 39 | rm -rf .tox/ 40 | find . -type d -name __pycache__ -delete 41 | find . -type f -name "*.pyc" -delete 42 | 43 | build: ## Build package 44 | python -m build 45 | 46 | docs: ## Build documentation 47 | rm -rf docs/ 48 | python3 generate.py 49 | 50 | dev-setup: ## Setup development environment 51 | python -m venv venv 52 | ./venv/bin/pip install --upgrade pip 53 | ./venv/bin/pip install -r requirements-dev.txt 54 | ./venv/bin/pip install -e . 55 | 56 | release: clean ## package and upload a release 57 | python setup.py sdist upload 58 | python setup.py bdist_wheel upload 59 | 60 | dist: clean ## builds source and wheel package 61 | python setup.py sdist 62 | python setup.py bdist_wheel 63 | ls -l dist 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | *.py,cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Robot Framework 53 | log.html 54 | output.xml 55 | report.html 56 | selenium-screenshot-*.png 57 | 58 | # Jupyter Notebook 59 | .ipynb_checkpoints 60 | 61 | # IPython 62 | profile_default/ 63 | ipython_config.py 64 | 65 | # pyenv 66 | .python-version 67 | 68 | # pipenv 69 | Pipfile.lock 70 | 71 | # PEP 582 72 | __pypackages__/ 73 | 74 | # Celery stuff 75 | celerybeat-schedule 76 | celerybeat.pid 77 | 78 | # SageMath parsed files 79 | *.sage.py 80 | 81 | # Environments 82 | .env 83 | .venv 84 | env/ 85 | venv/ 86 | ENV/ 87 | env.bak/ 88 | venv.bak/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | .dmypy.json 103 | dmypy.json 104 | 105 | # Pyre type checker 106 | .pyre/ 107 | 108 | # IDEs 109 | .vscode/ 110 | .idea/ 111 | *.swp 112 | *.swo 113 | 114 | # OS 115 | .DS_Store 116 | .DS_Store? 117 | ._* 118 | .Spotlight-V100 119 | .Trashes 120 | ehthumbs.db 121 | Thumbs.db 122 | .robotcode_cache 123 | .venv -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | testpaths = tests 3 | python_files = test_*.py 4 | python_classes = Test* 5 | python_functions = test_* 6 | addopts = 7 | -v 8 | --tb=short 9 | --strict-markers 10 | --disable-warnings 11 | --cov=src/JWTLibrary 12 | --cov-report=term-missing 13 | --cov-report=html:htmlcov 14 | --cov-report=xml 15 | markers = 16 | unit: marks tests as unit tests 17 | integration: marks tests as integration tests 18 | performance: marks tests as performance tests 19 | security: marks tests as security tests 20 | slow: marks tests as slow running 21 | filterwarnings = 22 | error 23 | ignore::UserWarning 24 | ignore::DeprecationWarning 25 | 26 | # .coveragerc 27 | [run] 28 | source = src 29 | omit = 30 | */tests/* 31 | */test_* 32 | setup.py 33 | */__init__.py 34 | 35 | [report] 36 | exclude_lines = 37 | pragma: no cover 38 | def __repr__ 39 | if self.debug: 40 | if settings.DEBUG 41 | raise AssertionError 42 | raise NotImplementedError 43 | if 0: 44 | if __name__ == .__main__.: 45 | class .*\bProtocol\): 46 | @(abc\.)?abstractmethod 47 | 48 | [html] 49 | directory = htmlcov 50 | 51 | # .bandit 52 | [bandit] 53 | exclude_dirs = tests,build,dist 54 | skips = B101,B601 55 | 56 | # .pylintrc (excerpt - key sections) 57 | [MASTER] 58 | load-plugins = pylint.extensions.docparams 59 | 60 | [MESSAGES CONTROL] 61 | disable = 62 | missing-docstring, 63 | too-few-public-methods, 64 | too-many-arguments, 65 | too-many-locals, 66 | too-many-branches, 67 | too-many-statements, 68 | duplicate-code 69 | 70 | [FORMAT] 71 | max-line-length = 88 72 | 73 | [DESIGN] 74 | max-args = 7 75 | max-locals = 15 76 | max-returns = 6 77 | max-branches = 12 78 | max-statements = 50 79 | 80 | # robot.yaml (Robot Framework configuration) 81 | suite_setup: 82 | - Set Global Variable ${GLOBAL_SECRET} test_global_secret_key 83 | 84 | suite_teardown: 85 | - Log Test suite completed 86 | 87 | test_setup: 88 | - Set Test Variable ${TEST_START_TIME} ${EMPTY} 89 | - ${current_time}= Get Current Date result_format=epoch 90 | - Set Test Variable ${TEST_START_TIME} ${current_time} 91 | 92 | test_teardown: 93 | - ${end_time}= Get Current Date result_format=epoch 94 | - ${duration}= Evaluate ${end_time} - ${TEST_START_TIME} 95 | - Log Test duration: ${duration} seconds 96 | 97 | output_dir: results 98 | log_level: INFO -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os 3 | 4 | # Read the README file 5 | with open("README.md", "r", encoding="utf-8") as fh: 6 | long_description = fh.read() 7 | 8 | # Read version from version file 9 | def get_version(): 10 | version_file = os.path.join("src", "JWTLibrary", "version.py") 11 | with open(version_file, "r", encoding="utf-8") as f: 12 | exec(f.read()) 13 | return locals()["__version__"] 14 | 15 | setup( 16 | name="robotframework-jwtlibrary", 17 | version=get_version(), 18 | author="Your Name", 19 | author_email="your.email@example.com", 20 | description="JWT Library for Robot Framework", 21 | long_description=long_description, 22 | long_description_content_type="text/markdown", 23 | url="https://github.com/ohmrefresh/robotframework-jwtlibrary", 24 | package_dir={"": "src"}, 25 | packages=find_packages(where="src"), 26 | classifiers=[ 27 | "Development Status :: 4 - Beta", 28 | "Intended Audience :: Developers", 29 | "Topic :: Software Development :: Testing", 30 | "License :: OSI Approved :: Apache Software License", 31 | "Programming Language :: Python :: 3", 32 | "Programming Language :: Python :: 3.7", 33 | "Programming Language :: Python :: 3.8", 34 | "Programming Language :: Python :: 3.9", 35 | "Programming Language :: Python :: 3.10", 36 | "Programming Language :: Python :: 3.11", 37 | "Framework :: Robot Framework :: Library", 38 | ], 39 | python_requires=">=3.7", 40 | install_requires=[ 41 | "PyJWT>=2.0.0", 42 | "robotframework>=4.0.0", 43 | ], 44 | extras_require={ 45 | "dev": [ 46 | "pytest>=6.0.0", 47 | "pytest-cov>=2.10.0", 48 | "black>=21.0.0", 49 | "flake8>=3.8.0", 50 | "mypy>=0.800", 51 | "robotframework-lint>=1.1", 52 | "sphinx>=4.0.0", 53 | "sphinx-rtd-theme>=0.5.0", 54 | ], 55 | "crypto": [ 56 | "cryptography>=3.0.0", 57 | ] 58 | }, 59 | entry_points={ 60 | "console_scripts": [ 61 | "jwt-robot-tool=JWTLibrary.cli:main", 62 | ], 63 | }, 64 | keywords="robotframework testing jwt authentication token", 65 | project_urls={ 66 | "Bug Reports": "https://github.com/ohmrefresh/robotframework-jwtlibrary/issues", 67 | "Source": "https://github.com/ohmrefresh/robotframework-jwtlibrary", 68 | "Documentation": "https://jwt-robotframework-library.readthedocs.io/", 69 | }, 70 | ) 71 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "robotframework-jwtlibrary" 7 | dynamic = ["version"] 8 | description = "JWT Library for Robot Framework" 9 | readme = "README.md" 10 | requires-python = ">=3.7" 11 | license = {text = "Apache-2.0"} 12 | authors = [ 13 | {name = "Your Name", email = "your.email@example.com"}, 14 | ] 15 | classifiers = [ 16 | "Development Status :: 4 - Beta", 17 | "Intended Audience :: Developers", 18 | "Topic :: Software Development :: Testing", 19 | "License :: OSI Approved :: Apache Software License", 20 | "Programming Language :: Python :: 3", 21 | "Framework :: Robot Framework :: Library", 22 | ] 23 | dependencies = [ 24 | "PyJWT>=2.0.0", 25 | "robotframework>=4.0.0", 26 | ] 27 | 28 | [project.optional-dependencies] 29 | dev = [ 30 | "pytest>=6.0.0", 31 | "pytest-cov>=2.10.0", 32 | "black>=21.0.0", 33 | "flake8>=3.8.0", 34 | "mypy>=0.800", 35 | "robotframework-lint>=1.1", 36 | ] 37 | docs = [ 38 | "sphinx>=4.0.0", 39 | "sphinx-rtd-theme>=0.5.0", 40 | "myst-parser>=0.15.0", 41 | ] 42 | crypto = [ 43 | "cryptography>=3.0.0", 44 | ] 45 | 46 | [project.urls] 47 | Homepage = "https://github.com/ohmrefresh/robotframework-jwtlibrary" 48 | Documentation = "https://jwt-robotframework-library.readthedocs.io/" 49 | Repository = "https://github.com/ohmrefresh/robotframework-jwtlibrary.git" 50 | "Bug Tracker" = "https://github.com/ohmrefresh/robotframework-jwtlibrary/issues" 51 | 52 | [tool.setuptools.packages.find] 53 | where = ["src"] 54 | 55 | [tool.setuptools.dynamic] 56 | version = {attr = "JWTLibrary.version.__version__"} 57 | 58 | [tool.black] 59 | line-length = 88 60 | target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] 61 | include = '\.pyi?$' 62 | extend-exclude = ''' 63 | /( 64 | \.eggs 65 | | \.git 66 | | \.hg 67 | | \.mypy_cache 68 | | \.tox 69 | | \.venv 70 | | _build 71 | | buck-out 72 | | build 73 | | dist 74 | )/ 75 | ''' 76 | 77 | [tool.isort] 78 | profile = "black" 79 | multi_line_output = 3 80 | line_length = 88 81 | 82 | [tool.mypy] 83 | python_version = "3.7" 84 | warn_return_any = true 85 | warn_unused_configs = true 86 | disallow_untyped_defs = true 87 | ignore_missing_imports = true 88 | 89 | [tool.pytest.ini_options] 90 | testpaths = ["tests"] 91 | python_files = ["test_*.py"] 92 | python_classes = ["Test*"] 93 | python_functions = ["test_*"] 94 | addopts = "-v --tb=short --strict-markers" 95 | markers = [ 96 | "unit: marks tests as unit tests", 97 | "integration: marks tests as integration tests", 98 | "slow: marks tests as slow", 99 | ] 100 | 101 | [tool.coverage.run] 102 | source = ["src"] 103 | omit = [ 104 | "*/tests/*", 105 | "*/test_*", 106 | "setup.py", 107 | ] 108 | 109 | [tool.coverage.report] 110 | exclude_lines = [ 111 | "pragma: no cover", 112 | "def __repr__", 113 | "if self.debug:", 114 | "if settings.DEBUG", 115 | "raise AssertionError", 116 | "raise NotImplementedError", 117 | "if 0:", 118 | "if __name__ == .__main__.:", 119 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JWT Robot Framework Library 2 | 3 | 4 | A comprehensive Robot Framework library for JSON Web Token (JWT) operations, enabling robust testing of JWT-based authentication and authorization in your test automation. 5 | 6 | ## 🚀 Features 7 | 8 | - **Complete JWT Lifecycle**: Generate, decode, validate, and analyze JWT tokens 9 | - **Multiple Algorithms**: Support for HMAC, RSA, ECDSA, and PSS algorithms 10 | - **Advanced Validation**: Comprehensive claim validation, expiration checking, and signature verification 11 | - **Security-First**: Built-in protection against common JWT vulnerabilities 12 | - **Easy Integration**: Simple keyword interface designed for Robot Framework 13 | - **Extensive Documentation**: Complete keyword reference with examples 14 | - **Error Handling**: Graceful error handling with detailed error messages 15 | - **Performance Optimized**: Efficient token operations for test automation 16 | 17 | ## 📦 Installation 18 | 19 | ### Using pip 20 | 21 | ```bash 22 | pip install robotframework-jwtlibrary 23 | ``` 24 | 25 | ### From source 26 | 27 | ```bash 28 | git clone https://github.com/ohmrefresh/robotframework-jwtlibrary.git 29 | cd jwt-robotframework-library 30 | pip install -e . 31 | ``` 32 | 33 | ### Dependencies 34 | 35 | - Python 3.7+ 36 | - Robot Framework 4.0+ 37 | - PyJWT 2.0+ 38 | 39 | ## 🏃 Quick Start 40 | 41 | ### Basic Usage 42 | 43 | ```robotframework 44 | *** Settings *** 45 | Library JWTLibrary 46 | 47 | *** Variables *** 48 | ${SECRET_KEY} your-secret-key-here 49 | 50 | *** Test Cases *** 51 | Basic JWT Operations 52 | # Create payload 53 | ${payload}= Create Dictionary user_id=123 role=admin 54 | 55 | # Generate token 56 | ${token}= Generate JWT Token ${payload} ${SECRET_KEY} 57 | 58 | # Decode and verify 59 | ${decoded}= Decode JWT Payload ${token} ${SECRET_KEY} 60 | Should Be Equal ${decoded['user_id']} 123 61 | 62 | # Validate token 63 | ${is_valid}= Verify JWT Token ${token} ${SECRET_KEY} 64 | Should Be True ${is_valid} 65 | ``` 66 | 67 | ### Advanced Example 68 | 69 | ```robotframework 70 | Advanced JWT Validation 71 | # Create comprehensive payload 72 | ${payload}= Create Dictionary 73 | ... iss=auth-service 74 | ... sub=user-12345 75 | ... aud=api-service 76 | ... user_id=12345 77 | ... role=admin 78 | ... permissions=["read", "write", "delete"] 79 | 80 | # Generate token with custom expiration 81 | ${token}= Generate JWT Token ${payload} ${SECRET_KEY} expiration_hours=2 82 | 83 | # Comprehensive validation 84 | ${exp_info}= Check JWT Expiration ${token} 85 | Should Be Equal ${exp_info['is_expired']} ${False} 86 | 87 | ${claims_valid}= Validate JWT Claims ${token} 88 | ... {"role": "admin", "user_id": 12345} ${SECRET_KEY} ${True} 89 | Should Be True ${claims_valid} 90 | 91 | ${aud_valid}= Validate JWT Audience ${token} api-service 92 | Should Be True ${aud_valid} 93 | ``` 94 | 95 | ## 📚 Available Keywords 96 | 97 | ### Token Generation 98 | - `Generate JWT Token` - Creates JWT tokens with custom payloads 99 | - `Generate JWT Token With Claims` - Creates tokens using keyword arguments 100 | - `Generate JWT Token Without Expiration` - Creates non-expiring tokens 101 | - `Generate JWT Token With Custom Expiration` - Creates tokens with specific expiration 102 | 103 | ### Token Decoding 104 | - `Decode JWT Payload` - Decodes token payloads with optional verification 105 | - `Decode JWT Header` - Decodes token headers 106 | - `Get JWT Claim` - Extracts specific claims from tokens 107 | - `Get Multiple JWT Claims` - Extracts multiple claims 108 | - `Extract All JWT Claims` - Gets all claims with metadata 109 | 110 | ### Token Validation 111 | - `Verify JWT Token` - Validates token signatures and expiration 112 | - `Check JWT Expiration` - Checks token expiration status 113 | - `Validate JWT Claims` - Validates expected claim values 114 | - `Check JWT Algorithm` - Validates token algorithm 115 | - `Validate JWT Structure` - Validates token format 116 | - `Check JWT Not Before` - Validates nbf claim 117 | - `Validate JWT Audience` - Validates audience claim 118 | 119 | ### Utilities 120 | - `Create JWT Payload` - Helper to create payload dictionaries 121 | - `Get JWT Token Info` - Gets comprehensive token information 122 | - `Compare JWT Tokens` - Compares two tokens 123 | - `Extract JWT Timestamps` - Extracts timestamp claims 124 | - `Generate Current Timestamp` - Creates current timestamp 125 | - `Generate Future Timestamp` - Creates future timestamp 126 | - `Convert Timestamp To Datetime` - Converts timestamps to datetime 127 | 128 | ## 🔧 Supported Algorithms 129 | 130 | | Family | Algorithms | Description | 131 | |--------|------------|-------------| 132 | | HMAC | HS256, HS384, HS512 | Symmetric signing | 133 | | RSA | RS256, RS384, RS512 | Asymmetric signing | 134 | | ECDSA | ES256, ES384, ES512 | Elliptic curve signing | 135 | | PSS | PS256, PS384, PS512 | RSA-PSS signing | 136 | 137 | ## 🎯 Use Cases 138 | 139 | ### API Testing 140 | ```robotframework 141 | Test API Authentication 142 | ${token}= Generate JWT Token {"user_id": 123} ${API_SECRET} 143 | 144 | # Use token in API requests 145 | ${headers}= Create Dictionary Authorization=Bearer ${token} 146 | ${response}= GET ${API_URL}/protected headers=${headers} 147 | Should Be Equal As Integers ${response.status_code} 200 148 | ``` 149 | 150 | ### Microservices Testing 151 | ```robotframework 152 | Test Service-to-Service Communication 153 | ${service_payload}= Create Dictionary 154 | ... iss=service-a 155 | ... aud=service-b 156 | ... scope=read:data 157 | 158 | ${service_token}= Generate JWT Token ${service_payload} ${SERVICE_SECRET} 159 | 160 | # Validate token at receiving service 161 | ${claims_valid}= Validate JWT Claims ${service_token} 162 | ... {"iss": "service-a", "aud": "service-b"} 163 | Should Be True ${claims_valid} 164 | ``` 165 | 166 | ### Security Testing 167 | ```robotframework 168 | Test Token Security 169 | ${token}= Generate JWT Token {"user_id": 123} ${SECRET_KEY} 170 | 171 | # Test with tampered token 172 | ${tampered_token}= Replace String ${token} . X count=1 173 | ${is_valid}= Verify JWT Token ${tampered_token} ${SECRET_KEY} 174 | Should Be Equal ${is_valid} ${False} 175 | 176 | # Test token expiration 177 | ${expired_token}= Generate JWT Token {"user_id": 123} ${SECRET_KEY} 178 | ... expiration_hours=0.001 179 | Sleep 1s 180 | ${exp_info}= Check JWT Expiration ${expired_token} 181 | Should Be True ${exp_info['is_expired']} 182 | ``` 183 | 184 | ## 🛡️ Security Considerations 185 | 186 | - **Secret Management**: Never hardcode secrets in test files 187 | - **Algorithm Validation**: Always verify the algorithm matches expectations 188 | - **Expiration Checking**: Validate token expiration in security tests 189 | - **Claim Validation**: Verify all security-relevant claims 190 | - **Signature Verification**: Always verify signatures in production scenarios 191 | 192 | ## 📖 Documentation 193 | 194 | - [Installation Guide](docs/installation.md) 195 | - [User Guide](docs/user_guide/basic_usage.md) 196 | - [API Reference](docs/api_reference/keywords.md) 197 | - [Examples](examples/) 198 | - [Contributing Guidelines](docs/contributing.md) 199 | 200 | ## 🔍 Examples 201 | 202 | Check out the [examples directory](examples/) for comprehensive usage examples: 203 | 204 | - [Basic Usage](examples/basic_usage/) 205 | - [Advanced Features](examples/advanced_usage/) 206 | - [Real-world Scenarios](examples/real_world_scenarios/) 207 | 208 | ## 🧪 Testing 209 | 210 | Run the test suite: 211 | 212 | ```bash 213 | # Install development dependencies 214 | pip install -r requirements-dev.txt 215 | 216 | # Run unit tests 217 | pytest tests/unit/ 218 | 219 | # Run Robot Framework tests 220 | robot tests/robot/acceptance/ 221 | 222 | # Run all tests 223 | make test 224 | ``` 225 | -------------------------------------------------------------------------------- /src/JWTLibrary/keywords/token_generation.py: -------------------------------------------------------------------------------- 1 | """Token generation keywords for JWT Library.""" 2 | 3 | from datetime import datetime, timezone 4 | from typing import Any, Dict 5 | 6 | import jwt 7 | from robot.api import logger 8 | from robot.api.deco import keyword 9 | 10 | from ..constants import DEFAULT_ALGORITHM, DEFAULT_EXPIRATION_HOURS 11 | from ..exceptions import JWTTokenGenerationError 12 | from ..utils import ( 13 | calculate_expiration, 14 | safe_json_dumps, 15 | validate_algorithm, 16 | validate_payload, 17 | ) 18 | 19 | 20 | class TokenGenerationKeywords: 21 | """Keywords for JWT token generation.""" 22 | 23 | @keyword("Generate JWT Token") 24 | def generate_jwt_token( 25 | self, 26 | payload: Dict[str, Any], 27 | secret_key: str, 28 | algorithm: str = None, 29 | expiration_hours: int = None, 30 | ) -> str: 31 | """ 32 | Generates a JWT token with the given payload and secret key. 33 | 34 | Arguments: 35 | - ``payload``: Dictionary containing the token payload data 36 | - ``secret_key``: Secret key used for signing the token 37 | - ``algorithm``: JWT algorithm (default: HS256) 38 | - ``expiration_hours``: Token expiration time in hours (default: 24) 39 | 40 | Returns: 41 | - JWT token as string 42 | 43 | Examples: 44 | | ${payload}= Create Dictionary user_id=123 role=admin 45 | | ${token}= Generate JWT Token ${payload} my_secret_key 46 | | ${token}= Generate JWT Token ${payload} my_secret_key algorithm=HS512 47 | | ${token}= Generate JWT Token ${payload} my_secret_key expiration_hours=1 48 | """ 49 | try: 50 | # Validate inputs 51 | algorithm = algorithm or DEFAULT_ALGORITHM 52 | validate_algorithm(algorithm) 53 | 54 | expiration_hours = expiration_hours or DEFAULT_EXPIRATION_HOURS 55 | 56 | # Create a copy of payload to avoid modifying the original 57 | token_payload = payload.copy() 58 | 59 | # Validate and sanitize payload 60 | token_payload = validate_payload(token_payload) 61 | 62 | # Add standard JWT claims 63 | now = datetime.now(tz=timezone.utc) 64 | 65 | # Only add standard claims if they don't already exist 66 | if "iat" not in token_payload: 67 | token_payload["iat"] = now 68 | if "exp" not in token_payload: 69 | token_payload["exp"] = calculate_expiration(expiration_hours, now) 70 | if "nbf" not in token_payload: 71 | token_payload["nbf"] = now 72 | 73 | # Generate the token 74 | token = jwt.encode(token_payload, secret_key, algorithm=algorithm) 75 | 76 | logger.info(f"JWT token generated successfully with algorithm: {algorithm}") 77 | logger.debug(f"Token payload: {safe_json_dumps(payload, indent=2)}") 78 | 79 | return token 80 | 81 | except Exception as e: 82 | error_msg = f"JWT token generation failed: {str(e)}" 83 | logger.error(error_msg) 84 | raise JWTTokenGenerationError(error_msg) 85 | 86 | @keyword("Generate JWT Token With Claims") 87 | def generate_jwt_token_with_claims( 88 | self, 89 | secret_key: str, 90 | algorithm: str = None, 91 | expiration_hours: int = None, 92 | **claims, 93 | ) -> str: 94 | """ 95 | Generates a JWT token with individual claims as keyword arguments. 96 | 97 | Arguments: 98 | - ``secret_key``: Secret key used for signing the token 99 | - ``algorithm``: JWT algorithm (default: HS256) 100 | - ``expiration_hours``: Token expiration time in hours (default: 24) 101 | - ``**claims``: Individual claims as keyword arguments 102 | 103 | Returns: 104 | - JWT token as string 105 | 106 | Examples: 107 | | ${token}= Generate JWT Token With Claims my_secret_key user_id=123 role=admin 108 | | ${token}= Generate JWT Token With Claims my_secret_key algorithm=HS512 user_id=456 109 | | ${token}= Generate JWT Token With Claims my_secret_key expiration_hours=1 email=test@example.com 110 | """ 111 | payload = dict(claims) 112 | return self.generate_jwt_token(payload, secret_key, algorithm, expiration_hours) 113 | 114 | @keyword("Generate JWT Token Without Expiration") 115 | def generate_jwt_token_without_expiration( 116 | self, payload: Dict[str, Any], secret_key: str, algorithm: str = None 117 | ) -> str: 118 | """ 119 | Generates a JWT token without expiration claim. 120 | 121 | Arguments: 122 | - ``payload``: Dictionary containing the token payload data 123 | - ``secret_key``: Secret key used for signing the token 124 | - ``algorithm``: JWT algorithm (default: HS256) 125 | 126 | Returns: 127 | - JWT token as string 128 | 129 | Examples: 130 | | ${payload}= Create Dictionary user_id=123 role=admin 131 | | ${token}= Generate JWT Token Without Expiration ${payload} my_secret_key 132 | """ 133 | try: 134 | algorithm = algorithm or DEFAULT_ALGORITHM 135 | validate_algorithm(algorithm) 136 | 137 | # Create a copy and validate payload 138 | token_payload = validate_payload(payload.copy()) 139 | 140 | # Add only iat and nbf, no exp 141 | now = datetime.utcnow() 142 | if "iat" not in token_payload: 143 | token_payload["iat"] = now 144 | if "nbf" not in token_payload: 145 | token_payload["nbf"] = now 146 | 147 | # Ensure no exp claim 148 | token_payload.pop("exp", None) 149 | 150 | # Generate the token 151 | token = jwt.encode(token_payload, secret_key, algorithm=algorithm) 152 | 153 | logger.info(f"JWT token without expiration generated successfully") 154 | logger.debug(f"Token payload: {safe_json_dumps(payload, indent=2)}") 155 | 156 | return token 157 | 158 | except Exception as e: 159 | error_msg = f"JWT token generation without expiration failed: {str(e)}" 160 | logger.error(error_msg) 161 | raise JWTTokenGenerationError(error_msg) 162 | 163 | @keyword("Generate JWT Token With Custom Expiration") 164 | def generate_jwt_token_with_custom_expiration( 165 | self, 166 | payload: Dict[str, Any], 167 | secret_key: str, 168 | expiration_datetime: datetime, 169 | algorithm: str = None, 170 | ) -> str: 171 | """ 172 | Generates a JWT token with a specific expiration datetime. 173 | 174 | Arguments: 175 | - ``payload``: Dictionary containing the token payload data 176 | - ``secret_key``: Secret key used for signing the token 177 | - ``expiration_datetime``: Specific expiration datetime 178 | - ``algorithm``: JWT algorithm (default: HS256) 179 | 180 | Returns: 181 | - JWT token as string 182 | 183 | Examples: 184 | | ${exp_time}= Add Time To Date ${current_date} 2 hours 185 | | ${payload}= Create Dictionary user_id=123 186 | | ${token}= Generate JWT Token With Custom Expiration ${payload} my_secret_key ${exp_time} 187 | """ 188 | try: 189 | algorithm = algorithm or DEFAULT_ALGORITHM 190 | validate_algorithm(algorithm) 191 | 192 | # Create a copy and validate payload 193 | token_payload = validate_payload(payload.copy()) 194 | 195 | # Add standard claims with custom expiration 196 | now = datetime.now(tz=timezone.utc) 197 | token_payload["iat"] = now 198 | token_payload["exp"] = expiration_datetime 199 | token_payload["nbf"] = now 200 | 201 | # Generate the token 202 | token = jwt.encode(token_payload, secret_key, algorithm=algorithm) 203 | 204 | logger.info(f"JWT token with custom expiration generated successfully") 205 | logger.debug(f"Expiration time: {expiration_datetime.isoformat()}") 206 | 207 | return token 208 | 209 | except Exception as e: 210 | error_msg = f"JWT token generation with custom expiration failed: {str(e)}" 211 | logger.error(error_msg) 212 | raise JWTTokenGenerationError(error_msg) 213 | -------------------------------------------------------------------------------- /src/JWTLibrary/utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions for JWT Library.""" 2 | 3 | import base64 4 | import json 5 | from datetime import datetime, timedelta, timezone 6 | from typing import Any, Dict, Optional, Union 7 | 8 | from .constants import STANDARD_CLAIMS, SUPPORTED_ALGORITHMS 9 | from .exceptions import JWTInvalidAlgorithmError, JWTInvalidTokenError 10 | 11 | 12 | def validate_algorithm(algorithm: str) -> str: 13 | """ 14 | Validates if the algorithm is supported. 15 | Args: 16 | algorithm: JWT algorithm to validate 17 | Returns: 18 | Validated algorithm string 19 | Raises: 20 | JWTInvalidAlgorithmError: If algorithm is not supported 21 | """ 22 | if algorithm not in SUPPORTED_ALGORITHMS: 23 | raise JWTInvalidAlgorithmError( 24 | f"Algorithm '{algorithm}' is not supported. " 25 | f"Supported algorithms: {', '.join(SUPPORTED_ALGORITHMS)}" 26 | ) 27 | return algorithm 28 | 29 | 30 | def format_datetime_for_jwt(dt: datetime) -> int: 31 | """ 32 | Converts datetime to JWT timestamp format. 33 | Args: 34 | dt: Datetime object 35 | Returns: 36 | Unix timestamp as integer 37 | """ 38 | return int(dt.timestamp()) 39 | 40 | 41 | def parse_jwt_timestamp(timestamp: Union[int, float]) -> datetime: 42 | """ 43 | Converts JWT timestamp to datetime object. 44 | Args: 45 | timestamp: Unix timestamp 46 | Returns: 47 | Datetime object 48 | """ 49 | return datetime.fromtimestamp(timestamp) 50 | 51 | 52 | def safe_json_dumps(obj: Any, **kwargs) -> str: 53 | """ 54 | Safely converts object to JSON string with datetime handling. 55 | Args: 56 | obj: Object to convert 57 | **kwargs: Additional arguments for json.dumps 58 | Returns: 59 | JSON string 60 | """ 61 | 62 | def default_serializer(o): 63 | if isinstance(o, datetime): 64 | return o.isoformat() 65 | raise TypeError(f"Object of type {type(o)} is not JSON serializable") 66 | 67 | return json.dumps(obj, default=default_serializer, **kwargs) 68 | 69 | 70 | def mask_sensitive_data( 71 | data: Dict[str, Any], sensitive_keys: list = None 72 | ) -> Dict[str, Any]: 73 | """ 74 | Masks sensitive data in dictionary for logging. 75 | Args: 76 | data: Dictionary containing data 77 | sensitive_keys: List of keys to mask (default: common sensitive keys) 78 | Returns: 79 | Dictionary with masked sensitive values 80 | """ 81 | if sensitive_keys is None: 82 | sensitive_keys = ["password", "secret", "key", "token", "auth"] 83 | masked_data = data.copy() 84 | for key, value in masked_data.items(): 85 | if any( 86 | sensitive_key.lower() in key.lower() for sensitive_key in sensitive_keys 87 | ): 88 | if isinstance(value, str) and len(value) > 8: 89 | masked_data[key] = value[:4] + "*" * (len(value) - 8) + value[-4:] 90 | else: 91 | masked_data[key] = "***" 92 | return masked_data 93 | 94 | 95 | def validate_payload(payload: Dict[str, Any]) -> Dict[str, Any]: 96 | """ 97 | Validates and sanitizes JWT payload. 98 | Args: 99 | payload: JWT payload dictionary 100 | Returns: 101 | Validated payload 102 | Raises: 103 | JWTInvalidTokenError: If payload is invalid 104 | """ 105 | if not isinstance(payload, dict): 106 | raise JWTInvalidTokenError("Payload must be a dictionary") 107 | # Check for reserved claims with wrong types 108 | for claim, description in STANDARD_CLAIMS.items(): 109 | if claim in payload: 110 | value = payload[claim] 111 | # Check timestamp claims 112 | if claim in ["exp", "nbf", "iat"]: 113 | if not isinstance(value, (int, float, datetime)): 114 | raise JWTInvalidTokenError( 115 | f"Claim '{claim}' ({description}) must be a number or datetime" 116 | ) 117 | # Convert datetime to timestamp 118 | if isinstance(value, datetime): 119 | payload[claim] = format_datetime_for_jwt(value) 120 | return payload 121 | 122 | 123 | def calculate_expiration( 124 | expiration_hours: Optional[int] = None, base_time: Optional[datetime] = None 125 | ) -> datetime: 126 | """ 127 | Calculates expiration datetime. 128 | Args: 129 | expiration_hours: Hours until expiration 130 | base_time: Base time to calculate from (default: now) 131 | Returns: 132 | Expiration datetime 133 | """ 134 | if base_time is None: 135 | base_time = datetime.now(tz=timezone.utc) 136 | if expiration_hours is None: 137 | from .constants import DEFAULT_EXPIRATION_HOURS 138 | 139 | expiration_hours = DEFAULT_EXPIRATION_HOURS 140 | return base_time + timedelta(hours=expiration_hours) 141 | 142 | 143 | def decode_jwt_header_unsafe(token: str) -> Dict[str, Any]: 144 | """ 145 | Decodes JWT header without verification (unsafe). 146 | Args: 147 | token: JWT token string 148 | Returns: 149 | Header dictionary 150 | Raises: 151 | JWTInvalidTokenError: If token format is invalid 152 | """ 153 | try: 154 | # Split token into parts 155 | parts = token.split(".") 156 | if len(parts) != 3: 157 | raise JWTInvalidTokenError("Invalid JWT token format") 158 | # Decode header (first part) 159 | header_b64 = parts[0] 160 | # Add padding if needed 161 | padding = len(header_b64) % 4 162 | if padding: 163 | header_b64 += "=" * (4 - padding) 164 | header_bytes = base64.urlsafe_b64decode(header_b64) 165 | header = json.loads(header_bytes.decode("utf-8")) 166 | return header 167 | except (ValueError, json.JSONDecodeError, UnicodeDecodeError) as e: 168 | raise JWTInvalidTokenError(f"Failed to decode JWT header: {str(e)}") 169 | 170 | 171 | def decode_jwt_payload_unsafe(token: str) -> Dict[str, Any]: 172 | """ 173 | Decodes JWT payload without verification (unsafe). 174 | Args: 175 | token: JWT token string 176 | Returns: 177 | Payload dictionary 178 | Raises: 179 | JWTInvalidTokenError: If token format is invalid 180 | """ 181 | try: 182 | # Split token into parts 183 | parts = token.split(".") 184 | if len(parts) != 3: 185 | raise JWTInvalidTokenError("Invalid JWT token format") 186 | # Decode payload (second part) 187 | payload_b64 = parts[1] 188 | # Add padding if needed 189 | padding = len(payload_b64) % 4 190 | if padding: 191 | payload_b64 += "=" * (4 - padding) 192 | payload_bytes = base64.urlsafe_b64decode(payload_b64) 193 | payload = json.loads(payload_bytes.decode("utf-8")) 194 | return payload 195 | except (ValueError, json.JSONDecodeError, UnicodeDecodeError) as e: 196 | raise JWTInvalidTokenError(f"Failed to decode JWT payload: {str(e)}") 197 | 198 | 199 | def get_token_info(token: str) -> Dict[str, Any]: 200 | """ 201 | Gets basic information about a JWT token without verification. 202 | Args: 203 | token: JWT token string 204 | Returns: 205 | Dictionary with token information 206 | """ 207 | try: 208 | header = decode_jwt_header_unsafe(token) 209 | payload = decode_jwt_payload_unsafe(token) 210 | # Extract basic info 211 | info = { 212 | "algorithm": header.get("alg"), 213 | "type": header.get("typ"), 214 | "issued_at": payload.get("iat"), 215 | "expires_at": payload.get("exp"), 216 | "not_before": payload.get("nbf"), 217 | "issuer": payload.get("iss"), 218 | "subject": payload.get("sub"), 219 | "audience": payload.get("aud"), 220 | "jwt_id": payload.get("jti"), 221 | "claims_count": len(payload), 222 | "header_params": list(header.keys()), 223 | "payload_claims": list(payload.keys()), 224 | } 225 | # Add expiration status if exp claim exists 226 | if info["expires_at"]: 227 | exp_dt = parse_jwt_timestamp(info["expires_at"]) 228 | now = datetime.utcnow() 229 | info["is_expired"] = now > exp_dt 230 | info["time_until_expiry"] = (exp_dt - now).total_seconds() 231 | return info 232 | except Exception as e: 233 | raise JWTInvalidTokenError(f"Failed to get token info: {str(e)}") 234 | -------------------------------------------------------------------------------- /example/basic_usage/example.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation Simple JWT usage examples 3 | Library JWTLibrary 4 | Library Collections 5 | 6 | *** Variables *** 7 | ${SECRET_KEY} my_application_secret_key 8 | 9 | *** Test Cases *** 10 | Example 1: Basic Token Generation and Validation 11 | [Documentation] Shows basic JWT token creation and validation 12 | [Tags] example basic 13 | 14 | # Step 1: Create user data 15 | ${user_data}= Create Dictionary 16 | ... user_id=12345 17 | ... username=john_doe 18 | ... email=john@example.com 19 | ... role=user 20 | 21 | # Step 2: Generate JWT token 22 | ${jwt_token}= Generate JWT Token ${user_data} ${SECRET_KEY} 23 | Log Generated JWT Token: ${jwt_token} 24 | 25 | # Step 3: Verify the token is valid 26 | ${is_valid}= Verify JWT Token ${jwt_token} ${SECRET_KEY} 27 | Should Be True ${is_valid} 28 | Log ✓ Token is valid 29 | 30 | # Step 4: Decode the token to get user data back 31 | ${decoded_data}= Decode JWT Payload ${jwt_token} ${SECRET_KEY} 32 | Should Be Equal ${decoded_data['username']} john_doe 33 | Should Be Equal ${decoded_data['role']} user 34 | Log ✓ User data successfully retrieved from token 35 | 36 | Example 2: Working with Token Expiration 37 | [Documentation] Shows how to work with token expiration 38 | [Tags] example expiration 39 | 40 | ${user_data}= Create Dictionary user_id=67890 session=temp 41 | 42 | # Create token that expires in 1 hour 43 | ${short_token}= Generate JWT Token ${user_data} ${SECRET_KEY} expiration_hours=1 44 | 45 | # Check when the token expires 46 | ${expiration_info}= Check JWT Expiration ${short_token} 47 | Log Token expires at: ${expiration_info['expires_at']} 48 | Log Time until expiration: ${expiration_info['time_until_expiry']} seconds 49 | Should Be Equal ${expiration_info['is_expired']} ${False} 50 | 51 | # Create token without expiration 52 | ${permanent_token}= Generate JWT Token Without Expiration ${user_data} ${SECRET_KEY} 53 | ${perm_exp_info}= Check JWT Expiration ${permanent_token} 54 | Should Be Equal ${perm_exp_info['has_expiration']} ${False} 55 | Log ✓ Permanent token created successfully 56 | 57 | Example 3: Extracting Specific Claims 58 | [Documentation] Shows how to extract specific information from tokens 59 | [Tags] example claims 60 | 61 | ${employee_data}= Create Dictionary 62 | ... employee_id=E001 63 | ... name=Alice Smith 64 | ... department=Engineering 65 | ... clearance_level=3 66 | ... email=alice@company.com 67 | 68 | ${token}= Generate JWT Token ${employee_data} ${SECRET_KEY} 69 | 70 | # Extract individual pieces of information 71 | ${employee_id}= Get JWT Claim ${token} employee_id 72 | ${department}= Get JWT Claim ${token} department 73 | ${clearance}= Get JWT Claim ${token} clearance_level 74 | 75 | Log Employee ID: ${employee_id} 76 | Log Department: ${department} 77 | Log Clearance Level: ${clearance} 78 | 79 | # Extract multiple claims at once 80 | ${claim_names}= Create List name email department 81 | ${employee_info}= Get Multiple JWT Claims ${token} ${claim_names} 82 | 83 | Should Be Equal ${employee_info['name']} Alice Smith 84 | Should Be Equal ${employee_info['email']} alice@company.com 85 | Log ✓ Employee information extracted successfully 86 | 87 | Example 4: Using Different JWT Algorithms 88 | [Documentation] Shows how to use different JWT signing algorithms 89 | [Tags] example algorithms 90 | 91 | ${api_data}= Create Dictionary api_key=abc123 service=payment 92 | 93 | # Use HS256 (default) 94 | ${token_hs256}= Generate JWT Token ${api_data} ${SECRET_KEY} 95 | ${header_hs256}= Decode JWT Header ${token_hs256} 96 | Should Be Equal ${header_hs256['alg']} HS256 97 | 98 | # Use HS512 for higher security 99 | ${token_hs512}= Generate JWT Token ${api_data} ${SECRET_KEY} algorithm=HS512 100 | ${header_hs512}= Decode JWT Header ${token_hs512} 101 | Should Be Equal ${header_hs512['alg']} HS512 102 | 103 | # Verify both tokens work with their respective algorithms 104 | ${valid_256}= Verify JWT Token ${token_hs256} ${SECRET_KEY} algorithm=HS256 105 | ${valid_512}= Verify JWT Token ${token_hs512} ${SECRET_KEY} algorithm=HS512 106 | Should Be True ${valid_256} 107 | Should Be True ${valid_512} 108 | 109 | Log ✓ Successfully used multiple JWT algorithms 110 | 111 | Example 5: Error Handling 112 | [Documentation] Shows how JWT operations handle errors gracefully 113 | [Tags] example error-handling 114 | 115 | ${valid_data}= Create Dictionary user_id=999 test=error_demo 116 | ${valid_token}= Generate JWT Token ${valid_data} ${SECRET_KEY} 117 | 118 | # Try to verify token with wrong secret - should return False, not crash 119 | ${wrong_secret_result}= Verify JWT Token ${valid_token} wrong_secret 120 | Should Be Equal ${wrong_secret_result} ${False} 121 | Log ✓ Wrong secret key handled gracefully 122 | 123 | # Try to verify completely invalid token - should return False 124 | ${invalid_token_result}= Verify JWT Token invalid.token.here ${SECRET_KEY} 125 | Should Be Equal ${invalid_token_result} ${False} 126 | Log ✓ Invalid token handled gracefully 127 | 128 | # Try to get non-existent claim - should raise clear error 129 | Run Keyword And Expect Error *Claim 'nonexistent' not found* 130 | ... Get JWT Claim ${valid_token} nonexistent 131 | Log ✓ Missing claim error handled with clear message 132 | 133 | Example 6: Creating Payloads Easily 134 | [Documentation] Shows convenient ways to create JWT payloads 135 | [Tags] example payload-creation 136 | 137 | # Method 1: Using Create JWT Payload keyword 138 | ${payload1}= Create JWT Payload 139 | ... user_id=111 140 | ... role=admin 141 | ... active=true 142 | ... login_count=5 143 | 144 | # Method 2: Using Generate JWT Token With Claims 145 | ${token1}= Generate JWT Token With Claims ${SECRET_KEY} 146 | ... user_id=222 147 | ... role=user 148 | ... department=sales 149 | 150 | # Method 3: Traditional dictionary creation 151 | ${payload2}= Create Dictionary 152 | ... user_id=333 153 | ... preferences={"theme": "dark", "notifications": true} 154 | 155 | ${token2}= Generate JWT Token ${payload2} ${SECRET_KEY} 156 | 157 | # Verify all methods work 158 | ${decoded1}= Decode JWT Payload ${token1} ${SECRET_KEY} 159 | ${decoded2}= Decode JWT Payload ${token2} ${SECRET_KEY} 160 | 161 | Should Be Equal As Integers ${decoded1['user_id']} 222 162 | Should Be Equal As Integers ${decoded2['user_id']} 333 163 | Log ✓ All payload creation methods work correctly 164 | 165 | *** Keywords *** 166 | Demo User Login Workflow 167 | [Arguments] ${username} ${user_id} ${role} 168 | [Documentation] Example workflow for user login with JWT 169 | 170 | Log Starting login workflow for user: ${username} 171 | 172 | # Step 1: Create user session data 173 | ${session_data}= Create Dictionary 174 | ... user_id=${user_id} 175 | ... username=${username} 176 | ... role=${role} 177 | ... login_time=${EMPTY} 178 | 179 | ${login_timestamp}= Generate Current Timestamp 180 | Set To Dictionary ${session_data} login_time=${login_timestamp} 181 | 182 | # Step 2: Generate session token (valid for 8 hours) 183 | ${session_token}= Generate JWT Token ${session_data} ${SECRET_KEY} expiration_hours=8 184 | 185 | # Step 3: Verify token is valid 186 | ${is_valid}= Verify JWT Token ${session_token} ${SECRET_KEY} 187 | Should Be True ${is_valid} 188 | 189 | Log ✓ Login successful - Session token created 190 | RETURN ${session_token} 191 | 192 | Demo API Authorization Check 193 | [Arguments] ${token} ${required_role} 194 | [Documentation] Example workflow for API endpoint authorization 195 | 196 | Log Checking authorization for role: ${required_role} 197 | 198 | # Step 1: Verify token is valid and not expired 199 | ${is_valid}= Verify JWT Token ${token} ${SECRET_KEY} 200 | Should Be True ${is_valid} Token is invalid or expired 201 | 202 | # Step 2: Extract user role from token 203 | ${user_role}= Get JWT Claim ${token} role 204 | 205 | # Step 3: Check if user has required role 206 | Should Be Equal ${user_role} ${required_role} Insufficient permissions 207 | 208 | Log ✓ Authorization successful - User has ${required_role} role 209 | RETURN ${True} 210 | 211 | Demo Token Refresh 212 | [Arguments] ${old_token} 213 | [Documentation] Example workflow for refreshing an expired token 214 | 215 | Log Refreshing token... 216 | 217 | # Step 1: Extract user data from old token (even if expired) 218 | ${old_data}= Decode JWT Payload ${old_token} verify_signature=False 219 | 220 | # Step 2: Create new token with updated timestamp 221 | ${refresh_data}= Copy Dictionary ${old_data} 222 | ${refresh_timestamp}= Generate Current Timestamp 223 | Set To Dictionary ${refresh_data} refreshed_at=${refresh_timestamp} 224 | 225 | # Step 3: Generate new token 226 | ${new_token}= Generate JWT Token ${refresh_data} ${SECRET_KEY} 227 | 228 | Log ✓ Token refreshed successfully 229 | RETURN ${new_token} 230 | -------------------------------------------------------------------------------- /src/JWTLibrary/jwt_library.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main JWT Library for Robot Framework 3 | 4 | This library provides keywords for JSON Web Token (JWT) operations in Robot Framework tests. 5 | 6 | Author: JWT Robot Framework Library Team 7 | Version: 1.0.0 8 | License: Apache 2.0 9 | """ 10 | 11 | import logging 12 | 13 | from .keywords.token_decoding import TokenDecodingKeywords 14 | from .keywords.token_generation import TokenGenerationKeywords 15 | from .keywords.token_validation import TokenValidationKeywords 16 | from .keywords.utilities import UtilityKeywords 17 | from .version import __version__ 18 | 19 | # Define a logger attribute 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | class JWTLibrary( 24 | TokenGenerationKeywords, 25 | TokenDecodingKeywords, 26 | TokenValidationKeywords, 27 | UtilityKeywords, 28 | ): 29 | """ 30 | JWT Library provides keywords for encoding, decoding, and validating JSON Web Tokens. 31 | 32 | = Table of contents = 33 | 34 | - `Introduction` 35 | - `Keywords` 36 | - `Examples` 37 | 38 | = Introduction = 39 | 40 | This library enables Robot Framework tests to work with JSON Web Tokens (JWT). 41 | It provides functionality to: 42 | 43 | - Generate JWT tokens with custom payloads 44 | - Decode JWT tokens and extract payloads 45 | - Validate JWT signatures and claims 46 | - Handle token expiration and timing 47 | - Compare and analyze token contents 48 | 49 | The library supports various JWT algorithms including: 50 | - HMAC: HS256, HS384, HS512 51 | - RSA: RS256, RS384, RS512 52 | - ECDSA: ES256, ES384, ES512 53 | - PSS: PS256, PS384, PS512 54 | 55 | = Installation = 56 | 57 | This library requires the PyJWT package: 58 | 59 | | pip install PyJWT robotframework 60 | 61 | = Keywords = 62 | 63 | == Token Generation == 64 | - `Generate JWT Token` - Creates JWT tokens with custom payloads 65 | - `Generate JWT Token With Claims` - Creates tokens using keyword arguments 66 | - `Generate JWT Token Without Expiration` - Creates non-expiring tokens 67 | - `Generate JWT Token With Custom Expiration` - Creates tokens with specific expiration 68 | 69 | == Token Decoding == 70 | - `Decode JWT Payload` - Decodes token payloads with optional verification 71 | - `Decode JWT Header` - Decodes token headers 72 | - `Get JWT Claim` - Extracts specific claims from tokens 73 | - `Get Multiple JWT Claims` - Extracts multiple claims 74 | - `Extract All JWT Claims` - Gets all claims with metadata 75 | 76 | == Token Validation == 77 | - `Verify JWT Token` - Validates token signatures and expiration 78 | - `Check JWT Expiration` - Checks token expiration status 79 | - `Validate JWT Claims` - Validates expected claim values 80 | - `Check JWT Algorithm` - Validates token algorithm 81 | - `Validate JWT Structure` - Validates token format 82 | - `Check JWT Not Before` - Validates nbf claim 83 | - `Validate JWT Audience` - Validates audience claim 84 | 85 | == Utilities == 86 | - `Create JWT Payload` - Helper to create payload dictionaries 87 | - `Get JWT Token Info` - Gets comprehensive token information 88 | - `Compare JWT Tokens` - Compares two tokens 89 | - `Extract JWT Timestamps` - Extracts timestamp claims 90 | - `Generate Current Timestamp` - Creates current timestamp 91 | - `Generate Future Timestamp` - Creates future timestamp 92 | - `Convert Timestamp To Datetime` - Converts timestamps to datetime 93 | - `Create JWT Header` - Creates header dictionaries 94 | - `Format JWT Claims For Logging` - Formats claims for safe logging 95 | - `Validate JWT Claim Types` - Validates claim data types 96 | 97 | = Usage Examples = 98 | 99 | == Basic Token Operations == 100 | 101 | | *** Settings *** 102 | | Library JWTLibrary 103 | | 104 | | *** Variables *** 105 | | ${SECRET_KEY} my_secret_key_123 106 | | 107 | | *** Test Cases *** 108 | | Basic JWT Operations 109 | | # Create payload 110 | | ${payload}= Create Dictionary user_id=123 role=admin 111 | | 112 | | # Generate token 113 | | ${token}= Generate JWT Token ${payload} ${SECRET_KEY} 114 | | 115 | | # Decode and verify 116 | | ${decoded}= Decode JWT Payload ${token} ${SECRET_KEY} 117 | | Should Be Equal ${decoded['user_id']} 123 118 | | 119 | | # Validate token 120 | | ${is_valid}= Verify JWT Token ${token} ${SECRET_KEY} 121 | | Should Be True ${is_valid} 122 | 123 | == Advanced Token Validation == 124 | 125 | | Advanced JWT Validation 126 | | ${payload}= Create Dictionary user_id=456 role=user email=test@example.com 127 | | ${token}= Generate JWT Token ${payload} ${SECRET_KEY} expiration_hours=1 128 | | 129 | | # Check expiration 130 | | ${exp_info}= Check JWT Expiration ${token} 131 | | Should Be Equal ${exp_info['is_expired']} ${False} 132 | | 133 | | # Validate specific claims 134 | | ${expected}= Create Dictionary role=user user_id=456 135 | | ${claims_valid}= Validate JWT Claims ${token} ${expected} ${SECRET_KEY} True 136 | | Should Be True ${claims_valid} 137 | | 138 | | # Check algorithm 139 | | ${alg_correct}= Check JWT Algorithm ${token} HS256 140 | | Should Be True ${alg_correct} 141 | 142 | == Token Comparison and Analysis == 143 | 144 | | JWT Analysis 145 | | ${payload1}= Create Dictionary user_id=123 role=admin 146 | | ${payload2}= Create Dictionary user_id=123 role=user 147 | | 148 | | ${token1}= Generate JWT Token ${payload1} ${SECRET_KEY} 149 | | ${token2}= Generate JWT Token ${payload2} ${SECRET_KEY} 150 | | 151 | | # Compare tokens 152 | | ${comparison}= Compare JWT Tokens ${token1} ${token2} 153 | | Should Be Equal ${comparison['are_identical']} ${False} 154 | | Should Be Equal ${comparison['payload_differences_count']} 1 155 | | 156 | | # Get detailed token info 157 | | ${info}= Get JWT Token Info ${token1} 158 | | Log Token Algorithm: ${info['algorithm']} 159 | | Log Claims Count: ${info['claims_count']} 160 | 161 | = Error Handling = 162 | 163 | The library provides specific exceptions for different error conditions: 164 | 165 | - `JWTTokenGenerationError` - Token generation failures 166 | - `JWTTokenDecodingError` - Token decoding failures 167 | - `JWTTokenValidationError` - Token validation failures 168 | - `JWTExpiredTokenError` - Expired token errors 169 | - `JWTInvalidSignatureError` - Signature verification failures 170 | - `JWTInvalidTokenError` - Invalid token format errors 171 | - `JWTClaimNotFoundError` - Missing claim errors 172 | 173 | = Security Considerations = 174 | 175 | - Always use strong secret keys for HMAC algorithms 176 | - Consider using RSA or ECDSA for distributed systems 177 | - Validate all claims, especially audience and issuer 178 | - Check token expiration and not-before claims 179 | - Use appropriate token lifetimes for your use case 180 | - Store secret keys securely, never in code or logs 181 | 182 | = Performance Notes = 183 | 184 | - Token verification involves cryptographic operations 185 | - Cache decoded payloads when possible 186 | - Use shorter tokens for better performance 187 | - Consider token format for network efficiency 188 | """ 189 | 190 | ROBOT_LIBRARY_SCOPE = "GLOBAL" 191 | ROBOT_LIBRARY_VERSION = __version__ 192 | ROBOT_LIBRARY_DOC_FORMAT = "ROBOT" 193 | 194 | def __init__(self): 195 | """Initialize the JWT Library with default settings.""" 196 | # Initialize parent classes 197 | TokenGenerationKeywords.__init__(self) 198 | TokenDecodingKeywords.__init__(self) 199 | TokenValidationKeywords.__init__(self) 200 | UtilityKeywords.__init__(self) 201 | 202 | # Library metadata 203 | self.library_name = "JWTLibrary" 204 | self.library_version = __version__ 205 | 206 | 207 | # For backward compatibility and direct import 208 | if __name__ == "__main__": 209 | # Example usage when run directly 210 | 211 | print("JWT Library Example Usage") 212 | print("=" * 50) 213 | 214 | jwt_lib = JWTLibrary() 215 | 216 | # Example 1: Basic token operations 217 | print("\n1. Basic Token Operations:") 218 | payload = {"user_id": 123, "username": "testuser", "role": "admin"} 219 | secret_key = "my_secret_key_123" 220 | 221 | token = jwt_lib.generate_jwt_token(payload, secret_key) 222 | 223 | print(f"Generated Token: {token[:50]}...") 224 | 225 | decoded_payload = jwt_lib.decode_jwt_payload(token, secret_key) 226 | print(f"Decoded Successfully: {decoded_payload['user_id']}") 227 | 228 | is_valid = jwt_lib.verify_jwt_token(token, secret_key) 229 | print(f"Token Valid: {is_valid}") 230 | 231 | # Example 2: Token analysis 232 | print("\n2. Token Analysis:") 233 | token_info = jwt_lib.get_jwt_token_info(token) 234 | print(f"Algorithm: {token_info['algorithm']}") 235 | print(f"Claims Count: {token_info['claims_count']}") 236 | 237 | # Example 3: Expiration check 238 | print("\n3. Expiration Check:") 239 | exp_info = jwt_lib.check_jwt_expiration(token) 240 | print(f"Expires At: {exp_info['expires_at']}") 241 | print(f"Is Expired: {exp_info['is_expired']}") 242 | # Example 4: Claim validation 243 | print("\n4. Claim Validation:") 244 | expected_claims = {"user_id": 123, "role": "admin"} 245 | claims_valid = jwt_lib.validate_jwt_claims(token, expected_claims, secret_key, True) 246 | print(f"Claims Valid: {claims_valid}") 247 | 248 | # Example 5: Utility functions 249 | print("\n5. Utility Functions:") 250 | current_ts = jwt_lib.generate_current_timestamp() 251 | future_ts = jwt_lib.generate_future_timestamp(hours=1) 252 | print(f"Current Timestamp: {current_ts}") 253 | print(f"Future Timestamp: {future_ts}") 254 | print("\nJWT Library examples completed successfully!") 255 | -------------------------------------------------------------------------------- /src/JWTLibrary/keywords/token_decoding.py: -------------------------------------------------------------------------------- 1 | """Token decoding keywords for JWT Library.""" 2 | 3 | from typing import Any, Dict 4 | 5 | import jwt 6 | from robot.api import logger 7 | from robot.api.deco import keyword 8 | 9 | from ..constants import DEFAULT_ALGORITHM 10 | from ..exceptions import ( 11 | JWTClaimNotFoundError, 12 | JWTExpiredTokenError, 13 | JWTInvalidSignatureError, 14 | JWTTokenDecodingError, 15 | ) 16 | from ..utils import ( 17 | decode_jwt_header_unsafe, 18 | decode_jwt_payload_unsafe, 19 | safe_json_dumps, 20 | validate_algorithm, 21 | ) 22 | 23 | 24 | class TokenDecodingKeywords: 25 | """Keywords for JWT token decoding.""" 26 | 27 | @keyword("Decode JWT Payload") 28 | def decode_jwt_payload( 29 | self, 30 | token: str, 31 | secret_key: str = None, 32 | algorithm: str = None, 33 | verify_signature: bool = True, 34 | ) -> Dict[str, Any]: 35 | """ 36 | Decodes a JWT token and returns the payload. 37 | 38 | Arguments: 39 | - ``token``: JWT token string to decode 40 | - ``secret_key``: Secret key used for verification 41 | - ``algorithm``: JWT algorithm (default: HS256) 42 | - ``verify_signature``: Whether to verify token signature (default: True) 43 | 44 | Returns: 45 | - Dictionary containing the decoded payload 46 | 47 | Examples: 48 | | ${payload}= Decode JWT Payload ${token} my_secret_key 49 | | ${payload}= Decode JWT Payload ${token} my_secret_key algorithm=HS512 50 | | ${payload}= Decode JWT Payload ${token} verify_signature=False 51 | """ 52 | try: 53 | if verify_signature: 54 | if not secret_key: 55 | raise JWTTokenDecodingError( 56 | "Secret key is required when verify_signature is True" 57 | ) 58 | 59 | algorithm = algorithm or DEFAULT_ALGORITHM 60 | validate_algorithm(algorithm) 61 | algorithms = [algorithm] 62 | 63 | # Decode with verification 64 | payload = jwt.decode(token, secret_key, algorithms=algorithms) 65 | else: 66 | # Decode without verification 67 | payload = jwt.decode(token, options={"verify_signature": False}) 68 | 69 | logger.info("JWT token decoded successfully") 70 | logger.debug(f"Decoded payload: {safe_json_dumps(payload, indent=2)}") 71 | 72 | return payload 73 | 74 | except jwt.ExpiredSignatureError: 75 | error_msg = "JWT token has expired" 76 | logger.error(error_msg) 77 | raise JWTExpiredTokenError(error_msg) 78 | except jwt.InvalidSignatureError: 79 | error_msg = "JWT token signature verification failed" 80 | logger.error(error_msg) 81 | raise JWTInvalidSignatureError(error_msg) 82 | except jwt.InvalidTokenError as e: 83 | error_msg = f"Invalid JWT token: {str(e)}" 84 | logger.error(error_msg) 85 | raise JWTTokenDecodingError(error_msg) 86 | except Exception as e: 87 | error_msg = f"JWT token decoding failed: {str(e)}" 88 | logger.error(error_msg) 89 | raise JWTTokenDecodingError(error_msg) 90 | 91 | @keyword("Decode JWT Header") 92 | def decode_jwt_header(self, token: str) -> Dict[str, Any]: 93 | """ 94 | Decodes JWT token header without verification. 95 | 96 | Arguments: 97 | - ``token``: JWT token string 98 | 99 | Returns: 100 | - Dictionary containing the token header 101 | 102 | Examples: 103 | | ${header}= Decode JWT Header ${token} 104 | """ 105 | try: 106 | header = decode_jwt_header_unsafe(token) 107 | logger.info("JWT header decoded successfully") 108 | logger.debug(f"Header: {safe_json_dumps(header, indent=2)}") 109 | return header 110 | except Exception as e: 111 | error_msg = f"JWT header decoding failed: {str(e)}" 112 | logger.error(error_msg) 113 | raise JWTTokenDecodingError(error_msg) 114 | 115 | @keyword("Get JWT Claim") 116 | def get_jwt_claim( 117 | self, 118 | token: str, 119 | claim_name: str, 120 | secret_key: str = None, 121 | verify_signature: bool = False, 122 | ) -> Any: 123 | """ 124 | Extracts a specific claim from JWT token payload. 125 | 126 | Arguments: 127 | - ``token``: JWT token string 128 | - ``claim_name``: Name of the claim to extract 129 | - ``secret_key``: Secret key (required if verify_signature is True) 130 | - ``verify_signature``: Whether to verify token signature 131 | 132 | Returns: 133 | - Value of the specified claim 134 | 135 | Examples: 136 | | ${user_id}= Get JWT Claim ${token} user_id 137 | | ${role}= Get JWT Claim ${token} role secret_key=my_secret_key verify_signature=True 138 | """ 139 | try: 140 | if verify_signature and not secret_key: 141 | raise JWTTokenDecodingError( 142 | "Secret key is required when verify_signature is True" 143 | ) 144 | 145 | # Get payload 146 | payload = self.decode_jwt_payload( 147 | token, secret_key, verify_signature=verify_signature 148 | ) 149 | 150 | if claim_name not in payload: 151 | raise JWTClaimNotFoundError( 152 | f"Claim '{claim_name}' not found in token payload" 153 | ) 154 | 155 | claim_value = payload[claim_name] 156 | logger.info(f"Retrieved claim '{claim_name}': {claim_value}") 157 | return claim_value 158 | 159 | except (JWTTokenDecodingError, JWTClaimNotFoundError): 160 | raise 161 | except Exception as e: 162 | error_msg = f"Failed to get JWT claim '{claim_name}': {str(e)}" 163 | logger.error(error_msg) 164 | raise JWTTokenDecodingError(error_msg) 165 | 166 | @keyword("Get Multiple JWT Claims") 167 | def get_multiple_jwt_claims( 168 | self, 169 | token: str, 170 | claim_names: list, 171 | secret_key: str = None, 172 | verify_signature: bool = False, 173 | ) -> Dict[str, Any]: 174 | """ 175 | Extracts multiple claims from JWT token payload. 176 | 177 | Arguments: 178 | - ``token``: JWT token string 179 | - ``claim_names``: List of claim names to extract 180 | - ``secret_key``: Secret key (required if verify_signature is True) 181 | - ``verify_signature``: Whether to verify token signature 182 | 183 | Returns: 184 | - Dictionary with claim names as keys and claim values 185 | 186 | Examples: 187 | | ${claims}= Get Multiple JWT Claims ${token} ["user_id", "role", "email"] 188 | | ${claims}= Get Multiple JWT Claims ${token} ["sub", "iat"] my_secret_key True 189 | """ 190 | try: 191 | # Get payload 192 | payload = self.decode_jwt_payload( 193 | token, secret_key, verify_signature=verify_signature 194 | ) 195 | 196 | claims = {} 197 | missing_claims = [] 198 | 199 | for claim_name in claim_names: 200 | if claim_name in payload: 201 | claims[claim_name] = payload[claim_name] 202 | else: 203 | missing_claims.append(claim_name) 204 | 205 | if missing_claims: 206 | logger.warn(f"Claims not found: {missing_claims}") 207 | 208 | logger.info(f"Retrieved {len(claims)} claims: {list(claims.keys())}") 209 | return claims 210 | 211 | except JWTTokenDecodingError: 212 | raise 213 | except Exception as e: 214 | error_msg = f"Failed to get multiple JWT claims: {str(e)}" 215 | logger.error(error_msg) 216 | raise JWTTokenDecodingError(error_msg) 217 | 218 | @keyword("Decode JWT Payload Unsafe") 219 | def decode_jwt_payload_unsafe(self, token: str) -> Dict[str, Any]: 220 | """ 221 | Decodes JWT token payload without any verification (unsafe). 222 | 223 | Arguments: 224 | - ``token``: JWT token string 225 | 226 | Returns: 227 | - Dictionary containing the decoded payload 228 | 229 | Examples: 230 | | ${payload}= Decode JWT Payload Unsafe ${token} 231 | 232 | Note: This keyword does not verify the token signature or expiration. 233 | Use only when you need to inspect token contents without validation. 234 | """ 235 | try: 236 | payload = decode_jwt_payload_unsafe(token) 237 | logger.info("JWT payload decoded without verification") 238 | logger.debug(f"Payload: {safe_json_dumps(payload, indent=2)}") 239 | return payload 240 | except Exception as e: 241 | error_msg = f"JWT payload unsafe decoding failed: {str(e)}" 242 | logger.error(error_msg) 243 | raise JWTTokenDecodingError(error_msg) 244 | 245 | @keyword("Extract All JWT Claims") 246 | def extract_all_jwt_claims( 247 | self, token: str, secret_key: str = None, verify_signature: bool = False 248 | ) -> Dict[str, Any]: 249 | """ 250 | Extracts all claims from JWT token payload with metadata. 251 | 252 | Arguments: 253 | - ``token``: JWT token string 254 | - ``secret_key``: Secret key (required if verify_signature is True) 255 | - ``verify_signature``: Whether to verify token signature 256 | 257 | Returns: 258 | - Dictionary with all claims and metadata 259 | 260 | Examples: 261 | | ${all_claims}= Extract All JWT Claims ${token} 262 | | ${all_claims}= Extract All JWT Claims ${token} my_secret_key True 263 | """ 264 | from ..constants import STANDARD_CLAIMS 265 | 266 | try: 267 | # Get payload and header 268 | payload = self.decode_jwt_payload( 269 | token, secret_key, verify_signature=verify_signature 270 | ) 271 | header = self.decode_jwt_header(token) 272 | 273 | # Separate standard and custom claims 274 | standard_claims = {} 275 | custom_claims = {} 276 | 277 | for key, value in payload.items(): 278 | if key in STANDARD_CLAIMS: 279 | standard_claims[key] = value 280 | else: 281 | custom_claims[key] = value 282 | 283 | result = { 284 | "header": header, 285 | "standard_claims": standard_claims, 286 | "custom_claims": custom_claims, 287 | "all_payload": payload, 288 | "total_claims": len(payload), 289 | } 290 | 291 | logger.info(f"Extracted all claims: {len(payload)} total claims") 292 | return result 293 | 294 | except JWTTokenDecodingError: 295 | raise 296 | except Exception as e: 297 | error_msg = f"Failed to extract all JWT claims: {str(e)}" 298 | logger.error(error_msg) 299 | raise JWTTokenDecodingError(error_msg) 300 | -------------------------------------------------------------------------------- /src/JWTLibrary/keywords/utilities.py: -------------------------------------------------------------------------------- 1 | """Utility keywords for JWT Library.""" 2 | 3 | from datetime import datetime, timedelta, timezone 4 | from typing import Any, Dict, Union 5 | 6 | from robot.api import logger 7 | from robot.api.deco import keyword 8 | 9 | from ..utils import get_token_info, safe_json_dumps 10 | 11 | 12 | class UtilityKeywords: 13 | """Utility keywords for JWT operations.""" 14 | 15 | @keyword("Create JWT Payload") 16 | def create_jwt_payload(self, **kwargs) -> Dict[str, Any]: 17 | """ 18 | Creates a JWT payload dictionary from keyword arguments. 19 | Arguments: 20 | - ``**kwargs``: Key-value pairs to include in payload 21 | Returns: 22 | - Dictionary containing the payload 23 | Examples: 24 | | ${payload}= Create JWT Payload user_id=123 25 | role=admin email=user@example.com 26 | """ 27 | payload = dict(kwargs) 28 | logger.info(f"Created JWT payload with keys: {list(payload.keys())}") 29 | return payload 30 | 31 | @keyword("Get JWT Token Info") 32 | def get_jwt_token_info(self, token: str) -> Dict[str, Any]: 33 | """ 34 | Gets comprehensive information about a JWT token without verification. 35 | Arguments: 36 | - ``token``: JWT token string 37 | Returns: 38 | - Dictionary with detailed token information 39 | Examples: 40 | | ${info}= Get JWT Token Info ${token} 41 | | Log Token algorithm: ${info['algorithm']} 42 | """ 43 | try: 44 | info = get_token_info(token) 45 | logger.info("JWT token information retrieved successfully") 46 | logger.debug(f"Token info: {safe_json_dumps(info, indent=2)}") 47 | return info 48 | except Exception as e: 49 | logger.error(f"Failed to get JWT token info: {str(e)}") 50 | raise 51 | 52 | @keyword("Compare JWT Tokens") 53 | def compare_jwt_tokens(self, token1: str, token2: str) -> Dict[str, Any]: 54 | """ 55 | Compares two JWT tokens and returns differences. 56 | Arguments: 57 | - ``token1``: First JWT token 58 | - ``token2``: Second JWT token 59 | Returns: 60 | - Dictionary with comparison results 61 | Examples: 62 | | ${comparison}= Compare JWT Tokens ${token1} ${token2} 63 | | Should Be True ${comparison['are_identical']} 64 | """ 65 | try: 66 | from .token_decoding import TokenDecodingKeywords 67 | 68 | decoder = TokenDecodingKeywords() 69 | # Decode both tokens without verification 70 | payload1 = decoder.decode_jwt_payload_unsafe(token1) 71 | payload2 = decoder.decode_jwt_payload_unsafe(token2) 72 | header1 = decoder.decode_jwt_header(token1) 73 | header2 = decoder.decode_jwt_header(token2) 74 | # Compare payloads 75 | payload_differences = {} 76 | all_keys = set(payload1.keys()) | set(payload2.keys()) 77 | for key in all_keys: 78 | val1 = payload1.get(key, "") 79 | val2 = payload2.get(key, "") 80 | if val1 != val2: 81 | payload_differences[key] = {"token1": val1, "token2": val2} 82 | # Compare headers 83 | header_differences = {} 84 | all_header_keys = set(header1.keys()) | set(header2.keys()) 85 | for key in all_header_keys: 86 | val1 = header1.get(key, "") 87 | val2 = header2.get(key, "") 88 | if val1 != val2: 89 | header_differences[key] = {"token1": val1, "token2": val2} 90 | are_identical = ( 91 | len(payload_differences) == 0 and len(header_differences) == 0 92 | ) 93 | comparison_result = { 94 | "are_identical": are_identical, 95 | "payload_differences": payload_differences, 96 | "header_differences": header_differences, 97 | "payload_differences_count": len(payload_differences), 98 | "header_differences_count": len(header_differences), 99 | } 100 | logger.info( 101 | f"JWT token comparison: " 102 | f"{'Identical' if are_identical else 'Different'}" 103 | ) 104 | return comparison_result 105 | except Exception as e: 106 | logger.error(f"JWT token comparison failed: {str(e)}") 107 | raise 108 | 109 | @keyword("Extract JWT Timestamps") 110 | def extract_jwt_timestamps(self, token: str) -> Dict[str, Any]: 111 | """ 112 | Extracts all timestamp claims from JWT token. 113 | Arguments: 114 | - ``token``: JWT token string 115 | Returns: 116 | - Dictionary with timestamp information 117 | Examples: 118 | | ${timestamps}= Extract JWT Timestamps ${token} 119 | | Log Token issued at: ${timestamps['issued_at']} 120 | """ 121 | try: 122 | from .token_decoding import TokenDecodingKeywords 123 | 124 | decoder = TokenDecodingKeywords() 125 | payload = decoder.decode_jwt_payload_unsafe(token) 126 | timestamps = {} 127 | # Extract timestamp claims 128 | timestamp_claims = ["iat", "exp", "nbf"] 129 | for claim in timestamp_claims: 130 | if claim in payload: 131 | timestamp = payload[claim] 132 | if isinstance(timestamp, (int, float)): 133 | dt = datetime.fromtimestamp(timestamp) 134 | timestamps[claim] = { 135 | "timestamp": timestamp, 136 | "datetime": dt.isoformat(), 137 | "human_readable": dt.strftime("%Y-%m-%d %H:%M:%S UTC"), 138 | } 139 | # Add computed fields 140 | current_time = datetime.utcnow() 141 | if "iat" in timestamps: 142 | issued_dt = datetime.fromtimestamp(timestamps["iat"]["timestamp"]) 143 | timestamps["age_seconds"] = (current_time - issued_dt).total_seconds() 144 | if "exp" in timestamps: 145 | exp_dt = datetime.fromtimestamp(timestamps["exp"]["timestamp"]) 146 | timestamps["expires_in_seconds"] = ( 147 | exp_dt - current_time 148 | ).total_seconds() 149 | timestamps["is_expired"] = current_time > exp_dt 150 | logger.info(f"Extracted {len(timestamps)} timestamp claims from JWT") 151 | return timestamps 152 | except Exception as e: 153 | logger.error(f"JWT timestamp extraction failed: {str(e)}") 154 | raise 155 | 156 | @keyword("Generate Current Timestamp") 157 | def generate_current_timestamp(self) -> int: 158 | """ 159 | Generates current UTC timestamp for JWT claims. 160 | Returns: 161 | - Current timestamp as integer 162 | Examples: 163 | | ${current_time}= Generate Current Timestamp 164 | | ${payload}= Create Dictionary iat=${current_time} 165 | """ 166 | timestamp = int(datetime.now(tz=timezone.utc).timestamp()) 167 | logger.info(f"Generated current timestamp: {timestamp}") 168 | return timestamp 169 | 170 | @keyword("Generate Future Timestamp") 171 | def generate_future_timestamp( 172 | self, hours: int = 24, minutes: int = 0, seconds: int = 0 173 | ) -> int: 174 | """ 175 | Generates future timestamp for JWT expiration claims. 176 | Arguments: 177 | - ``hours``: Hours to add (default: 24) 178 | - ``minutes``: Minutes to add (default: 0) 179 | - ``seconds``: Seconds to add (default: 0) 180 | Returns: 181 | - Future timestamp as integer 182 | Examples: 183 | | ${exp_time}= Generate Future Timestamp hours=1 184 | | ${exp_time}= Generate Future Timestamp hours=0 minutes=30 185 | """ 186 | future_time = datetime.now(tz=timezone.utc) + timedelta( 187 | hours=hours, minutes=minutes, seconds=seconds 188 | ) 189 | timestamp = int(future_time.timestamp()) 190 | logger.info( 191 | f"Generated future timestamp: {timestamp} " 192 | f"({hours}h {minutes}m {seconds}s from now)" 193 | ) 194 | return timestamp 195 | 196 | @keyword("Convert Timestamp To Datetime") 197 | def convert_timestamp_to_datetime(self, timestamp: Union[int, float]) -> str: 198 | """ 199 | Converts Unix timestamp to human-readable datetime string. 200 | Arguments: 201 | - ``timestamp``: Unix timestamp 202 | Returns: 203 | - ISO format datetime string 204 | Examples: 205 | | ${datetime_str}= Convert Timestamp To Datetime 1640995200 206 | """ 207 | try: 208 | dt = datetime.fromtimestamp(timestamp) 209 | datetime_str = dt.isoformat() 210 | logger.info(f"Converted timestamp {timestamp} to {datetime_str}") 211 | return datetime_str 212 | except Exception as e: 213 | logger.error(f"Timestamp conversion failed: {str(e)}") 214 | raise 215 | 216 | @keyword("Create JWT Header") 217 | def create_jwt_header(self, algorithm: str = "HS256", **kwargs) -> Dict[str, Any]: 218 | """ 219 | Creates a JWT header dictionary. 220 | Arguments: 221 | - ``algorithm``: JWT algorithm (default: HS256) 222 | - ``**kwargs``: Additional header parameters 223 | Returns: 224 | - Dictionary containing the header 225 | Examples: 226 | | ${header}= Create JWT Header algorithm=RS256 kid=key1 227 | """ 228 | header = {"alg": algorithm, "typ": "JWT"} 229 | header.update(kwargs) 230 | logger.info(f"Created JWT header with algorithm: {algorithm}") 231 | return header 232 | 233 | @keyword("Format JWT Claims For Logging") 234 | def format_jwt_claims_for_logging( 235 | self, claims: Dict[str, Any], mask_sensitive: bool = True 236 | ) -> str: 237 | """ 238 | Formats JWT claims for safe logging. 239 | Arguments: 240 | - ``claims``: Dictionary of JWT claims 241 | - ``mask_sensitive``: Whether to mask sensitive values 242 | Returns: 243 | - Formatted string suitable for logging 244 | Examples: 245 | | ${log_str}= Format JWT Claims For Logging ${claims} 246 | | Log Claims: ${log_str} 247 | """ 248 | try: 249 | if mask_sensitive: 250 | from ..utils import mask_sensitive_data 251 | 252 | safe_claims = mask_sensitive_data(claims) 253 | else: 254 | safe_claims = claims 255 | formatted = safe_json_dumps(safe_claims, indent=2) 256 | logger.debug("Formatted JWT claims for logging") 257 | return formatted 258 | except Exception as e: 259 | logger.error(f"JWT claims formatting failed: {str(e)}") 260 | return str(claims) 261 | 262 | @keyword("Validate JWT Claim Types") 263 | def validate_jwt_claim_types(self, claims: Dict[str, Any]) -> Dict[str, Any]: 264 | """ 265 | Validates JWT claim data types. 266 | Arguments: 267 | - ``claims``: Dictionary of JWT claims 268 | Returns: 269 | - Dictionary with validation results 270 | Examples: 271 | | ${validation}= Validate JWT Claim Types ${claims} 272 | | Should Be True ${validation['is_valid']} 273 | """ 274 | validation_result = {"is_valid": True, "errors": [], "warnings": []} 275 | # Check standard claim types 276 | type_expectations = { 277 | "iss": str, # issuer 278 | "sub": str, # subject 279 | "aud": (str, list), # audience 280 | "exp": (int, float), # expiration 281 | "nbf": (int, float), # not before 282 | "iat": (int, float), # issued at 283 | "jti": str, # JWT ID 284 | } 285 | for claim_name, claim_value in claims.items(): 286 | if claim_name in type_expectations: 287 | expected_type = type_expectations[claim_name] 288 | if not isinstance(claim_value, expected_type): 289 | validation_result["errors"].append( 290 | f"Claim '{claim_name}' should be {expected_type}" 291 | f", got {type(claim_value)}" 292 | ) 293 | validation_result["is_valid"] = False 294 | # Additional validations 295 | if "exp" in claims and "iat" in claims: 296 | if claims["exp"] <= claims["iat"]: 297 | validation_result["warnings"].append( 298 | "Expiration time should be after issued at time" 299 | ) 300 | logger.info( 301 | f"JWT claim type validation:" 302 | f" {'Valid' if validation_result['is_valid'] else 'Invalid'}" 303 | ) 304 | return validation_result 305 | -------------------------------------------------------------------------------- /src/JWTLibrary/cli.py: -------------------------------------------------------------------------------- 1 | """Command-line interface for JWT Library.""" 2 | 3 | import argparse 4 | import json 5 | import sys 6 | from datetime import datetime 7 | 8 | from .exceptions import JWTLibraryError 9 | from .jwt_library import JWTLibrary 10 | from .version import __version__ 11 | 12 | 13 | def create_parser() -> argparse.ArgumentParser: 14 | """Create and configure the argument parser.""" 15 | parser = argparse.ArgumentParser( 16 | prog="jwt-robot-tool", 17 | description="JWT Robot Framework Library CLI Tool", 18 | formatter_class=argparse.RawDescriptionHelpFormatter, 19 | epilog=""" 20 | Examples: 21 | # Generate a JWT token 22 | jwt-robot-tool generate --payload '{"user_id": 123}' --secret mykey 23 | 24 | # Decode a JWT token 25 | jwt-robot-tool decode --token --secret mykey 26 | 27 | # Validate a JWT token 28 | jwt-robot-tool validate --token --secret mykey 29 | 30 | # Get token information 31 | jwt-robot-tool info --token 32 | 33 | # Compare two tokens 34 | jwt-robot-tool compare --token1 --token2 35 | """, 36 | ) 37 | parser.add_argument( 38 | "--version", action="version", version=f"JWT Library {__version__}" 39 | ) 40 | subparsers = parser.add_subparsers(dest="command", help="Available commands") 41 | # Generate command 42 | generate_parser = subparsers.add_parser("generate", help="Generate a JWT token") 43 | generate_parser.add_argument( 44 | "--payload", required=True, help="JSON payload for the token" 45 | ) 46 | generate_parser.add_argument( 47 | "--secret", required=True, help="Secret key for signing" 48 | ) 49 | generate_parser.add_argument( 50 | "--algorithm", default="HS256", help="JWT algorithm (default: HS256)" 51 | ) 52 | generate_parser.add_argument( 53 | "--expiration-hours", 54 | type=int, 55 | default=24, 56 | help="Token expiration in hours (default: 24)", 57 | ) 58 | generate_parser.add_argument( 59 | "--no-expiration", action="store_true", help="Generate token without expiration" 60 | ) 61 | # Decode command 62 | decode_parser = subparsers.add_parser("decode", help="Decode a JWT token") 63 | decode_parser.add_argument("--token", required=True, help="JWT token to decode") 64 | decode_parser.add_argument( 65 | "--secret", help="Secret key for verification (optional for unsafe decode)" 66 | ) 67 | decode_parser.add_argument( 68 | "--algorithm", default="HS256", help="JWT algorithm (default: HS256)" 69 | ) 70 | decode_parser.add_argument( 71 | "--no-verify", action="store_true", help="Decode without signature verification" 72 | ) 73 | # Validate command 74 | validate_parser = subparsers.add_parser("validate", help="Validate a JWT token") 75 | validate_parser.add_argument("--token", required=True, help="JWT token to validate") 76 | validate_parser.add_argument( 77 | "--secret", required=True, help="Secret key for validation" 78 | ) 79 | validate_parser.add_argument( 80 | "--algorithm", default="HS256", help="JWT algorithm (default: HS256)" 81 | ) 82 | validate_parser.add_argument( 83 | "--expected-claims", help="JSON object of expected claims" 84 | ) 85 | # Info command 86 | info_parser = subparsers.add_parser("info", help="Get JWT token information") 87 | info_parser.add_argument("--token", required=True, help="JWT token to analyze") 88 | 89 | # Compare command 90 | compare_parser = subparsers.add_parser("compare", help="Compare two JWT tokens") 91 | compare_parser.add_argument("--token1", required=True, help="First JWT token") 92 | compare_parser.add_argument("--token2", required=True, help="Second JWT token") 93 | # Verify command 94 | verify_parser = subparsers.add_parser( 95 | "verify", help="Verify JWT token signature and expiration" 96 | ) 97 | verify_parser.add_argument("--token", required=True, help="JWT token to verify") 98 | verify_parser.add_argument( 99 | "--secret", required=True, help="Secret key for verification" 100 | ) 101 | verify_parser.add_argument( 102 | "--algorithm", default="HS256", help="JWT algorithm (default: HS256)" 103 | ) 104 | return parser 105 | 106 | 107 | def command_generate(args, jwt_lib: JWTLibrary) -> int: 108 | """Handle the generate command.""" 109 | try: 110 | # Parse payload JSON 111 | payload = json.loads(args.payload) 112 | 113 | if args.no_expiration: 114 | token = jwt_lib.generate_jwt_token_without_expiration( 115 | payload, args.secret, args.algorithm 116 | ) 117 | else: 118 | token = jwt_lib.generate_jwt_token( 119 | payload, args.secret, args.algorithm, args.expiration_hours 120 | ) 121 | 122 | print(f"Generated JWT Token:") 123 | print(token) 124 | print() 125 | 126 | # Show token info 127 | info = jwt_lib.get_jwt_token_info(token) 128 | print("Token Information:") 129 | print(json.dumps(info, indent=2, default=str)) 130 | return 0 131 | 132 | except json.JSONDecodeError as e: 133 | print(f"Error: Invalid JSON payload - {e}", file=sys.stderr) 134 | return 1 135 | except JWTLibraryError as e: 136 | print(f"Error: {e}", file=sys.stderr) 137 | return 1 138 | except Exception as e: 139 | print(f"Unexpected error: {e}", file=sys.stderr) 140 | return 1 141 | 142 | 143 | def command_decode(args, jwt_lib: JWTLibrary) -> int: 144 | """Handle the decode command.""" 145 | try: 146 | verify_signature = not args.no_verify and args.secret is not None 147 | 148 | if verify_signature: 149 | payload = jwt_lib.decode_jwt_payload( 150 | args.token, args.secret, args.algorithm, verify_signature=True 151 | ) 152 | print("✓ Token signature verified and decoded successfully") 153 | else: 154 | payload = jwt_lib.decode_jwt_payload(args.token, verify_signature=False) 155 | print("⚠ Token decoded without verification (unsafe)") 156 | print() 157 | print("Decoded Payload:") 158 | print(json.dumps(payload, indent=2, default=str)) 159 | 160 | # Also show header 161 | header = jwt_lib.decode_jwt_header(args.token) 162 | print() 163 | print("Token Header:") 164 | print(json.dumps(header, indent=2)) 165 | return 0 166 | 167 | except JWTLibraryError as e: 168 | print(f"Error: {e}", file=sys.stderr) 169 | return 1 170 | except Exception as e: 171 | print(f"Unexpected error: {e}", file=sys.stderr) 172 | return 1 173 | 174 | 175 | def command_validate(args, jwt_lib: JWTLibrary) -> int: 176 | """Handle the validate command.""" 177 | try: 178 | # Verify token 179 | is_valid = jwt_lib.verify_jwt_token(args.token, args.secret, args.algorithm) 180 | 181 | if is_valid: 182 | print("✓ Token is valid") 183 | else: 184 | print("✗ Token is invalid") 185 | return 1 186 | 187 | # Check expiration 188 | exp_info = jwt_lib.check_jwt_expiration(args.token) 189 | if exp_info["has_expiration"]: 190 | if exp_info["is_expired"]: 191 | print("✗ Token has expired") 192 | return 1 193 | else: 194 | print(f"✓ Token expires in {exp_info['time_until_expiry']:.0f} seconds") 195 | else: 196 | print("ℹ Token has no expiration") 197 | 198 | # Validate expected claims if provided 199 | if args.expected_claims: 200 | expected = json.loads(args.expected_claims) 201 | claims_valid = jwt_lib.validate_jwt_claims( 202 | args.token, expected, args.secret, verify_signature=True 203 | ) 204 | 205 | if claims_valid: 206 | print("✓ Expected claims match") 207 | else: 208 | print("✗ Expected claims do not match") 209 | return 1 210 | 211 | print() 212 | print("Validation Summary:") 213 | print(f" Signature: {'Valid' if is_valid else 'Invalid'}") 214 | print( 215 | f" Expiration: {'Valid' if not exp_info.get('is_expired', False) else 'Expired'}" 216 | ) 217 | if args.expected_claims: 218 | print(f" Claims: {'Valid' if claims_valid else 'Invalid'}") 219 | 220 | return 0 221 | 222 | except json.JSONDecodeError as e: 223 | print(f"Error: Invalid JSON in expected claims - {e}", file=sys.stderr) 224 | return 1 225 | except JWTLibraryError as e: 226 | print(f"Error: {e}", file=sys.stderr) 227 | return 1 228 | except Exception as e: 229 | print(f"Unexpected error: {e}", file=sys.stderr) 230 | return 1 231 | 232 | 233 | def command_info(args, jwt_lib: JWTLibrary) -> int: 234 | """Handle the info command.""" 235 | try: 236 | # Get comprehensive token info 237 | info = jwt_lib.get_jwt_token_info(args.token) 238 | 239 | print("JWT Token Information:") 240 | print("=" * 50) 241 | print(f"Algorithm: {info.get('algorithm', 'Unknown')}") 242 | print(f"Type: {info.get('type', 'Unknown')}") 243 | print(f"Claims Count: {info.get('claims_count', 0)}") 244 | 245 | if info.get("issued_at"): 246 | issued_dt = datetime.fromtimestamp(info["issued_at"]) 247 | print(f"Issued At: {issued_dt.isoformat()}") 248 | 249 | if info.get("expires_at"): 250 | expires_dt = datetime.fromtimestamp(info["expires_at"]) 251 | print(f"Expires At: {expires_dt.isoformat()}") 252 | 253 | if info.get("is_expired") is not None: 254 | status = "Expired" if info["is_expired"] else "Valid" 255 | print(f"Status: {status}") 256 | 257 | if info.get("not_before"): 258 | nbf_dt = datetime.fromtimestamp(info["not_before"]) 259 | print(f"Not Before: {nbf_dt.isoformat()}") 260 | 261 | print(f"Issuer: {info.get('issuer', 'Not specified')}") 262 | print(f"Subject: {info.get('subject', 'Not specified')}") 263 | print(f"Audience: {info.get('audience', 'Not specified')}") 264 | 265 | print() 266 | print("Header Parameters:") 267 | for param in info.get("header_params", []): 268 | print(f" - {param}") 269 | 270 | print() 271 | print("Payload Claims:") 272 | for claim in info.get("payload_claims", []): 273 | print(f" - {claim}") 274 | 275 | # Extract timestamps for detailed analysis 276 | timestamps = jwt_lib.extract_jwt_timestamps(args.token) 277 | if timestamps: 278 | print() 279 | print("Timestamp Analysis:") 280 | for claim, ts_info in timestamps.items(): 281 | if isinstance(ts_info, dict) and "human_readable" in ts_info: 282 | print(f" {claim}: {ts_info['human_readable']}") 283 | 284 | return 0 285 | 286 | except JWTLibraryError as e: 287 | print(f"Error: {e}", file=sys.stderr) 288 | return 1 289 | except Exception as e: 290 | print(f"Unexpected error: {e}", file=sys.stderr) 291 | return 1 292 | 293 | 294 | def command_compare(args, jwt_lib: JWTLibrary) -> int: 295 | """Handle the compare command.""" 296 | try: 297 | comparison = jwt_lib.compare_jwt_tokens(args.token1, args.token2) 298 | 299 | print("JWT Token Comparison:") 300 | print("=" * 50) 301 | 302 | if comparison["are_identical"]: 303 | print("✓ Tokens are identical") 304 | else: 305 | print("✗ Tokens are different") 306 | 307 | print(f"Payload differences: {comparison['payload_differences_count']}") 308 | print(f"Header differences: {comparison['header_differences_count']}") 309 | 310 | if comparison["payload_differences"]: 311 | print() 312 | print("Payload Differences:") 313 | for claim, diff in comparison["payload_differences"].items(): 314 | print(f" {claim}:") 315 | print(f" Token 1: {diff['token1']}") 316 | print(f" Token 2: {diff['token2']}") 317 | 318 | if comparison["header_differences"]: 319 | print() 320 | print("Header Differences:") 321 | for param, diff in comparison["header_differences"].items(): 322 | print(f" {param}:") 323 | print(f" Token 1: {diff['token1']}") 324 | print(f" Token 2: {diff['token2']}") 325 | 326 | return 0 327 | 328 | except JWTLibraryError as e: 329 | print(f"Error: {e}", file=sys.stderr) 330 | return 1 331 | except Exception as e: 332 | print(f"Unexpected error: {e}", file=sys.stderr) 333 | return 1 334 | 335 | 336 | def command_verify(args, jwt_lib: JWTLibrary) -> int: 337 | """Handle the verify command.""" 338 | try: 339 | is_valid = jwt_lib.verify_jwt_token(args.token, args.secret, args.algorithm) 340 | 341 | if is_valid: 342 | print("✓ Token verification successful") 343 | print(" - Signature is valid") 344 | print(" - Token is not expired") 345 | print(" - Token format is correct") 346 | return 0 347 | else: 348 | print("✗ Token verification failed") 349 | 350 | # Try to provide more specific information 351 | try: 352 | # Check structure 353 | structure = jwt_lib.validate_jwt_structure(args.token) 354 | if not structure["is_valid_structure"]: 355 | print(" - Invalid token structure") 356 | for error in structure.get("errors", []): 357 | print(f" • {error}") 358 | else: 359 | # Check expiration if structure is valid 360 | exp_info = jwt_lib.check_jwt_expiration(args.token) 361 | if exp_info.get("is_expired"): 362 | print(" - Token has expired") 363 | else: 364 | print(" - Invalid signature or algorithm mismatch") 365 | except: 366 | print(" - Unable to determine specific cause") 367 | 368 | return 1 369 | 370 | except JWTLibraryError as e: 371 | print(f"Error: {e}", file=sys.stderr) 372 | return 1 373 | except Exception as e: 374 | print(f"Unexpected error: {e}", file=sys.stderr) 375 | return 1 376 | 377 | 378 | def main() -> int: 379 | """Main CLI entry point.""" 380 | parser = create_parser() 381 | args = parser.parse_args() 382 | if not args.command: 383 | parser.print_help() 384 | return 1 385 | # Initialize JWT Library 386 | jwt_lib = JWTLibrary() 387 | # Route to appropriate command handler 388 | command_handlers = { 389 | "generate": command_generate, 390 | "decode": command_decode, 391 | "validate": command_validate, 392 | "info": command_info, 393 | "compare": command_compare, 394 | "verify": command_verify, 395 | } 396 | handler = command_handlers.get(args.command) 397 | if handler: 398 | return handler(args, jwt_lib) 399 | else: 400 | print(f"Unknown command: {args.command}", file=sys.stderr) 401 | return 1 402 | 403 | 404 | if __name__ == "__main__": 405 | sys.exit(main()) 406 | -------------------------------------------------------------------------------- /tests/robot/performance/jwt_performance.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation JWT performance test suite 3 | Library JWTLibrary 4 | Library Collections 5 | Library DateTime 6 | Library OperatingSystem 7 | 8 | *** Variables *** 9 | ${PERF_SECRET} performance_test_secret_key_12345 10 | ${ITERATIONS} 1000 11 | ${LARGE_ITERATIONS} 100 12 | 13 | *** Test Cases *** 14 | Performance Test Token Generation 15 | [Documentation] Test JWT token generation performance 16 | [Tags] jwt performance generation 17 | 18 | ${payload}= Create Dictionary user_id=12345 role=user test=performance 19 | 20 | ${start_time}= Get Current Date result_format=epoch 21 | 22 | FOR ${i} IN RANGE ${ITERATIONS} 23 | ${token}= Generate JWT Token ${payload} ${PERF_SECRET} 24 | Should Not Be Empty ${token} 25 | END 26 | 27 | ${end_time}= Get Current Date result_format=epoch 28 | ${duration}= Evaluate ${end_time} - ${start_time} 29 | ${tokens_per_second}= Evaluate ${ITERATIONS} / ${duration} 30 | 31 | Log Generated ${ITERATIONS} tokens in ${duration} seconds 32 | Log Performance: ${tokens_per_second} tokens/second 33 | 34 | # Performance assertion (should generate at least 100 tokens per second) 35 | Should Be True ${tokens_per_second} > 100 36 | 37 | Performance Test Token Decoding 38 | [Documentation] Test JWT token decoding performance 39 | [Tags] jwt performance decoding 40 | 41 | ${payload}= Create Dictionary user_id=67890 role=admin test=decoding_perf 42 | ${token}= Generate JWT Token ${payload} ${PERF_SECRET} 43 | 44 | ${start_time}= Get Current Date result_format=epoch 45 | 46 | FOR ${i} IN RANGE ${ITERATIONS} 47 | ${decoded}= Decode JWT Payload ${token} ${PERF_SECRET} 48 | Should Be Equal As Integers ${decoded['user_id']} 67890 49 | END 50 | 51 | ${end_time}= Get Current Date result_format=epoch 52 | ${duration}= Evaluate ${end_time} - ${start_time} 53 | ${decodes_per_second}= Evaluate ${ITERATIONS} / ${duration} 54 | 55 | Log Decoded ${ITERATIONS} tokens in ${duration} seconds 56 | Log Performance: ${decodes_per_second} decodes/second 57 | 58 | # Performance assertion (should decode at least 200 tokens per second) 59 | Should Be True ${decodes_per_second} > 200 60 | 61 | Performance Test Token Verification 62 | [Documentation] Test JWT token verification performance 63 | [Tags] jwt performance verification 64 | 65 | ${payload}= Create Dictionary user_id=11111 role=tester test=verification_perf 66 | ${token}= Generate JWT Token ${payload} ${PERF_SECRET} 67 | 68 | ${start_time}= Get Current Date result_format=epoch 69 | 70 | FOR ${i} IN RANGE ${ITERATIONS} 71 | ${is_valid}= Verify JWT Token ${token} ${PERF_SECRET} 72 | Should Be True ${is_valid} 73 | END 74 | 75 | ${end_time}= Get Current Date result_format=epoch 76 | ${duration}= Evaluate ${end_time} - ${start_time} 77 | ${verifications_per_second}= Evaluate ${ITERATIONS} / ${duration} 78 | 79 | Log Verified ${ITERATIONS} tokens in ${duration} seconds 80 | Log Performance: ${verifications_per_second} verifications/second 81 | 82 | # Performance assertion (should verify at least 200 tokens per second) 83 | Should Be True ${verifications_per_second} > 200 84 | 85 | Performance Test Large Payload Handling 86 | [Documentation] Test JWT performance with large payloads 87 | [Tags] jwt performance large-payload 88 | 89 | # Create large payload 90 | ${large_payload}= Create Dictionary user_id=99999 test=large_payload 91 | 92 | FOR ${i} IN RANGE 100 93 | Set To Dictionary ${large_payload} field_${i}=value_${i}_with_some_additional_data_to_make_it_longer 94 | ${nested_data}= Create Dictionary 95 | ... sub_field_1=nested_value_${i}_1 96 | ... sub_field_2=nested_value_${i}_2 97 | ... sub_field_3=nested_value_${i}_3 98 | Set To Dictionary ${large_payload} nested_${i}=${nested_data} 99 | END 100 | 101 | ${start_time}= Get Current Date result_format=epoch 102 | 103 | FOR ${i} IN RANGE ${LARGE_ITERATIONS} 104 | ${token}= Generate JWT Token ${large_payload} ${PERF_SECRET} 105 | ${decoded}= Decode JWT Payload ${token} ${PERF_SECRET} 106 | Should Be Equal As Integers ${decoded['user_id']} 99999 107 | END 108 | 109 | ${end_time}= Get Current Date result_format=epoch 110 | ${duration}= Evaluate ${end_time} - ${start_time} 111 | ${operations_per_second}= Evaluate ${LARGE_ITERATIONS} / ${duration} 112 | 113 | Log Processed ${LARGE_ITERATIONS} large tokens in ${duration} seconds 114 | Log Performance: ${operations_per_second} operations/second 115 | 116 | # Performance assertion for large payloads (should handle at least 10 per second) 117 | Should Be True ${operations_per_second} > 10 118 | 119 | Performance Test Different Algorithms 120 | [Documentation] Test performance comparison across different algorithms 121 | [Tags] jwt performance algorithms 122 | 123 | ${payload}= Create Dictionary user_id=55555 algorithm_test=true 124 | @{algorithms}= Create List HS256 HS384 HS512 125 | ${algorithm_results}= Create Dictionary 126 | 127 | FOR ${algorithm} IN @{algorithms} 128 | Log Testing algorithm: ${algorithm} 129 | 130 | ${start_time}= Get Current Date result_format=epoch 131 | 132 | FOR ${i} IN RANGE 500 133 | ${token}= Generate JWT Token ${payload} ${PERF_SECRET} algorithm=${algorithm} 134 | ${decoded}= Decode JWT Payload ${token} ${PERF_SECRET} algorithm=${algorithm} 135 | END 136 | 137 | ${end_time}= Get Current Date result_format=epoch 138 | ${duration}= Evaluate ${end_time} - ${start_time} 139 | ${ops_per_second}= Evaluate 500 / ${duration} 140 | 141 | Set To Dictionary ${algorithm_results} ${algorithm}=${ops_per_second} 142 | Log ${algorithm}: ${ops_per_second} operations/second 143 | END 144 | 145 | # Log comparison results 146 | Log Algorithm Performance Comparison: 147 | FOR ${alg} ${perf} IN &{algorithm_results} 148 | Log ${alg}: ${perf} ops/sec 149 | END 150 | 151 | Performance Test Concurrent Token Operations 152 | [Documentation] Test performance under simulated concurrent load 153 | [Tags] jwt performance concurrent 154 | 155 | ${payload}= Create Dictionary user_id=77777 concurrent_test=true 156 | 157 | # Simulate concurrent token generation 158 | ${start_time}= Get Current Date result_format=epoch 159 | 160 | # Generate multiple tokens rapidly 161 | @{tokens}= Create List 162 | FOR ${i} IN RANGE 200 163 | ${token}= Generate JWT Token ${payload} ${PERF_SECRET} 164 | Append To List ${tokens} ${token} 165 | END 166 | 167 | # Verify all tokens rapidly 168 | FOR ${token} IN @{tokens} 169 | ${is_valid}= Verify JWT Token ${token} ${PERF_SECRET} 170 | Should Be True ${is_valid} 171 | END 172 | 173 | ${end_time}= Get Current Date result_format=epoch 174 | ${duration}= Evaluate ${end_time} - ${start_time} 175 | ${total_operations}= Evaluate 200 * 2 # Generation + verification 176 | ${ops_per_second}= Evaluate ${total_operations} / ${duration} 177 | 178 | Log Performed ${total_operations} operations in ${duration} seconds 179 | Log Concurrent performance: ${ops_per_second} operations/second 180 | 181 | # Should handle concurrent operations efficiently 182 | Should Be True ${ops_per_second} > 300 183 | 184 | Performance Test Memory Usage Pattern 185 | [Documentation] Test memory usage patterns with JWT operations 186 | [Tags] jwt performance memory 187 | 188 | ${base_payload}= Create Dictionary user_id=88888 memory_test=true 189 | 190 | # Test with increasing payload sizes 191 | @{payload_sizes}= Create List 10 50 100 200 500 192 | 193 | FOR ${size} IN @{payload_sizes} 194 | Log Testing payload size: ${size} fields 195 | 196 | # Create payload of specified size 197 | ${test_payload}= Copy Dictionary ${base_payload} 198 | FOR ${i} IN RANGE ${size} 199 | Set To Dictionary ${test_payload} data_${i}=test_value_${i} 200 | END 201 | 202 | ${start_time}= Get Current Date result_format=epoch 203 | 204 | # Perform operations 205 | FOR ${i} IN RANGE 50 206 | ${token}= Generate JWT Token ${test_payload} ${PERF_SECRET} 207 | ${decoded}= Decode JWT Payload ${token} ${PERF_SECRET} 208 | ${info}= Get JWT Token Info ${token} 209 | END 210 | 211 | ${end_time}= Get Current Date result_format=epoch 212 | ${duration}= Evaluate ${end_time} - ${start_time} 213 | ${ops_per_second}= Evaluate 150 / ${duration} # 50 * 3 operations 214 | 215 | Log Payload size ${size}: ${ops_per_second} ops/sec 216 | END 217 | 218 | Performance Test Batch Operations 219 | [Documentation] Test performance of batch JWT operations 220 | [Tags] jwt performance batch 221 | 222 | # Generate batch of different payloads 223 | @{payloads}= Create List 224 | FOR ${i} IN RANGE 100 225 | ${payload}= Create Dictionary 226 | ... user_id=${i} 227 | ... username=user_${i} 228 | ... role=batch_user 229 | ... batch_id=${i} 230 | Append To List ${payloads} ${payload} 231 | END 232 | 233 | ${start_time}= Get Current Date result_format=epoch 234 | 235 | # Batch generate tokens 236 | @{tokens}= Create List 237 | FOR ${payload} IN @{payloads} 238 | ${token}= Generate JWT Token ${payload} ${PERF_SECRET} 239 | Append To List ${tokens} ${token} 240 | END 241 | 242 | # Batch verify tokens 243 | ${valid_count}= Set Variable 0 244 | FOR ${token} IN @{tokens} 245 | ${is_valid}= Verify JWT Token ${token} ${PERF_SECRET} 246 | ${valid_count}= Evaluate ${valid_count} + (1 if ${is_valid} else 0) 247 | END 248 | 249 | ${end_time}= Get Current Date result_format=epoch 250 | ${duration}= Evaluate ${end_time} - ${start_time} 251 | ${total_ops}= Evaluate len($tokens) * 2 # Generate + verify 252 | ${ops_per_second}= Evaluate ${total_ops} / ${duration} 253 | 254 | Log Batch processed ${total_ops} operations in ${duration} seconds 255 | Log Batch performance: ${ops_per_second} operations/second 256 | Log Valid tokens: ${valid_count}/${total_ops//2} 257 | 258 | Should Be Equal As Integers ${valid_count} 100 259 | Should Be True ${ops_per_second} > 200 260 | 261 | Performance Test Token Claim Extraction 262 | [Documentation] Test performance of claim extraction operations 263 | [Tags] jwt performance claims 264 | 265 | # Create token with many claims 266 | ${rich_payload}= Create Dictionary 267 | ... user_id=99999 268 | ... username=performance_user 269 | ... email=perf@test.com 270 | ... role=admin 271 | ... department=engineering 272 | ... clearance_level=5 273 | ... permissions=["read", "write", "delete", "admin"] 274 | ... settings={"theme": "dark", "lang": "en"} 275 | ... metadata={"created": "2024-01-01", "updated": "2024-01-15"} 276 | 277 | FOR ${i} IN RANGE 20 278 | Set To Dictionary ${rich_payload} extra_field_${i}=extra_value_${i} 279 | END 280 | 281 | ${token}= Generate JWT Token ${rich_payload} ${PERF_SECRET} 282 | 283 | ${start_time}= Get Current Date result_format=epoch 284 | 285 | # Extract claims repeatedly 286 | FOR ${i} IN RANGE ${ITERATIONS} 287 | ${user_id}= Get JWT Claim ${token} user_id 288 | ${username}= Get JWT Claim ${token} username 289 | ${role}= Get JWT Claim ${token} role 290 | ${permissions}= Get JWT Claim ${token} permissions 291 | END 292 | 293 | ${end_time}= Get Current Date result_format=epoch 294 | ${duration}= Evaluate ${end_time} - ${start_time} 295 | ${extractions_per_second}= Evaluate (${ITERATIONS} * 4) / ${duration} 296 | 297 | Log Extracted ${ITERATIONS * 4} claims in ${duration} seconds 298 | Log Claim extraction performance: ${extractions_per_second} extractions/second 299 | 300 | Should Be True ${extractions_per_second} > 1000 301 | 302 | *** Keywords *** 303 | Log Performance Results 304 | [Arguments] ${test_name} ${operations} ${duration} ${threshold} 305 | [Documentation] Helper keyword to log and validate performance results 306 | 307 | ${ops_per_second}= Evaluate ${operations} / ${duration} 308 | 309 | Log === Performance Results: ${test_name} === 310 | Log Operations: ${operations} 311 | Log Duration: ${duration} seconds 312 | Log Performance: ${ops_per_second} ops/second 313 | Log Threshold: ${threshold} ops/second 314 | Log Status: ${'PASS' if ${ops_per_second} > ${threshold} else 'FAIL'} 315 | Log ============================================ 316 | 317 | Should Be True ${ops_per_second} > ${threshold} 318 | ... Performance below threshold: ${ops_per_second} < ${threshold} 319 | 320 | Create Performance Payload 321 | [Arguments] ${size}=10 ${user_id}=12345 322 | [Documentation] Helper to create payloads of different sizes for testing 323 | 324 | ${payload}= Create Dictionary user_id=${user_id} test=performance 325 | 326 | FOR ${i} IN RANGE ${size} 327 | Set To Dictionary ${payload} field_${i}=value_${i} 328 | END 329 | 330 | RETURN ${payload} 331 | 332 | Measure Operation Time 333 | [Arguments] ${operation_keyword} @{args} 334 | [Documentation] Helper to measure execution time of operations 335 | 336 | ${start_time}= Get Current Date result_format=epoch 337 | ${result}= Run Keyword ${operation_keyword} @{args} 338 | ${end_time}= Get Current Date result_format=epoch 339 | ${duration}= Evaluate ${end_time} - ${start_time} 340 | 341 | RETURN ${result} ${duration} 342 | 343 | Performance Benchmark Summary 344 | [Documentation] Generate performance benchmark summary 345 | 346 | Log === JWT Library Performance Benchmark Summary === 347 | Log Test Environment: Robot Framework + Python 348 | Log JWT Algorithm: HS256 (default) 349 | Log Expected Performance Thresholds: 350 | Log - Token Generation: > 100 tokens/second 351 | Log - Token Decoding: > 200 decodes/second 352 | Log - Token Verification: > 200 verifications/second 353 | Log - Large Payload Operations: > 10 operations/second 354 | Log - Claim Extraction: > 1000 extractions/second 355 | Log ================================================ 356 | -------------------------------------------------------------------------------- /tests/robot/acceptance/jwt_basic_robot_test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation Basic JWT operations test suite 3 | Library JWTLibrary 4 | Library Collections 5 | Library BuiltIn 6 | Library String 7 | 8 | *** Variables *** 9 | ${SECRET_KEY} my_test_secret_key_123 10 | ${ALGORITHM} HS256 11 | ${USER_ID} 12345 12 | ${USERNAME} testuser 13 | ${ROLE} admin 14 | ${EMAIL} test@example.com 15 | 16 | *** Test Cases *** 17 | Generate And Decode Basic JWT Token 18 | [Documentation] Test basic JWT token generation and decoding 19 | [Tags] jwt basic generation decoding 20 | 21 | # Create test payload 22 | ${payload}= Create Dictionary 23 | ... user_id=${USER_ID} 24 | ... username=${USERNAME} 25 | ... role=${ROLE} 26 | ... email=${EMAIL} 27 | 28 | # Generate JWT token 29 | ${token}= Generate JWT Token ${payload} ${SECRET_KEY} 30 | Should Not Be Empty ${token} 31 | Log Generated Token: ${token} 32 | 33 | # Verify token structure (should have 3 parts separated by dots) 34 | ${token_parts}= Split String ${token} . 35 | Length Should Be ${token_parts} 3 36 | 37 | # Decode the token payload 38 | ${decoded_payload}= Decode JWT Payload ${token} ${SECRET_KEY} 39 | Log Decoded Payload: ${decoded_payload} 40 | 41 | # Verify decoded data matches original payload 42 | Should Be Equal As Integers ${decoded_payload['user_id']} ${USER_ID} 43 | Should Be Equal ${decoded_payload['username']} ${USERNAME} 44 | Should Be Equal ${decoded_payload['role']} ${ROLE} 45 | Should Be Equal ${decoded_payload['email']} ${EMAIL} 46 | 47 | # Verify standard JWT claims are present 48 | Should Contain ${decoded_payload} iat 49 | Should Contain ${decoded_payload} exp 50 | Should Contain ${decoded_payload} nbf 51 | 52 | Generate JWT Token With Different Algorithms 53 | [Documentation] Test JWT token generation with different algorithms 54 | [Tags] jwt algorithms 55 | 56 | ${payload}= Create Dictionary user_id=999 test=algorithm 57 | 58 | # Test HS256 (default) 59 | ${token_hs256}= Generate JWT Token ${payload} ${SECRET_KEY} 60 | ${header_hs256}= Decode JWT Header ${token_hs256} 61 | Should Be Equal ${header_hs256['alg']} HS256 62 | 63 | # Test HS384 64 | ${token_hs384}= Generate JWT Token ${payload} ${SECRET_KEY} algorithm=HS384 65 | ${header_hs384}= Decode JWT Header ${token_hs384} 66 | Should Be Equal ${header_hs384['alg']} HS384 67 | 68 | # Test HS512 69 | ${token_hs512}= Generate JWT Token ${payload} ${SECRET_KEY} algorithm=HS512 70 | ${header_hs512}= Decode JWT Header ${token_hs512} 71 | Should Be Equal ${header_hs512['alg']} HS512 72 | 73 | # Verify all tokens can be decoded with correct algorithm 74 | ${decoded_hs256}= Decode JWT Payload ${token_hs256} ${SECRET_KEY} algorithm=HS256 75 | ${decoded_hs384}= Decode JWT Payload ${token_hs384} ${SECRET_KEY} algorithm=HS384 76 | ${decoded_hs512}= Decode JWT Payload ${token_hs512} ${SECRET_KEY} algorithm=HS512 77 | 78 | Should Be Equal As Integers ${decoded_hs256['user_id']} 999 79 | Should Be Equal As Integers ${decoded_hs384['user_id']} 999 80 | Should Be Equal As Integers ${decoded_hs512['user_id']} 999 81 | 82 | Generate JWT Token With Custom Expiration 83 | [Documentation] Test JWT token generation with custom expiration time 84 | [Tags] jwt expiration 85 | 86 | ${payload}= Create Dictionary user_id=777 role=temp_user 87 | 88 | # Generate token with 1 hour expiration 89 | ${token_1h}= Generate JWT Token ${payload} ${SECRET_KEY} expiration_hours=1 90 | 91 | # Generate token with 24 hour expiration (default) 92 | ${token_24h}= Generate JWT Token ${payload} ${SECRET_KEY} 93 | 94 | # Check expiration details 95 | ${exp_info_1h}= Check JWT Expiration ${token_1h} 96 | ${exp_info_24h}= Check JWT Expiration ${token_24h} 97 | 98 | # Both tokens should not be expired 99 | Should Be Equal ${exp_info_1h['is_expired']} ${False} 100 | Should Be Equal ${exp_info_24h['is_expired']} ${False} 101 | 102 | # 24h token should expire later than 1h token 103 | Should Be True ${exp_info_24h['time_until_expiry']} > ${exp_info_1h['time_until_expiry']} 104 | 105 | Generate JWT Token Without Expiration 106 | [Documentation] Test JWT token generation without expiration claim 107 | [Tags] jwt no-expiration 108 | 109 | ${payload}= Create Dictionary user_id=888 permanent=true 110 | 111 | # Generate token without expiration 112 | ${token}= Generate JWT Token Without Expiration ${payload} ${SECRET_KEY} 113 | 114 | # Check expiration info 115 | ${exp_info}= Check JWT Expiration ${token} 116 | Should Be Equal ${exp_info['has_expiration']} ${False} 117 | Should Be Equal ${exp_info['is_expired']} ${False} 118 | Should Be Equal ${exp_info['expires_at']} ${None} 119 | 120 | JWT Token Verification 121 | [Documentation] Test JWT token verification functionality 122 | [Tags] jwt verification 123 | 124 | ${payload}= Create Dictionary user_id=555 role=user 125 | ${token}= Generate JWT Token ${payload} ${SECRET_KEY} 126 | 127 | # Verify with correct secret key 128 | ${is_valid_correct}= Verify JWT Token ${token} ${SECRET_KEY} 129 | Should Be True ${is_valid_correct} 130 | 131 | # Verify with wrong secret key 132 | ${is_valid_wrong}= Verify JWT Token ${token} wrong_secret_key 133 | Should Be Equal ${is_valid_wrong} ${False} 134 | 135 | # Verify invalid token format 136 | ${is_valid_invalid}= Verify JWT Token invalid.token.format ${SECRET_KEY} 137 | Should Be Equal ${is_valid_invalid} ${False} 138 | 139 | Extract Specific JWT Claims 140 | [Documentation] Test extracting specific claims from JWT tokens 141 | [Tags] jwt claims 142 | 143 | ${payload}= Create Dictionary 144 | ... user_id=123 145 | ... username=claimtest 146 | ... role=admin 147 | ... department=engineering 148 | ... clearance_level=5 149 | 150 | ${token}= Generate JWT Token ${payload} ${SECRET_KEY} 151 | 152 | # Extract individual claims 153 | ${user_id}= Get JWT Claim ${token} user_id 154 | ${username}= Get JWT Claim ${token} username 155 | ${role}= Get JWT Claim ${token} role 156 | ${department}= Get JWT Claim ${token} department 157 | ${clearance}= Get JWT Claim ${token} clearance_level 158 | 159 | # Verify extracted claims 160 | Should Be Equal As Integers ${user_id} 123 161 | Should Be Equal ${username} claimtest 162 | Should Be Equal ${role} admin 163 | Should Be Equal ${department} engineering 164 | Should Be Equal As Integers ${clearance} 5 165 | 166 | # Extract multiple claims at once 167 | ${claim_names}= Create List user_id role department 168 | ${multiple_claims}= Get Multiple JWT Claims ${token} ${claim_names} 169 | 170 | Should Be Equal As Integers ${multiple_claims['user_id']} 123 171 | Should Be Equal ${multiple_claims['role']} admin 172 | Should Be Equal ${multiple_claims['department']} engineering 173 | 174 | JWT Claims Validation 175 | [Documentation] Test JWT claims validation functionality 176 | [Tags] jwt validation 177 | 178 | ${payload}= Create Dictionary 179 | ... user_id=456 180 | ... role=manager 181 | ... email=manager@company.com 182 | ... active=true 183 | 184 | ${token}= Generate JWT Token ${payload} ${SECRET_KEY} 185 | 186 | # Test valid claims validation 187 | ${expected_valid}= Create Dictionary 188 | ... user_id=456 189 | ... role=manager 190 | ... active=true 191 | 192 | ${validation_result}= Validate JWT Claims ${token} ${expected_valid} 193 | Should Be True ${validation_result} 194 | 195 | # Test invalid claims validation 196 | ${expected_invalid}= Create Dictionary 197 | ... user_id=999 198 | ... role=admin 199 | 200 | ${validation_result_invalid}= Validate JWT Claims ${token} ${expected_invalid} 201 | Should Be Equal ${validation_result_invalid} ${False} 202 | 203 | JWT Token Structure Validation 204 | [Documentation] Test JWT token structure validation 205 | [Tags] jwt structure 206 | 207 | ${payload}= Create Dictionary user_id=321 test=structure 208 | ${token}= Generate JWT Token ${payload} ${SECRET_KEY} 209 | 210 | # Validate structure of valid token 211 | ${structure_info}= Validate JWT Structure ${token} 212 | Should Be True ${structure_info['is_valid_structure']} 213 | Should Be True ${structure_info['has_three_parts']} 214 | Should Be True ${structure_info['has_valid_header']} 215 | Should Be True ${structure_info['has_valid_payload']} 216 | 217 | # Check header info 218 | Should Be Equal ${structure_info['header_info']['algorithm']} HS256 219 | Should Be Equal ${structure_info['header_info']['type']} JWT 220 | 221 | # Check payload info 222 | Should Be True ${structure_info['payload_info']['claims_count']} > 0 223 | Should Be True ${structure_info['payload_info']['has_expiration']} 224 | Should Be True ${structure_info['payload_info']['has_issued_at']} 225 | 226 | JWT Utility Functions 227 | [Documentation] Test JWT utility functions 228 | [Tags] jwt utilities 229 | 230 | # Test payload creation 231 | ${created_payload}= Create JWT Payload 232 | ... user_id=999 233 | ... service=api 234 | ... version=1.0 235 | 236 | Should Be Equal As Integers ${created_payload['user_id']} 999 237 | Should Be Equal ${created_payload['service']} api 238 | Should Be Equal ${created_payload['version']} 1.0 239 | 240 | # Test timestamp generation 241 | ${current_ts}= Generate Current Timestamp 242 | ${future_ts}= Generate Future Timestamp hours=1 243 | 244 | Should Be True ${future_ts} > ${current_ts} 245 | 246 | # Test timestamp conversion 247 | ${datetime_str}= Convert Timestamp To Datetime ${current_ts} 248 | Should Contain ${datetime_str} T # ISO format contains T 249 | 250 | # Generate token and get info 251 | ${token}= Generate JWT Token ${created_payload} ${SECRET_KEY} 252 | ${token_info}= Get JWT Token Info ${token} 253 | 254 | Should Be Equal ${token_info['algorithm']} HS256 255 | Should Be Equal ${token_info['type']} JWT 256 | Should Be True ${token_info['claims_count']} > 0 257 | 258 | JWT Token Comparison 259 | [Documentation] Test JWT token comparison functionality 260 | [Tags] jwt comparison 261 | 262 | # Create two different payloads 263 | ${payload1}= Create Dictionary user_id=111 role=user team=alpha 264 | ${payload2}= Create Dictionary user_id=222 role=admin team=beta 265 | 266 | ${token1}= Generate JWT Token ${payload1} ${SECRET_KEY} 267 | ${token2}= Generate JWT Token ${payload2} ${SECRET_KEY} 268 | 269 | # Compare different tokens 270 | ${comparison}= Compare JWT Tokens ${token1} ${token2} 271 | Should Be Equal ${comparison['are_identical']} ${False} 272 | Should Be True ${comparison['payload_differences_count']} > 0 273 | 274 | # Verify specific differences 275 | Should Contain ${comparison['payload_differences']} user_id 276 | Should Contain ${comparison['payload_differences']} role 277 | Should Contain ${comparison['payload_differences']} team 278 | 279 | Decode JWT Without Verification 280 | [Documentation] Test decoding JWT without signature verification 281 | [Tags] jwt unsafe-decoding 282 | 283 | ${payload}= Create Dictionary user_id=666 unsafe=True 284 | ${token}= Generate JWT Token ${payload} ${SECRET_KEY} 285 | 286 | # Decode without verification (unsafe) 287 | ${decoded_unsafe}= Decode JWT Payload ${token} verify_signature=False 288 | Should Be Equal As Integers ${decoded_unsafe['user_id']} 666 289 | Should Be Equal ${decoded_unsafe['unsafe']} True 290 | 291 | # Should work even with wrong or no secret key 292 | ${decoded_no_secret}= Decode JWT Payload Unsafe ${token} 293 | Should Be Equal As Integers ${decoded_no_secret['user_id']} 666 294 | 295 | Generate JWT Token With Claims Keyword Arguments 296 | [Documentation] Test generating tokens using keyword arguments 297 | [Tags] jwt keyword-args 298 | 299 | # Generate token using keyword arguments 300 | ${token}= Generate JWT Token With Claims ${SECRET_KEY} 301 | ... user_id=789 302 | ... name=John Doe 303 | ... role=developer 304 | ... projects=["project1", "project2"] 305 | 306 | # Verify token content 307 | ${decoded}= Decode JWT Payload ${token} ${SECRET_KEY} 308 | Should Be Equal As Integers ${decoded['user_id']} 789 309 | Should Be Equal ${decoded['name']} John Doe 310 | Should Be Equal ${decoded['role']} developer 311 | Should Be Equal ${decoded['projects']} ["project1", "project2"] 312 | 313 | *** Keywords *** 314 | Log JWT Token Details 315 | [Arguments] ${token} ${secret_key}=${SECRET_KEY} 316 | [Documentation] Helper keyword to log comprehensive JWT token details 317 | 318 | ${header}= Decode JWT Header ${token} 319 | ${payload}= Decode JWT Payload ${token} ${secret_key} 320 | ${token_info}= Get JWT Token Info ${token} 321 | ${exp_info}= Check JWT Expiration ${token} 322 | 323 | Log === JWT Token Details === 324 | Log Header: ${header} 325 | Log Payload: ${payload} 326 | Log Token Info: ${token_info} 327 | Log Expiration Info: ${exp_info} 328 | Log ======================== 329 | 330 | Create Test User Payload 331 | [Arguments] ${user_id} ${username} ${role}=user 332 | [Documentation] Helper keyword to create standardized user payload 333 | 334 | ${payload}= Create Dictionary 335 | ... user_id=${user_id} 336 | ... username=${username} 337 | ... role=${role} 338 | ... created_at=${EMPTY} 339 | 340 | ${current_time}= Generate Current Timestamp 341 | Set To Dictionary ${payload} created_at=${current_time} 342 | 343 | RETURN ${payload} 344 | 345 | Verify JWT Token Contains Standard Claims 346 | [Arguments] ${token} ${secret_key}=${SECRET_KEY} 347 | [Documentation] Helper keyword to verify standard JWT claims 348 | 349 | ${payload}= Decode JWT Payload ${token} ${secret_key} 350 | 351 | # Verify standard claims exist 352 | Should Contain ${payload} iat # Issued At 353 | Should Contain ${payload} exp # Expiration 354 | Should Contain ${payload} nbf # Not Before 355 | 356 | # Verify claim types 357 | Should Be True isinstance($payload['iat'], (int, float)) 358 | Should Be True isinstance($payload['exp'], (int, float)) 359 | Should Be True isinstance($payload['nbf'], (int, float)) 360 | 361 | # Verify logical order: nbf <= iat < exp 362 | Should Be True ${payload['nbf']} <= ${payload['iat']} 363 | Should Be True ${payload['iat']} < ${payload['exp']} 364 | -------------------------------------------------------------------------------- /src/JWTLibrary/keywords/token_validation.py: -------------------------------------------------------------------------------- 1 | """Token validation keywords for JWT Library.""" 2 | 3 | from datetime import datetime, timezone 4 | from typing import Any, Dict, Union 5 | 6 | import jwt 7 | from robot.api import logger 8 | from robot.api.deco import keyword 9 | 10 | from ..constants import DEFAULT_ALGORITHM 11 | from ..exceptions import JWTTokenValidationError 12 | from ..utils import parse_jwt_timestamp, validate_algorithm 13 | 14 | 15 | class TokenValidationKeywords: 16 | """Keywords for JWT token validation.""" 17 | 18 | @keyword("Verify JWT Token") 19 | def verify_jwt_token( 20 | self, token: str, secret_key: str, algorithm: str = None 21 | ) -> bool: 22 | """ 23 | Verifies if a JWT token is valid and not expired. 24 | Arguments: 25 | - ``token``: JWT token string to verify 26 | - ``secret_key``: Secret key used for verification 27 | - ``algorithm``: JWT algorithm (default: HS256) 28 | Returns: 29 | - True if token is valid, False otherwise 30 | Examples: 31 | | ${is_valid}= Verify JWT Token ${token} my_secret_key 32 | | Should Be True ${is_valid} 33 | """ 34 | try: 35 | algorithm = algorithm or DEFAULT_ALGORITHM 36 | validate_algorithm(algorithm) 37 | 38 | jwt.decode(token, secret_key, algorithms=[algorithm]) 39 | logger.info("JWT token verification successful") 40 | return True 41 | except ( 42 | jwt.ExpiredSignatureError, 43 | jwt.InvalidSignatureError, 44 | jwt.InvalidTokenError, 45 | ): 46 | logger.info("JWT token verification failed") 47 | return False 48 | except Exception as e: 49 | logger.error(f"JWT token verification error: {str(e)}") 50 | return False 51 | 52 | @keyword("Check JWT Expiration") 53 | def check_jwt_expiration( 54 | self, token: str, secret_key: str = None 55 | ) -> Dict[str, Any]: 56 | """ 57 | Checks JWT token expiration details. 58 | Arguments: 59 | - ``token``: JWT token string 60 | - ``secret_key``: Secret key for verification (optional) 61 | Returns: 62 | - Dictionary with expiration information: 63 | - expires_at: Expiration timestamp 64 | - is_expired: Boolean indicating if token is expired 65 | - time_until_expiry: Seconds until expiration (negative if expired) 66 | Examples: 67 | | ${exp_info}= Check JWT Expiration ${token} 68 | | Should Be True ${exp_info['is_expired']} == False 69 | """ 70 | try: 71 | # Decode without verification to get expiration info 72 | payload = jwt.decode(token, options={"verify_signature": False}) 73 | 74 | if "exp" not in payload: 75 | return { 76 | "expires_at": None, 77 | "is_expired": False, 78 | "time_until_expiry": None, 79 | "has_expiration": False, 80 | } 81 | exp_timestamp = payload["exp"] 82 | exp_datetime = parse_jwt_timestamp(exp_timestamp) 83 | current_time = datetime.now(tz=timezone.utc) 84 | # Ensure both datetimes are timezone-aware for comparison 85 | if exp_datetime.tzinfo is None: 86 | exp_datetime = exp_datetime.replace(tzinfo=timezone.utc) 87 | is_expired = current_time > exp_datetime 88 | time_until_expiry = (exp_datetime - current_time).total_seconds() 89 | expiration_info = { 90 | "expires_at": exp_datetime.isoformat(), 91 | "is_expired": is_expired, 92 | "time_until_expiry": time_until_expiry, 93 | "has_expiration": True, 94 | } 95 | logger.info( 96 | f"Token expiration check: expires in {time_until_expiry} seconds" 97 | ) 98 | return expiration_info 99 | except Exception as e: 100 | error_msg = f"JWT expiration check failed: {str(e)}" 101 | logger.error(error_msg) 102 | raise JWTTokenValidationError(error_msg) 103 | 104 | @keyword("Validate JWT Claims") 105 | def validate_jwt_claims( 106 | self, 107 | token: str, 108 | expected_claims: Dict[str, Any], 109 | secret_key: str = None, 110 | verify_signature: bool = False, 111 | ) -> bool: 112 | """ 113 | Validates that JWT token contains expected claims with correct values. 114 | Arguments: 115 | - ``token``: JWT token string 116 | - ``expected_claims``: Dictionary of expected claim key-value pairs 117 | - ``secret_key``: Secret key (required if verify_signature is True) 118 | - ``verify_signature``: Whether to verify token signature 119 | Returns: 120 | - True if all expected claims match, False otherwise 121 | 122 | Examples: 123 | | ${expected}= Create Dictionary user_id=123 role=admin 124 | | ${valid}= Validate JWT Claims ${token} ${expected} 125 | | Should Be True ${valid} 126 | """ 127 | try: 128 | from .token_decoding import TokenDecodingKeywords 129 | 130 | decoder = TokenDecodingKeywords() 131 | 132 | # Get payload 133 | payload = decoder.decode_jwt_payload( 134 | token, secret_key, verify_signature=verify_signature 135 | ) 136 | 137 | # Check each expected claim 138 | mismatched_claims = [] 139 | missing_claims = [] 140 | for claim_name, expected_value in expected_claims.items(): 141 | if claim_name not in payload: 142 | missing_claims.append(claim_name) 143 | elif payload[claim_name] != expected_value: 144 | mismatched_claims.append( 145 | { 146 | "claim": claim_name, 147 | "expected": expected_value, 148 | "actual": payload[claim_name], 149 | } 150 | ) 151 | 152 | if missing_claims or mismatched_claims: 153 | logger.info( 154 | f"Claim validation failed - Missing: {missing_claims}, Mismatched: {mismatched_claims}" 155 | ) 156 | return False 157 | 158 | logger.info(f"All {len(expected_claims)} claims validated successfully") 159 | return True 160 | 161 | except Exception as e: 162 | logger.error(f"JWT claims validation error: {str(e)}") 163 | return False 164 | 165 | @keyword("Check JWT Algorithm") 166 | def check_jwt_algorithm(self, token: str, expected_algorithm: str) -> bool: 167 | """ 168 | Checks if JWT token uses expected algorithm. 169 | Arguments: 170 | - ``token``: JWT token string 171 | - ``expected_algorithm``: Expected algorithm (e.g., HS256, RS256) 172 | Returns: 173 | - True if algorithm matches, False otherwise 174 | Examples: 175 | | ${correct_alg}= Check JWT Algorithm ${token} HS256 176 | | Should Be True ${correct_alg} 177 | """ 178 | try: 179 | from .token_decoding import TokenDecodingKeywords 180 | 181 | decoder = TokenDecodingKeywords() 182 | 183 | header = decoder.decode_jwt_header(token) 184 | actual_algorithm = header.get("alg") 185 | 186 | if actual_algorithm == expected_algorithm: 187 | logger.info( 188 | f"JWT algorithm verification successful: {actual_algorithm}" 189 | ) 190 | return True 191 | else: 192 | logger.info( 193 | f"JWT algorithm mismatch - Expected: {expected_algorithm}, Actual: {actual_algorithm}" 194 | ) 195 | return False 196 | 197 | except Exception as e: 198 | logger.error(f"JWT algorithm check error: {str(e)}") 199 | return False 200 | 201 | @keyword("Validate JWT Structure") 202 | def validate_jwt_structure(self, token: str) -> Dict[str, Any]: 203 | """ 204 | Validates JWT token structure and returns detailed information. 205 | Arguments: 206 | - ``token``: JWT token string 207 | Returns: 208 | - Dictionary with structure validation results 209 | Examples: 210 | | ${structure}= Validate JWT Structure ${token} 211 | | Should Be True ${structure['is_valid_structure']} 212 | """ 213 | try: 214 | validation_result = { 215 | "is_valid_structure": False, 216 | "has_three_parts": False, 217 | "has_valid_header": False, 218 | "has_valid_payload": False, 219 | "header_info": None, 220 | "payload_info": None, 221 | "errors": [], 222 | } 223 | # Check if token has three parts 224 | parts = token.split(".") 225 | if len(parts) == 3: 226 | validation_result["has_three_parts"] = True 227 | else: 228 | validation_result["errors"].append( 229 | f"Token has {len(parts)} parts, expected 3" 230 | ) 231 | return validation_result 232 | 233 | try: 234 | from .token_decoding import TokenDecodingKeywords 235 | 236 | decoder = TokenDecodingKeywords() 237 | # Validate header 238 | header = decoder.decode_jwt_header(token) 239 | validation_result["has_valid_header"] = True 240 | validation_result["header_info"] = { 241 | "algorithm": header.get("alg"), 242 | "type": header.get("typ"), 243 | "keys": list(header.keys()), 244 | } 245 | 246 | except Exception as e: 247 | validation_result["errors"].append(f"Invalid header: {str(e)}") 248 | 249 | try: 250 | # Validate payload 251 | payload = decoder.decode_jwt_payload_unsafe(token) 252 | validation_result["has_valid_payload"] = True 253 | validation_result["payload_info"] = { 254 | "claims_count": len(payload), 255 | "has_expiration": "exp" in payload, 256 | "has_issued_at": "iat" in payload, 257 | "claims": list(payload.keys()), 258 | } 259 | 260 | except Exception as e: 261 | validation_result["errors"].append(f"Invalid payload: {str(e)}") 262 | 263 | # Overall validation 264 | validation_result["is_valid_structure"] = ( 265 | validation_result["has_three_parts"] 266 | and validation_result["has_valid_header"] 267 | and validation_result["has_valid_payload"] 268 | ) 269 | 270 | logger.info( 271 | f"JWT structure validation: {'Valid' if validation_result['is_valid_structure'] else 'Invalid'}" 272 | ) 273 | return validation_result 274 | 275 | except Exception as e: 276 | error_msg = f"JWT structure validation failed: {str(e)}" 277 | logger.error(error_msg) 278 | raise JWTTokenValidationError(error_msg) 279 | 280 | @keyword("Check JWT Not Before") 281 | def check_jwt_not_before(self, token: str) -> Dict[str, Any]: 282 | """ 283 | Checks JWT token 'not before' (nbf) claim. 284 | Arguments: 285 | - ``token``: JWT token string 286 | Returns: 287 | - Dictionary with 'not before' information 288 | Examples: 289 | | ${nbf_info}= Check JWT Not Before ${token} 290 | | Should Be True ${nbf_info['is_active']} 291 | """ 292 | try: 293 | payload = jwt.decode(token, options={"verify_signature": False}) 294 | if "nbf" not in payload: 295 | return { 296 | "not_before": None, 297 | "is_active": True, 298 | "time_until_active": 0, 299 | "has_not_before": False, 300 | } 301 | nbf_timestamp = payload["nbf"] 302 | nbf_datetime = parse_jwt_timestamp(nbf_timestamp) 303 | current_time = datetime.now() 304 | is_active = current_time >= nbf_datetime 305 | time_until_active = (nbf_datetime - current_time).total_seconds() 306 | nbf_info = { 307 | "not_before": nbf_datetime.isoformat(), 308 | "is_active": is_active, 309 | "time_until_active": max(0, time_until_active), 310 | "has_not_before": True, 311 | } 312 | logger.info( 313 | f"JWT not before check: {'Active' if is_active else f'Active in {time_until_active} seconds'}" 314 | ) 315 | return nbf_info 316 | except Exception as e: 317 | error_msg = f"JWT not before check failed: {str(e)}" 318 | logger.error(error_msg) 319 | raise JWTTokenValidationError(error_msg) 320 | 321 | @keyword("Validate JWT Audience") 322 | def validate_jwt_audience( 323 | self, 324 | token: str, 325 | expected_audience: Union[str, list], 326 | secret_key: str = None, 327 | verify_signature: bool = False, 328 | ) -> bool: 329 | """ 330 | Validates JWT token audience claim. 331 | Arguments: 332 | - ``token``: JWT token string 333 | - ``expected_audience``: Expected audience (string or list) 334 | - ``secret_key``: Secret key (required if verify_signature is True) 335 | - ``verify_signature``: Whether to verify token signature 336 | Returns: 337 | - True if audience matches, False otherwise 338 | Examples: 339 | | ${valid_aud}= Validate JWT Audience ${token} my-api 340 | | ${valid_aud}= Validate JWT Audience ${token} ["api1", "api2"] 341 | """ 342 | try: 343 | from .token_decoding import TokenDecodingKeywords 344 | 345 | decoder = TokenDecodingKeywords() 346 | payload = decoder.decode_jwt_payload( 347 | token, secret_key, verify_signature=verify_signature 348 | ) 349 | if "aud" not in payload: 350 | logger.info("Token does not contain audience claim") 351 | return False 352 | actual_audience = payload["aud"] 353 | # Handle different audience formats 354 | if isinstance(expected_audience, str): 355 | if isinstance(actual_audience, str): 356 | is_valid = actual_audience == expected_audience 357 | elif isinstance(actual_audience, list): 358 | is_valid = expected_audience in actual_audience 359 | else: 360 | is_valid = False 361 | elif isinstance(expected_audience, list): 362 | if isinstance(actual_audience, str): 363 | is_valid = actual_audience in expected_audience 364 | elif isinstance(actual_audience, list): 365 | is_valid = any(aud in expected_audience for aud in actual_audience) 366 | else: 367 | is_valid = False 368 | else: 369 | is_valid = False 370 | logger.info( 371 | f"JWT audience validation: " f"{'Valid' if is_valid else 'Invalid'}" 372 | ) 373 | return is_valid 374 | except Exception as e: 375 | logger.error(f"JWT audience validation error: {str(e)}") 376 | return False 377 | -------------------------------------------------------------------------------- /tests/unit/test_jwt_library.py: -------------------------------------------------------------------------------- 1 | """Unit tests for JWT Library main functionality.""" 2 | 3 | import time 4 | from datetime import datetime, timedelta, timezone 5 | 6 | import jwt 7 | import pytest 8 | 9 | from JWTLibrary.exceptions import ( 10 | JWTClaimNotFoundError, 11 | JWTExpiredTokenError, 12 | JWTInvalidSignatureError, 13 | JWTTokenDecodingError, 14 | JWTTokenGenerationError, 15 | ) 16 | from JWTLibrary.jwt_library import JWTLibrary 17 | 18 | 19 | class TestJWTLibrary: 20 | """Test cases for JWT Library main functionality.""" 21 | 22 | def setup_method(self): 23 | """Setup test fixtures.""" 24 | self.jwt_lib = JWTLibrary() 25 | self.secret_key = "test_secret_key_123" 26 | self.test_payload = { 27 | "user_id": 123, 28 | "username": "testuser", 29 | "role": "admin", 30 | "email": "test@example.com", 31 | } 32 | 33 | def test_library_initialization(self): 34 | """Test library initialization.""" 35 | assert self.jwt_lib.library_name == "JWTLibrary" 36 | assert hasattr(self.jwt_lib, "library_version") 37 | assert self.jwt_lib.ROBOT_LIBRARY_SCOPE == "GLOBAL" 38 | 39 | def test_generate_basic_jwt_token(self): 40 | """Test basic JWT token generation.""" 41 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 42 | assert isinstance(token, str) 43 | assert len(token.split(".")) == 3 # JWT has 3 parts 44 | # Verify token can be decoded 45 | decoded = jwt.decode(token, self.secret_key, algorithms=["HS256"]) 46 | assert decoded["user_id"] == 123 47 | assert decoded["username"] == "testuser" 48 | assert decoded["role"] == "admin" 49 | 50 | def test_generate_jwt_token_with_custom_algorithm(self): 51 | """Test JWT token generation with custom algorithm.""" 52 | token = self.jwt_lib.generate_jwt_token( 53 | self.test_payload, self.secret_key, algorithm="HS512" 54 | ) 55 | # Verify token header contains correct algorithm 56 | header = jwt.get_unverified_header(token) 57 | assert header["alg"] == "HS512" 58 | # Verify token can be decoded with correct algorithm 59 | decoded = jwt.decode(token, self.secret_key, algorithms=["HS512"]) 60 | assert decoded["user_id"] == 123 61 | 62 | def test_generate_jwt_token_with_custom_expiration(self): 63 | """Test JWT token generation with custom expiration.""" 64 | token = self.jwt_lib.generate_jwt_token( 65 | self.test_payload, self.secret_key, expiration_hours=1 66 | ) 67 | decoded = jwt.decode(token, self.secret_key, algorithms=["HS256"]) 68 | # Check expiration is approximately 1 hour from now 69 | exp_time = datetime.fromtimestamp(decoded["exp"]) 70 | expected_exp = datetime.now() + timedelta(hours=1) 71 | time_diff = abs((exp_time - expected_exp).total_seconds()) 72 | assert time_diff < 60 # Within 1 minute tolerance 73 | 74 | def test_generate_jwt_token_invalid_algorithm(self): 75 | """Test JWT token generation with invalid algorithm.""" 76 | with pytest.raises(JWTTokenGenerationError): 77 | self.jwt_lib.generate_jwt_token( 78 | self.test_payload, self.secret_key, algorithm="INVALID" 79 | ) 80 | 81 | def test_decode_jwt_payload_valid_token(self): 82 | """Test decoding valid JWT payload.""" 83 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 84 | decoded = self.jwt_lib.decode_jwt_payload(token, self.secret_key) 85 | assert decoded["user_id"] == 123 86 | assert decoded["username"] == "testuser" 87 | assert decoded["role"] == "admin" 88 | 89 | def test_decode_jwt_payload_without_verification(self): 90 | """Test decoding JWT payload without signature verification.""" 91 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 92 | decoded = self.jwt_lib.decode_jwt_payload(token, verify_signature=False) 93 | assert decoded["user_id"] == 123 94 | assert decoded["username"] == "testuser" 95 | 96 | def test_decode_jwt_payload_wrong_secret(self): 97 | """Test decoding JWT payload with wrong secret key.""" 98 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 99 | with pytest.raises(JWTInvalidSignatureError): 100 | self.jwt_lib.decode_jwt_payload(token, "wrong_secret") 101 | 102 | def test_decode_jwt_payload_expired_token(self): 103 | """Test decoding expired JWT token.""" 104 | # Create token that expires immediately 105 | past_time = datetime.now(tz=timezone.utc) - timedelta(hours=1) 106 | token = self.jwt_lib.generate_jwt_token_with_custom_expiration( 107 | self.test_payload, self.secret_key, past_time 108 | ) 109 | with pytest.raises(JWTExpiredTokenError): 110 | self.jwt_lib.decode_jwt_payload(token, self.secret_key) 111 | 112 | def test_decode_jwt_payload_invalid_token(self): 113 | """Test decoding invalid JWT token.""" 114 | with pytest.raises(JWTTokenDecodingError): 115 | self.jwt_lib.decode_jwt_payload("invalid.token.here", self.secret_key) 116 | 117 | def test_decode_jwt_header(self): 118 | """Test decoding JWT header.""" 119 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 120 | header = self.jwt_lib.decode_jwt_header(token) 121 | assert header["alg"] == "HS256" 122 | assert header["typ"] == "JWT" 123 | 124 | def test_verify_jwt_token_valid(self): 125 | """Test verifying valid JWT token.""" 126 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 127 | is_valid = self.jwt_lib.verify_jwt_token(token, self.secret_key) 128 | assert is_valid is True 129 | 130 | def test_verify_jwt_token_invalid(self): 131 | """Test verifying invalid JWT token.""" 132 | is_valid = self.jwt_lib.verify_jwt_token("invalid.token.here", self.secret_key) 133 | assert is_valid is False 134 | 135 | def test_verify_jwt_token_wrong_secret(self): 136 | """Test verifying JWT token with wrong secret.""" 137 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 138 | is_valid = self.jwt_lib.verify_jwt_token(token, "wrong_secret") 139 | assert is_valid is False 140 | 141 | def test_get_jwt_claim_existing(self): 142 | """Test getting existing JWT claim.""" 143 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 144 | user_id = self.jwt_lib.get_jwt_claim(token, "user_id") 145 | assert user_id == 123 146 | 147 | def test_get_jwt_claim_missing(self): 148 | """Test getting non-existing JWT claim.""" 149 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 150 | with pytest.raises(JWTClaimNotFoundError): 151 | self.jwt_lib.get_jwt_claim(token, "non_existing_claim") 152 | 153 | def test_get_jwt_claim_with_verification(self): 154 | """Test getting JWT claim with signature verification.""" 155 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 156 | role = self.jwt_lib.get_jwt_claim( 157 | token, "role", secret_key=self.secret_key, verify_signature=True 158 | ) 159 | assert role == "admin" 160 | 161 | def test_check_jwt_expiration_not_expired(self): 162 | """Test checking expiration of non-expired token.""" 163 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 164 | exp_info = self.jwt_lib.check_jwt_expiration(token) 165 | assert exp_info["is_expired"] is False 166 | assert exp_info["time_until_expiry"] > 0 167 | assert exp_info["has_expiration"] is True 168 | 169 | def test_check_jwt_expiration_expired(self): 170 | """Test checking expiration of expired token.""" 171 | past_time = datetime.now(tz=timezone.utc) - timedelta(days=1) 172 | token = self.jwt_lib.generate_jwt_token_with_custom_expiration( 173 | self.test_payload, self.secret_key, past_time 174 | ) 175 | exp_info = self.jwt_lib.check_jwt_expiration(token) 176 | assert exp_info["is_expired"] is True 177 | assert exp_info["time_until_expiry"] < 0 178 | 179 | def test_check_jwt_expiration_no_exp_claim(self): 180 | """Test checking expiration of token without exp claim.""" 181 | token = self.jwt_lib.generate_jwt_token_without_expiration( 182 | self.test_payload, self.secret_key 183 | ) 184 | exp_info = self.jwt_lib.check_jwt_expiration(token) 185 | assert exp_info["has_expiration"] is False 186 | assert exp_info["is_expired"] is False 187 | 188 | def test_create_jwt_payload(self): 189 | """Test creating JWT payload from keyword arguments.""" 190 | payload = self.jwt_lib.create_jwt_payload( 191 | user_id=456, role="user", email="user@test.com" 192 | ) 193 | assert payload["user_id"] == 456 194 | assert payload["role"] == "user" 195 | assert payload["email"] == "user@test.com" 196 | 197 | def test_validate_jwt_claims_valid(self): 198 | """Test validating JWT claims with correct values.""" 199 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 200 | expected_claims = {"user_id": 123, "role": "admin"} 201 | is_valid = self.jwt_lib.validate_jwt_claims(token, expected_claims) 202 | assert is_valid is True 203 | 204 | def test_validate_jwt_claims_invalid(self): 205 | """Test validating JWT claims with incorrect values.""" 206 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 207 | expected_claims = {"user_id": 456, "role": "user"} # Wrong values 208 | is_valid = self.jwt_lib.validate_jwt_claims(token, expected_claims) 209 | assert is_valid is False 210 | 211 | def test_generate_jwt_token_with_claims(self): 212 | """Test generating JWT token using keyword arguments.""" 213 | token = self.jwt_lib.generate_jwt_token_with_claims( 214 | self.secret_key, user_id=789, role="moderator", email="mod@test.com" 215 | ) 216 | decoded = self.jwt_lib.decode_jwt_payload(token, self.secret_key) 217 | assert decoded["user_id"] == 789 218 | assert decoded["role"] == "moderator" 219 | assert decoded["email"] == "mod@test.com" 220 | 221 | def test_compare_jwt_tokens_identical(self): 222 | """Test comparing identical JWT tokens.""" 223 | payload = {"user_id": 123, "role": "admin"} 224 | token1 = self.jwt_lib.generate_jwt_token(payload, self.secret_key) 225 | time.sleep(1) # Ensure iat is different 226 | token2 = self.jwt_lib.generate_jwt_token(payload, self.secret_key) 227 | 228 | # Note: Tokens won't be identical due to iat timestamp 229 | comparison = self.jwt_lib.compare_jwt_tokens(token1, token2) 230 | assert comparison["are_identical"] is False # Due to different iat timestamps 231 | 232 | def test_compare_jwt_tokens_different(self): 233 | """Test comparing different JWT tokens.""" 234 | payload1 = {"user_id": 123, "role": "admin"} 235 | payload2 = {"user_id": 456, "role": "user"} 236 | token1 = self.jwt_lib.generate_jwt_token(payload1, self.secret_key) 237 | token2 = self.jwt_lib.generate_jwt_token(payload2, self.secret_key) 238 | comparison = self.jwt_lib.compare_jwt_tokens(token1, token2) 239 | assert comparison["are_identical"] is False 240 | assert comparison["payload_differences_count"] > 0 241 | 242 | def test_get_jwt_token_info(self): 243 | """Test getting comprehensive JWT token information.""" 244 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 245 | info = self.jwt_lib.get_jwt_token_info(token) 246 | assert info["algorithm"] == "HS256" 247 | assert info["type"] == "JWT" 248 | assert info["claims_count"] > 0 249 | assert "issued_at" in info 250 | assert "expires_at" in info 251 | 252 | def test_extract_jwt_timestamps(self): 253 | """Test extracting JWT timestamp claims.""" 254 | token = self.jwt_lib.generate_jwt_token(self.test_payload, self.secret_key) 255 | timestamps = self.jwt_lib.extract_jwt_timestamps(token) 256 | assert "iat" in timestamps 257 | assert "exp" in timestamps 258 | assert "age_seconds" in timestamps 259 | assert "expires_in_seconds" in timestamps 260 | 261 | def test_generate_current_timestamp(self): 262 | """Test generating current timestamp.""" 263 | timestamp = self.jwt_lib.generate_current_timestamp() 264 | assert isinstance(timestamp, int) 265 | current_time = datetime.now(tz=timezone.utc).timestamp() 266 | assert abs(timestamp - current_time) < 2 # Within 2 seconds 267 | 268 | def test_generate_future_timestamp(self): 269 | """Test generating future timestamp.""" 270 | timestamp = self.jwt_lib.generate_future_timestamp(hours=1) 271 | assert isinstance(timestamp, int) 272 | expected_time = (datetime.now(tz=timezone.utc) + timedelta(hours=1)).timestamp() 273 | assert abs(timestamp - expected_time) < 2 # Within 2 seconds 274 | 275 | def test_convert_timestamp_to_datetime(self): 276 | """Test converting timestamp to datetime string.""" 277 | timestamp = int(datetime.utcnow().timestamp()) 278 | datetime_str = self.jwt_lib.convert_timestamp_to_datetime(timestamp) 279 | assert isinstance(datetime_str, str) 280 | # Should be able to parse back to datetime 281 | parsed_dt = datetime.fromisoformat(datetime_str) 282 | assert isinstance(parsed_dt, datetime) 283 | 284 | def test_error_handling_robustness(self): 285 | """Test library handles various error conditions gracefully.""" 286 | # Test with None inputs 287 | with pytest.raises((JWTTokenGenerationError, TypeError)): 288 | self.jwt_lib.generate_jwt_token(None, self.secret_key) 289 | # Test with empty payload 290 | empty_payload = {} 291 | token = self.jwt_lib.generate_jwt_token(empty_payload, self.secret_key) 292 | assert isinstance(token, str) 293 | # Test with very long secret key 294 | long_secret = "x" * 1000 295 | token = self.jwt_lib.generate_jwt_token(self.test_payload, long_secret) 296 | assert isinstance(token, str) 297 | 298 | def test_multiple_algorithms_support(self): 299 | """Test library supports multiple JWT algorithms.""" 300 | algorithms = ["HS256", "HS384", "HS512"] 301 | for alg in algorithms: 302 | token = self.jwt_lib.generate_jwt_token( 303 | self.test_payload, self.secret_key, algorithm=alg 304 | ) 305 | # Verify algorithm in header 306 | header = self.jwt_lib.decode_jwt_header(token) 307 | assert header["alg"] == alg 308 | # Verify token can be decoded 309 | decoded = self.jwt_lib.decode_jwt_payload( 310 | token, self.secret_key, algorithm=alg 311 | ) 312 | assert decoded["user_id"] == 123 313 | 314 | def test_edge_cases(self): 315 | """Test various edge cases.""" 316 | # Very short expiration 317 | token = self.jwt_lib.generate_jwt_token( 318 | self.test_payload, self.secret_key, expiration_hours=0.001 # Very short 319 | ) 320 | assert isinstance(token, str) 321 | # Large payload 322 | large_payload = {f"key_{i}": f"value_{i}" for i in range(100)} 323 | token = self.jwt_lib.generate_jwt_token(large_payload, self.secret_key) 324 | assert isinstance(token, str) 325 | # Unicode in payload 326 | unicode_payload = {"message": "Hello 世界", "emoji": "🚀"} 327 | token = self.jwt_lib.generate_jwt_token(unicode_payload, self.secret_key) 328 | decoded = self.jwt_lib.decode_jwt_payload(token, self.secret_key) 329 | assert decoded["message"] == "Hello 世界" 330 | assert decoded["emoji"] == "🚀" 331 | -------------------------------------------------------------------------------- /tests/robot/acceptance/jwt_error_handling.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation JWT error handling test suite 3 | Library JWTLibrary 4 | Library Collections 5 | 6 | *** Variables *** 7 | ${SECRET_KEY} error_test_secret_key 8 | ${INVALID_TOKEN} invalid.token.format 9 | ${MALFORMED_TOKEN} not-a-jwt-token 10 | ${EMPTY_TOKEN} ${EMPTY} 11 | 12 | *** Test Cases *** 13 | Handle Invalid Token Formats 14 | [Documentation] Test handling of various invalid token formats 15 | [Tags] jwt error-handling invalid-tokens 16 | 17 | # Test completely invalid token 18 | ${is_valid}= Verify JWT Token ${INVALID_TOKEN} ${SECRET_KEY} 19 | Should Be Equal ${is_valid} ${False} 20 | 21 | # Test malformed token 22 | ${is_valid_malformed}= Verify JWT Token ${MALFORMED_TOKEN} ${SECRET_KEY} 23 | Should Be Equal ${is_valid_malformed} ${False} 24 | 25 | # Test empty token 26 | ${is_valid_empty}= Verify JWT Token ${EMPTY_TOKEN} ${SECRET_KEY} 27 | Should Be Equal ${is_valid_empty} ${False} 28 | 29 | # Test token with wrong number of parts 30 | ${two_part_token}= Set Variable header.payload 31 | ${is_valid_two_parts}= Verify JWT Token ${two_part_token} ${SECRET_KEY} 32 | Should Be Equal ${is_valid_two_parts} ${False} 33 | 34 | # Test token with too many parts 35 | ${four_part_token}= Set Variable header.payload.signature.extra 36 | ${is_valid_four_parts}= Verify JWT Token ${four_part_token} ${SECRET_KEY} 37 | Should Be Equal ${is_valid_four_parts} ${False} 38 | # 39 | #Handle Decoding Errors 40 | # [Documentation] Test error handling during token decoding 41 | # [Tags] jwt error-handling decoding 42 | # 43 | # # Test decoding invalid token with verification 44 | # Run Keyword And Expect Error *JWT token decoding failed* 45 | # ... Decode JWT Payload ${INVALID_TOKEN} ${SECRET_KEY} 46 | # 47 | # # Test decoding with missing secret key when verification required 48 | # Run Keyword And Expect Error *Secret key is required* 49 | # ... Decode JWT Payload ${INVALID_TOKEN} verify_signature=${True} 50 | # 51 | # # Test decoding malformed token 52 | # Run Keyword And Expect Error *JWT token decoding failed* 53 | # ... Decode JWT Payload ${MALFORMED_TOKEN} ${SECRET_KEY} 54 | # 55 | # # Test header decoding of invalid token 56 | # Run Keyword And Expect Error *JWT header decoding failed* 57 | # ... Decode JWT Header ${INVALID_TOKEN} 58 | 59 | Handle Expired Tokens 60 | [Documentation] Test handling of expired JWT tokens 61 | [Tags] jwt error-handling expired-tokens 62 | 63 | # Create token that expires immediately (in the past) 64 | ${expired_payload}= Create Dictionary user_id=999 test=expired 65 | ${past_timestamp}= Evaluate __import__('time').time() - 360000 # 100 hours in the past 66 | ${past_datetime}= Evaluate __import__('datetime').datetime.fromtimestamp(${past_timestamp}) 67 | 68 | ${expired_token}= Generate JWT Token With Custom Expiration 69 | ... ${expired_payload} ${SECRET_KEY} ${past_datetime} 70 | 71 | # Verify token is detected as expired 72 | ${exp_info}= Check JWT Expiration ${expired_token} 73 | Should Be Equal ${exp_info['is_expired']} ${True} 74 | Should Be True ${exp_info['time_until_expiry']} < 0 75 | 76 | # Verify decoding with verification fails 77 | Run Keyword And Expect Error *JWT token has expired* 78 | ... Decode JWT Payload ${expired_token} ${SECRET_KEY} 79 | 80 | # Verify verification fails 81 | ${is_valid}= Verify JWT Token ${expired_token} ${SECRET_KEY} 82 | Should Be Equal ${is_valid} ${False} 83 | 84 | # But unsafe decoding should still work 85 | ${unsafe_decoded}= Decode JWT Payload ${expired_token} verify_signature=${False} 86 | Should Be Equal As Integers ${unsafe_decoded['user_id']} 999 87 | 88 | Handle Invalid Signatures 89 | [Documentation] Test handling of tokens with invalid signatures 90 | [Tags] jwt error-handling invalid-signatures 91 | 92 | # Create valid token 93 | ${payload}= Create Dictionary user_id=123 test=signature 94 | ${valid_token}= Generate JWT Token ${payload} ${SECRET_KEY} 95 | 96 | # Try to verify with wrong secret key 97 | ${wrong_secret}= Set Variable wrong_secret_key 98 | 99 | # Verification should fail 100 | ${is_valid}= Verify JWT Token ${valid_token} ${wrong_secret} 101 | Should Be Equal ${is_valid} ${False} 102 | 103 | # Decoding with wrong secret should fail 104 | Run Keyword And Expect Error *JWT token signature verification failed* 105 | ... Decode JWT Payload ${valid_token} ${wrong_secret} 106 | 107 | # Getting claims with verification should fail 108 | Run Keyword And Expect Error *JWT token signature verification failed* 109 | ... Get JWT Claim ${valid_token} user_id secret_key=${wrong_secret} verify_signature=${True} 110 | 111 | Handle Missing Claims 112 | [Documentation] Test handling of missing claims in tokens 113 | [Tags] jwt error-handling missing-claims 114 | 115 | ${payload}= Create Dictionary user_id=456 role=user 116 | ${token}= Generate JWT Token ${payload} ${SECRET_KEY} 117 | 118 | # Test getting non-existent claim 119 | Run Keyword And Expect Error *Claim 'non_existent' not found* 120 | ... Get JWT Claim ${token} non_existent 121 | 122 | # Test getting multiple claims where some don't exist 123 | ${claim_names}= Create List user_id role missing_claim another_missing 124 | ${claims}= Get Multiple JWT Claims ${token} ${claim_names} 125 | 126 | # Should get existing claims 127 | Should Be Equal As Integers ${claims['user_id']} 456 128 | Should Be Equal ${claims['role']} user 129 | 130 | # Should not contain missing claims 131 | Should Not Contain ${claims} missing_claim 132 | Should Not Contain ${claims} another_missing 133 | 134 | Handle Invalid Algorithm Errors 135 | [Documentation] Test handling of invalid algorithm specifications 136 | [Tags] jwt error-handling invalid-algorithms 137 | 138 | ${payload}= Create Dictionary user_id=789 test=algorithm 139 | 140 | # Test generation with invalid algorithm 141 | Run Keyword And Expect Error *JWT token generation failed* 142 | ... Generate JWT Token ${payload} ${SECRET_KEY} algorithm=INVALID_ALG 143 | 144 | # Test generation with unsupported algorithm 145 | Run Keyword And Expect Error *JWT token generation failed* 146 | ... Generate JWT Token ${payload} ${SECRET_KEY} algorithm=CUSTOM256 147 | 148 | Handle Payload Type Errors 149 | [Documentation] Test handling of invalid payload types and structures 150 | [Tags] jwt error-handling payload-errors 151 | 152 | # Test with None payload 153 | Run Keyword And Expect Error * 154 | ... Generate JWT Token ${None} ${SECRET_KEY} 155 | 156 | # Test with string payload (should be dict) 157 | Run Keyword And Expect Error * 158 | ... Generate JWT Token "invalid_payload" ${SECRET_KEY} 159 | 160 | # Test with list payload (should be dict) 161 | ${list_payload}= Create List item1 item2 162 | Run Keyword And Expect Error * 163 | ... Generate JWT Token ${list_payload} ${SECRET_KEY} 164 | 165 | #Handle Secret Key Errors 166 | # [Documentation] Test handling of invalid secret keys 167 | # [Tags] jwt error-handling secret-key-errors 168 | # 169 | # ${payload}= Create Dictionary user_id=999 test=secret 170 | # 171 | # # Test with None secret key 172 | # Run Keyword And Expect Error * 173 | # ... Generate JWT Token ${payload} ${None} 174 | # 175 | # # Test with empty secret key 176 | # Run Keyword And Expect Error * 177 | # ... Generate JWT Token ${payload} ${EMPTY} 178 | # 179 | # # Test verification with None secret key 180 | # ${valid_token}= Generate JWT Token ${payload} ${SECRET_KEY} 181 | # ${is_valid}= Verify JWT Token ${valid_token} ${None} 182 | # Should Be Equal ${is_valid} ${False} 183 | # 184 | #Handle Token Structure Validation Errors 185 | # [Documentation] Test token structure validation error scenarios 186 | # [Tags] jwt error-handling structure-validation 187 | # 188 | # # Test validation of completely invalid token 189 | # ${structure_info}= Validate JWT Structure ${INVALID_TOKEN} 190 | # Should Be Equal ${structure_info['is_valid_structure']} ${False} 191 | # Should Be Equal ${structure_info['has_three_parts']} ${False} 192 | # Should Not Be Empty ${structure_info['errors']} 193 | # 194 | # # Test validation of malformed token 195 | # ${malformed_structure}= Validate JWT Structure ${MALFORMED_TOKEN} 196 | # Should Be Equal ${malformed_structure['is_valid_structure']} ${False} 197 | # Should Not Be Empty ${malformed_structure['errors']} 198 | # 199 | # # Test validation of token with invalid base64 200 | # ${invalid_b64_token}= Set Variable invalid_header.invalid_payload.invalid_signature 201 | # ${invalid_structure}= Validate JWT Structure ${invalid_b64_token} 202 | # Should Be Equal ${invalid_structure['is_valid_structure']} ${False} 203 | # 204 | #Handle Timestamp Conversion Errors 205 | # [Documentation] Test timestamp conversion error handling 206 | # [Tags] jwt error-handling timestamp-errors 207 | # 208 | # # Test with invalid timestamp 209 | # Run Keyword And Expect Error * 210 | # ... Convert Timestamp To Datetime invalid_timestamp 211 | # 212 | # # Test with negative timestamp 213 | # Run Keyword And Expect Error * 214 | # ... Convert Timestamp To Datetime -1 215 | # 216 | # # Test with extremely large timestamp 217 | # ${large_timestamp}= Evaluate 9999999999999 218 | # Run Keyword And Expect Error * 219 | # ... Convert Timestamp To Datetime ${large_timestamp} 220 | 221 | Handle Claims Validation Errors 222 | [Documentation] Test claims validation error scenarios 223 | [Tags] jwt error-handling claims-validation 224 | 225 | ${payload}= Create Dictionary user_id=123 role=user 226 | ${token}= Generate JWT Token ${payload} ${SECRET_KEY} 227 | 228 | # Test claims validation with None expected claims 229 | Run Keyword And Expect Error * 230 | ... Validate JWT Claims ${token} ${None} 231 | 232 | # Test claims validation with invalid token 233 | ${expected_claims}= Create Dictionary user_id=123 234 | ${claims_valid}= Validate JWT Claims ${INVALID_TOKEN} ${expected_claims} 235 | Should Be Equal ${claims_valid} ${False} 236 | 237 | # Test audience validation with invalid token 238 | ${aud_valid}= Validate JWT Audience ${INVALID_TOKEN} my-audience 239 | Should Be Equal ${aud_valid} ${False} 240 | 241 | Handle Edge Case Scenarios 242 | [Documentation] Test various edge case error scenarios 243 | [Tags] jwt error-handling edge-cases 244 | 245 | # Test with very long claim names 246 | ${long_claim_name}= Evaluate 'very_long_claim_name' * 100 247 | ${payload}= Create Dictionary ${long_claim_name}=value 248 | ${token}= Generate JWT Token ${payload} ${SECRET_KEY} 249 | 250 | # Should be able to extract the long claim name 251 | ${long_claim_value}= Get JWT Claim ${token} ${long_claim_name} 252 | Should Be Equal ${long_claim_value} value 253 | 254 | # Test with very long claim values 255 | ${long_value}= Evaluate 'x' * 10000 256 | ${long_payload}= Create Dictionary user_id=123 long_field=${long_value} 257 | ${long_token}= Generate JWT Token ${long_payload} ${SECRET_KEY} 258 | 259 | ${extracted_long_value}= Get JWT Claim ${long_token} long_field 260 | Should Be Equal ${extracted_long_value} ${long_value} 261 | 262 | # Test with special characters in claims 263 | ${special_payload}= Create Dictionary 264 | ... user_id=123 265 | ... special_chars=!@#$%^&*() 266 | ... unicode_chars=🚀🎉✨ 267 | ... quotes="'test'" 268 | ... newlines=line1\nline2 269 | 270 | ${special_token}= Generate JWT Token ${special_payload} ${SECRET_KEY} 271 | ${decoded_special}= Decode JWT Payload ${special_token} ${SECRET_KEY} 272 | 273 | Should Be Equal ${decoded_special['special_chars']} !@#$%^&*() 274 | Should Be Equal ${decoded_special['unicode_chars']} 🚀🎉✨ 275 | Should Be Equal ${decoded_special['quotes']} "'test'" 276 | 277 | Recovery From Errors 278 | [Documentation] Test error recovery and graceful degradation 279 | [Tags] jwt error-handling recovery 280 | 281 | # Test that library continues to work after errors 282 | ${payload}= Create Dictionary user_id=123 test=recovery 283 | 284 | # Cause an error 285 | Run Keyword And Expect Error * 286 | ... Generate JWT Token ${payload} ${SECRET_KEY} algorithm=INVALID 287 | 288 | # Verify library still works normally after error 289 | ${valid_token}= Generate JWT Token ${payload} ${SECRET_KEY} 290 | ${decoded}= Decode JWT Payload ${valid_token} ${SECRET_KEY} 291 | Should Be Equal As Integers ${decoded['user_id']} 123 292 | 293 | # Cause another error 294 | Run Keyword And Expect Error * 295 | ... Decode JWT Payload ${INVALID_TOKEN} ${SECRET_KEY} 296 | 297 | # Verify library still works 298 | ${is_valid}= Verify JWT Token ${valid_token} ${SECRET_KEY} 299 | Should Be True ${is_valid} 300 | 301 | *** Keywords *** 302 | Verify Error Contains Message 303 | [Arguments] ${keyword} ${expected_message} @{args} 304 | [Documentation] Helper to verify error messages contain expected text 305 | 306 | ${error_occurred}= Run Keyword And Return Status 307 | ... Run Keyword ${keyword} @{args} 308 | 309 | Should Be Equal ${error_occurred} ${False} 310 | 311 | ${error_message}= Run Keyword And Expect Error * 312 | ... Run Keyword ${keyword} @{args} 313 | 314 | Should Contain ${error_message} ${expected_message} 315 | 316 | Test Error Robustness 317 | [Arguments] ${test_name} ${operation_keyword} @{args} 318 | [Documentation] Helper to test operation robustness with various invalid inputs 319 | 320 | Log Testing ${test_name} robustness 321 | 322 | # Test with None inputs 323 | FOR ${i} IN RANGE len($args) 324 | ${modified_args}= Copy List ${args} 325 | Set List Value ${modified_args} ${i} ${None} 326 | 327 | ${status}= Run Keyword And Return Status 328 | ... Run Keyword ${operation_keyword} @{modified_args} 329 | 330 | # Operation should either succeed or fail gracefully 331 | Log ${test_name} with None at position ${i}: ${'PASS' if $status else 'EXPECTED FAIL'} 332 | END 333 | 334 | Create Error Test Matrix 335 | [Arguments] ${base_payload} ${base_secret} 336 | [Documentation] Helper to create various error test combinations 337 | 338 | # Invalid payloads to test 339 | @{invalid_payloads}= Create List ${None} ${EMPTY} invalid_string 12345 340 | 341 | # Invalid secrets to test 342 | @{invalid_secrets}= Create List ${None} ${EMPTY} 12345 343 | 344 | # Invalid algorithms to test 345 | @{invalid_algorithms}= Create List INVALID NONE ${None} 12345 346 | 347 | # Return test matrix 348 | ${test_matrix}= Create Dictionary 349 | ... invalid_payloads=${invalid_payloads} 350 | ... invalid_secrets=${invalid_secrets} 351 | ... invalid_algorithms=${invalid_algorithms} 352 | 353 | RETURN ${test_matrix} 354 | -------------------------------------------------------------------------------- /example/real_world/api_testing_example.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation Real-world API authentication workflow using JWT 3 | Library JWTLibrary 4 | Library RequestsLibrary 5 | Library Collections 6 | Library DateTime 7 | 8 | *** Variables *** 9 | ${API_BASE_URL} http://localhost:8080/api 10 | ${JWT_SECRET} production_api_secret_key_2024 11 | ${ADMIN_SECRET} admin_service_secret_key 12 | ${SERVICE_SECRET} microservice_communication_key 13 | 14 | *** Test Cases *** 15 | Complete User Authentication Flow 16 | [Documentation] Test complete user login and API access workflow 17 | [Tags] real-world authentication api 18 | 19 | # Step 1: User Registration/Login - Generate JWT 20 | ${user_credentials}= Create Dictionary 21 | ... username=john.doe 22 | ... email=john.doe@company.com 23 | ... user_id=12345 24 | ... role=standard_user 25 | ... permissions=["profile:read", "profile:write", "data:read"] 26 | ... department=engineering 27 | 28 | ${auth_token}= Generate JWT Token ${user_credentials} ${JWT_SECRET} expiration_hours=8 29 | Log User authentication token generated 30 | 31 | # Step 2: Validate token structure and content 32 | ${token_info}= Get JWT Token Info ${auth_token} 33 | Should Be Equal ${token_info['algorithm']} HS256 34 | Should Be True ${token_info['claims_count']} >= 6 35 | 36 | # Step 3: API Request - Get User Profile 37 | ${profile_response}= Simulate API Request GET /user/profile ${auth_token} 38 | Should Be Equal ${profile_response['status']} authorized 39 | 40 | # Step 4: API Request - Update Profile (requires write permission) 41 | ${update_data}= Create Dictionary 42 | ... display_name=John Doe 43 | ... phone=+1234567890 44 | 45 | ${update_response}= Simulate API Request POST /user/profile ${auth_token} ${update_data} 46 | Should Be Equal ${update_response['status']} authorized 47 | 48 | # Step 5: Try unauthorized action (should fail) 49 | ${unauthorized_response}= Simulate API Request DELETE /admin/users/999 ${auth_token} 50 | Should Be Equal ${unauthorized_response['status']} forbidden 51 | 52 | Admin User Workflow 53 | [Documentation] Test admin user capabilities with JWT 54 | [Tags] real-world admin authorization 55 | 56 | # Admin user with elevated permissions 57 | ${admin_credentials}= Create Dictionary 58 | ... username=admin.user 59 | ... email=admin@company.com 60 | ... user_id=1 61 | ... role=admin 62 | ... permissions=["*"] 63 | ... clearance_level=5 64 | ... admin_since=2024-01-01 65 | 66 | ${admin_token}= Generate JWT Token ${admin_credentials} ${ADMIN_SECRET} expiration_hours=4 67 | 68 | # Validate admin token 69 | ${is_admin_valid}= Verify JWT Token ${admin_token} ${ADMIN_SECRET} 70 | Should Be True ${is_admin_valid} 71 | 72 | # Admin operations 73 | ${admin_role}= Get JWT Claim ${admin_token} role 74 | ${admin_permissions}= Get JWT Claim ${admin_token} permissions 75 | ${clearance}= Get JWT Claim ${admin_token} clearance_level 76 | 77 | Should Be Equal ${admin_role} admin 78 | Should Contain ${admin_permissions} * 79 | Should Be True ${clearance} >= 5 80 | 81 | # Simulate admin API calls 82 | ${user_list_response}= Simulate API Request GET /admin/users ${admin_token} 83 | ${system_config_response}= Simulate API Request GET /admin/system/config ${admin_token} 84 | 85 | Should Be Equal ${user_list_response['status']} authorized 86 | Should Be Equal ${system_config_response['status']} authorized 87 | 88 | Service-to-Service Authentication 89 | [Documentation] Test microservice communication using JWT 90 | [Tags] real-world microservices service-auth 91 | 92 | # User Service requesting data from Data Service 93 | ${service_request}= Create Dictionary 94 | ... service_name=user-service 95 | ... version=1.2.3 96 | ... iss=user-service 97 | ... aud=data-service 98 | ... sub=system 99 | ... scope=user:read user:write 100 | ... request_id=req_${EMPTY} 101 | 102 | ${request_timestamp}= Generate Current Timestamp 103 | ${request_id}= Set Variable req_${request_timestamp} 104 | Set To Dictionary ${service_request} request_id=${request_id} 105 | 106 | ${service_token}= Generate JWT Token ${service_request} ${SERVICE_SECRET} expiration_hours=1 107 | 108 | # Data Service validates the request 109 | ${service_valid}= Verify JWT Token ${service_token} ${SERVICE_SECRET} 110 | Should Be True ${service_valid} 111 | 112 | # Validate service-specific claims 113 | ${issuer}= Get JWT Claim ${service_token} iss 114 | ${audience}= Get JWT Claim ${service_token} aud 115 | ${scope}= Get JWT Claim ${service_token} scope 116 | 117 | Should Be Equal ${issuer} user-service 118 | Should Be Equal ${audience} data-service 119 | Should Contain ${scope} user:read 120 | 121 | # Validate audience specifically 122 | ${aud_valid}= Validate JWT Audience ${service_token} data-service 123 | Should Be True ${aud_valid} 124 | 125 | Log ✓ Service-to-service authentication successful 126 | 127 | API Rate Limiting with JWT 128 | [Documentation] Test API rate limiting based on JWT claims 129 | [Tags] real-world rate-limiting api 130 | 131 | # Basic tier user 132 | ${basic_user}= Create Dictionary 133 | ... user_id=10001 134 | ... username=basic.user 135 | ... subscription_tier=basic 136 | ... rate_limit_per_hour=100 137 | ... burst_limit=10 138 | 139 | ${basic_token}= Generate JWT Token ${basic_user} ${JWT_SECRET} 140 | 141 | # Premium tier user 142 | ${premium_user}= Create Dictionary 143 | ... user_id=10002 144 | ... username=premium.user 145 | ... subscription_tier=premium 146 | ... rate_limit_per_hour=1000 147 | ... burst_limit=50 148 | 149 | ${premium_token}= Generate JWT Token ${premium_user} ${JWT_SECRET} 150 | 151 | # Extract rate limiting information 152 | ${basic_limit}= Get JWT Claim ${basic_token} rate_limit_per_hour 153 | ${premium_limit}= Get JWT Claim ${premium_token} rate_limit_per_hour 154 | 155 | Should Be Equal As Integers ${basic_limit} 100 156 | Should Be Equal As Integers ${premium_limit} 1000 157 | Should Be True ${premium_limit} > ${basic_limit} 158 | 159 | # Simulate rate limit enforcement 160 | ${basic_burst}= Get JWT Claim ${basic_token} burst_limit 161 | ${premium_burst}= Get JWT Claim ${premium_token} burst_limit 162 | 163 | Should Be True ${premium_burst} > ${basic_burst} 164 | Log ✓ Rate limiting configuration extracted from JWT 165 | 166 | Multi-Tenant Application Workflow 167 | [Documentation] Test multi-tenant application with JWT isolation 168 | [Tags] real-world multi-tenant isolation 169 | 170 | # Tenant A user 171 | ${tenant_a_user}= Create Dictionary 172 | ... user_id=20001 173 | ... username=user.a 174 | ... tenant_id=acme-corp 175 | ... tenant_name=ACME Corporation 176 | ... role=manager 177 | ... data_access_scope=["acme-corp.*"] 178 | 179 | ${tenant_a_secret}= Set Variable acme_corp_secret_2024 180 | ${tenant_a_token}= Generate JWT Token ${tenant_a_user} ${tenant_a_secret} 181 | 182 | # Tenant B user 183 | ${tenant_b_user}= Create Dictionary 184 | ... user_id=30001 185 | ... username=user.b 186 | ... tenant_id=globex-inc 187 | ... tenant_name=Globex Inc 188 | ... role=analyst 189 | ... data_access_scope=["globex-inc.*"] 190 | 191 | ${tenant_b_secret}= Set Variable globex_inc_secret_2024 192 | ${tenant_b_token}= Generate JWT Token ${tenant_b_user} ${tenant_b_secret} 193 | 194 | # Validate tenant isolation 195 | ${tenant_a_valid_own}= Verify JWT Token ${tenant_a_token} ${tenant_a_secret} 196 | ${tenant_a_valid_other}= Verify JWT Token ${tenant_a_token} ${tenant_b_secret} 197 | 198 | Should Be True ${tenant_a_valid_own} 199 | Should Be Equal ${tenant_a_valid_other} ${False} 200 | 201 | # Verify tenant-specific data access 202 | ${tenant_a_scope}= Get JWT Claim ${tenant_a_token} data_access_scope 203 | ${tenant_b_scope}= Get JWT Claim ${tenant_b_token} data_access_scope 204 | 205 | Should Contain ${tenant_a_scope} acme-corp.* 206 | Should Not Contain ${tenant_a_scope} globex-inc.* 207 | Should Contain ${tenant_b_scope} globex-inc.* 208 | Should Not Contain ${tenant_b_scope} acme-corp.* 209 | 210 | Log ✓ Multi-tenant isolation verified 211 | 212 | Session Management with JWT 213 | [Documentation] Test session management and concurrent sessions 214 | [Tags] real-world session-management 215 | 216 | ${user_base}= Create Dictionary 217 | ... user_id=40001 218 | ... username=session.user 219 | ... email=session@example.com 220 | 221 | # Web session 222 | ${web_session}= Copy Dictionary ${user_base} 223 | ${web_session_id}= Set Variable web_${EMPTY} 224 | ${web_timestamp}= Generate Current Timestamp 225 | ${web_session_id}= Set Variable web_${web_timestamp} 226 | 227 | Set To Dictionary ${web_session} 228 | ... session_id=${web_session_id} 229 | ... device_type=web 230 | ... user_agent=Mozilla/5.0 Chrome/120.0 231 | ... ip_address=192.168.1.100 232 | 233 | ${web_token}= Generate JWT Token ${web_session} ${JWT_SECRET} expiration_hours=8 234 | 235 | # Mobile session 236 | ${mobile_session}= Copy Dictionary ${user_base} 237 | ${mobile_session_id}= Set Variable mobile_${EMPTY} 238 | ${mobile_timestamp}= Generate Current Timestamp 239 | ${mobile_session_id}= Set Variable mobile_${mobile_timestamp} 240 | 241 | Set To Dictionary ${mobile_session} 242 | ... session_id=${mobile_session_id} 243 | ... device_type=mobile 244 | ... user_agent=MyApp/1.0 iOS/17.0 245 | ... ip_address=192.168.1.101 246 | 247 | ${mobile_token}= Generate JWT Token ${mobile_session} ${JWT_SECRET} expiration_hours=24 248 | 249 | # Validate both sessions are independent 250 | ${web_session_id_extracted}= Get JWT Claim ${web_token} session_id 251 | ${mobile_session_id_extracted}= Get JWT Claim ${mobile_token} session_id 252 | 253 | Should Be Equal ${web_session_id_extracted} ${web_session_id} 254 | Should Be Equal ${mobile_session_id_extracted} ${mobile_session_id} 255 | Should Not Be Equal ${web_session_id} ${mobile_session_id} 256 | 257 | # Compare session tokens 258 | ${session_comparison}= Compare JWT Tokens ${web_token} ${mobile_token} 259 | Should Be Equal ${session_comparison['are_identical']} ${False} 260 | Should Contain ${session_comparison['payload_differences']} session_id 261 | Should Contain ${session_comparison['payload_differences']} device_type 262 | 263 | Log ✓ Concurrent session management verified 264 | 265 | *** Keywords *** 266 | Simulate API Request 267 | [Arguments] ${method} ${endpoint} ${token} ${payload}=${None} 268 | [Documentation] Simulate an API request with JWT authentication 269 | 270 | # Validate token first 271 | ${is_valid}= Verify JWT Token ${token} ${JWT_SECRET} 272 | 273 | IF not ${is_valid} 274 | ${response}= Create Dictionary status=invalid_token error=Token validation failed 275 | RETURN ${response} 276 | END 277 | 278 | # Extract user information for authorization 279 | ${user_role}= Get JWT Claim ${token} role 280 | ${permissions}= Get JWT Claim ${token} permissions 281 | 282 | # Simulate authorization logic 283 | ${is_authorized}= Check Endpoint Authorization ${method} ${endpoint} ${user_role} ${permissions} 284 | 285 | IF ${is_authorized} 286 | ${response}= Create Dictionary status=authorized method=${method} endpoint=${endpoint} 287 | IF ${payload} is not None 288 | Set To Dictionary ${response} payload_received=${payload} 289 | END 290 | ELSE 291 | ${response}= Create Dictionary status=forbidden error=Insufficient permissions 292 | END 293 | 294 | Log API ${method} ${endpoint}: ${response['status']} 295 | RETURN ${response} 296 | 297 | Check Endpoint Authorization 298 | [Arguments] ${method} ${endpoint} ${role} ${permissions} 299 | [Documentation] Check if user has permission for specific endpoint 300 | 301 | # Admin role has access to everything 302 | IF '${role}' == 'admin' 303 | RETURN ${True} 304 | END 305 | 306 | # Check specific endpoint permissions 307 | IF '${endpoint}' == '/user/profile' 308 | IF '${method}' == 'GET' 309 | RETURN ${'profile:read' in $permissions} 310 | ELIF '${method}' == 'POST' 311 | RETURN ${'profile:write' in $permissions} 312 | END 313 | END 314 | 315 | IF '/admin/' in '${endpoint}' 316 | RETURN ${False} # Regular users can't access admin endpoints 317 | END 318 | 319 | IF '${method}' == 'GET' and '/data/' in '${endpoint}' 320 | RETURN ${'data:read' in $permissions} 321 | END 322 | 323 | # Default to deny 324 | RETURN ${False} 325 | 326 | Generate API Test User 327 | [Arguments] ${user_id} ${role}=user ${additional_claims}=${None} 328 | [Documentation] Generate a test user with API access token 329 | 330 | ${base_user}= Create Dictionary 331 | ... user_id=${user_id} 332 | ... username=test_user_${user_id} 333 | ... email=user${user_id}@test.com 334 | ... role=${role} 335 | 336 | # Add role-specific permissions 337 | IF '${role}' == 'admin' 338 | Set To Dictionary ${base_user} permissions=["*"] 339 | ELIF '${role}' == 'manager' 340 | Set To Dictionary ${base_user} permissions=["profile:read", "profile:write", "data:read", "team:read"] 341 | ELSE 342 | Set To Dictionary ${base_user} permissions=["profile:read", "profile:write"] 343 | END 344 | 345 | # Add additional claims if provided 346 | IF ${additional_claims} is not None 347 | FOR ${key} ${value} IN &{additional_claims} 348 | Set To Dictionary ${base_user} ${key}=${value} 349 | END 350 | END 351 | 352 | ${token}= Generate JWT Token ${base_user} ${JWT_SECRET} 353 | RETURN ${token} 354 | 355 | Validate Service Communication 356 | [Arguments] ${source_service} ${target_service} ${operation} 357 | [Documentation] Validate service-to-service communication 358 | 359 | ${service_payload}= Create Dictionary 360 | ... iss=${source_service} 361 | ... aud=${target_service} 362 | ... sub=system 363 | ... operation=${operation} 364 | ... timestamp=${EMPTY} 365 | 366 | ${timestamp}= Generate Current Timestamp 367 | Set To Dictionary ${service_payload} timestamp=${timestamp} 368 | 369 | ${service_token}= Generate JWT Token ${service_payload} ${SERVICE_SECRET} expiration_hours=0.5 370 | 371 | # Validate at target service 372 | ${is_valid}= Verify JWT Token ${service_token} ${SERVICE_SECRET} 373 | Should Be True ${is_valid} 374 | 375 | ${aud_valid}= Validate JWT Audience ${service_token} ${target_service} 376 | Should Be True ${aud_valid} 377 | 378 | ${extracted_operation}= Get JWT Claim ${service_token} operation 379 | Should Be Equal ${extracted_operation} ${operation} 380 | 381 | Log ✓ Service communication validated: ${source_service} → ${target_service} (${operation}) 382 | RETURN ${service_token} 383 | -------------------------------------------------------------------------------- /tests/robot/integration/jwt_integration.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation JWT API integration test suite 3 | Library JWTLibrary 4 | Library RequestsLibrary 5 | Library Collections 6 | Library DateTime 7 | 8 | *** Variables *** 9 | ${API_SECRET} integration_test_secret_key 10 | ${API_BASE_URL} http://localhost:8000 11 | ${SERVICE_SECRET} service_to_service_secret 12 | 13 | *** Test Cases *** 14 | Test JWT Authentication Flow 15 | [Documentation] Test complete JWT authentication flow with API 16 | [Tags] jwt integration api authentication 17 | 18 | # Step 1: Generate user authentication token 19 | ${user_payload}= Create Dictionary 20 | ... user_id=12345 21 | ... username=testuser 22 | ... email=test@example.com 23 | ... role=user 24 | ... permissions=["read", "write"] 25 | 26 | ${auth_token}= Generate JWT Token ${user_payload} ${API_SECRET} expiration_hours=1 27 | 28 | # Step 2: Validate token structure 29 | ${token_info}= Get JWT Token Info ${auth_token} 30 | Should Be Equal ${token_info['algorithm']} HS256 31 | Should Be True ${token_info['claims_count']} > 5 32 | 33 | # Step 3: Verify token before API call 34 | ${is_valid}= Verify JWT Token ${auth_token} ${API_SECRET} 35 | Should Be True ${is_valid} 36 | 37 | # Step 4: Extract user information from token 38 | ${user_id}= Get JWT Claim ${auth_token} user_id 39 | ${role}= Get JWT Claim ${auth_token} role 40 | ${permissions}= Get JWT Claim ${auth_token} permissions 41 | 42 | Should Be Equal As Integers ${user_id} 12345 43 | Should Be Equal ${role} user 44 | Should Contain ${permissions} read 45 | Should Contain ${permissions} write 46 | 47 | # Step 5: Simulate API header preparation 48 | ${auth_header}= Set Variable Bearer ${auth_token} 49 | Log Authorization header prepared: ${auth_header} 50 | 51 | Test Service-to-Service JWT Communication 52 | [Documentation] Test JWT tokens for service-to-service communication 53 | [Tags] jwt integration microservices 54 | 55 | # Service A generates token for Service B 56 | ${service_a_payload}= Create Dictionary 57 | ... iss=service-a 58 | ... aud=service-b 59 | ... sub=system 60 | ... scope=data:read data:write 61 | ... service_id=svc-001 62 | ... environment=test 63 | 64 | ${service_token}= Generate JWT Token ${service_a_payload} ${SERVICE_SECRET} expiration_hours=0.5 65 | 66 | # Service B validates the token 67 | ${token_valid}= Verify JWT Token ${service_token} ${SERVICE_SECRET} 68 | Should Be True ${token_valid} 69 | 70 | # Service B extracts service information 71 | ${issuer}= Get JWT Claim ${service_token} iss 72 | ${audience}= Get JWT Claim ${service_token} aud 73 | ${scope}= Get JWT Claim ${service_token} scope 74 | 75 | Should Be Equal ${issuer} service-a 76 | Should Be Equal ${audience} service-b 77 | Should Contain ${scope} data:read 78 | 79 | # Validate audience specifically 80 | ${aud_valid}= Validate JWT Audience ${service_token} service-b 81 | Should Be True ${aud_valid} 82 | 83 | # Validate expected claims 84 | ${expected_claims}= Create Dictionary iss=service-a aud=service-b sub=system 85 | ${claims_valid}= Validate JWT Claims ${service_token} ${expected_claims} ${SERVICE_SECRET} ${True} 86 | Should Be True ${claims_valid} 87 | 88 | Test JWT Token Refresh Workflow 89 | [Documentation] Test token refresh and re-authentication workflow 90 | [Tags] jwt integration refresh 91 | 92 | # Generate initial token with short expiration 93 | ${initial_payload}= Create Dictionary 94 | ... user_id=99999 95 | ... session_id=sess-123 96 | ... role=admin 97 | ... refresh_allowed=true 98 | 99 | ${short_token}= Generate JWT Token ${initial_payload} ${API_SECRET} expiration_hours=0.01 # Very short 100 | 101 | # Verify initial token is valid 102 | ${initial_valid}= Verify JWT Token ${short_token} ${API_SECRET} 103 | Should Be True ${initial_valid} 104 | 105 | # Wait for token to expire 106 | Sleep 1 minute 107 | 108 | # Verify token is now expired 109 | ${exp_info}= Check JWT Expiration ${short_token} 110 | Should Be True ${exp_info['is_expired']} 111 | 112 | # Generate refresh token with extended expiration 113 | ${refresh_payload}= Create Dictionary 114 | ... user_id=99999 115 | ... session_id=sess-123 116 | ... role=admin 117 | ... refreshed_at=${EMPTY} 118 | 119 | ${current_time}= Get Current Date result_format=epoch 120 | Set To Dictionary ${refresh_payload} refreshed_at=${current_time} 121 | 122 | ${refresh_token}= Generate JWT Token ${refresh_payload} ${API_SECRET} expiration_hours=2 123 | 124 | # Verify refresh token 125 | ${refresh_valid}= Verify JWT Token ${refresh_token} ${API_SECRET} 126 | Should Be True ${refresh_valid} 127 | 128 | # Compare original and refresh tokens 129 | ${comparison}= Compare JWT Tokens ${short_token} ${refresh_token} 130 | Should Be Equal ${comparison['are_identical']} ${False} 131 | Should Contain ${comparison['payload_differences']} refreshed_at 132 | 133 | Test Multi-Tenant JWT Isolation 134 | [Documentation] Test JWT token isolation between different tenants 135 | [Tags] jwt integration multi-tenant 136 | 137 | # Tenant A token 138 | ${tenant_a_payload}= Create Dictionary 139 | ... user_id=1001 140 | ... tenant_id=tenant-a 141 | ... role=admin 142 | ... data_access=["tenant-a-data"] 143 | 144 | ${tenant_a_secret}= Set Variable tenant_a_secret_key 145 | ${tenant_a_token}= Generate JWT Token ${tenant_a_payload} ${tenant_a_secret} 146 | 147 | # Tenant B token 148 | ${tenant_b_payload}= Create Dictionary 149 | ... user_id=2001 150 | ... tenant_id=tenant-b 151 | ... role=user 152 | ... data_access=["tenant-b-data"] 153 | 154 | ${tenant_b_secret}= Set Variable tenant_b_secret_key 155 | ${tenant_b_token}= Generate JWT Token ${tenant_b_payload} ${tenant_b_secret} 156 | 157 | # Verify tenant isolation - Tenant A token should not validate with Tenant B secret 158 | ${cross_tenant_valid}= Verify JWT Token ${tenant_a_token} ${tenant_b_secret} 159 | Should Be Equal ${cross_tenant_valid} ${False} 160 | 161 | # Verify tenant-specific tokens work with their own secrets 162 | ${tenant_a_valid}= Verify JWT Token ${tenant_a_token} ${tenant_a_secret} 163 | ${tenant_b_valid}= Verify JWT Token ${tenant_b_token} ${tenant_b_secret} 164 | Should Be True ${tenant_a_valid} 165 | Should Be True ${tenant_b_valid} 166 | 167 | # Verify tenant-specific data access 168 | ${tenant_a_access}= Get JWT Claim ${tenant_a_token} data_access 169 | ${tenant_b_access}= Get JWT Claim ${tenant_b_token} data_access 170 | 171 | Should Contain ${tenant_a_access} tenant-a-data 172 | Should Not Contain ${tenant_a_access} tenant-b-data 173 | Should Contain ${tenant_b_access} tenant-b-data 174 | Should Not Contain ${tenant_b_access} tenant-a-data 175 | 176 | Test JWT API Rate Limiting Simulation 177 | [Documentation] Test JWT tokens in rate limiting scenarios 178 | [Tags] jwt integration rate-limiting 179 | 180 | # Generate tokens for different rate limit tiers 181 | ${basic_user_payload}= Create Dictionary 182 | ... user_id=3001 183 | ... tier=basic 184 | ... rate_limit=100 185 | ... requests_per_minute=10 186 | 187 | ${premium_user_payload}= Create Dictionary 188 | ... user_id=3002 189 | ... tier=premium 190 | ... rate_limit=1000 191 | ... requests_per_minute=100 192 | 193 | ${basic_token}= Generate JWT Token ${basic_user_payload} ${API_SECRET} 194 | ${premium_token}= Generate JWT Token ${premium_user_payload} ${API_SECRET} 195 | 196 | # Extract rate limiting information 197 | ${basic_limit}= Get JWT Claim ${basic_token} rate_limit 198 | ${premium_limit}= Get JWT Claim ${premium_token} rate_limit 199 | 200 | Should Be Equal As Integers ${basic_limit} 100 201 | Should Be Equal As Integers ${premium_limit} 1000 202 | Should Be True ${premium_limit} > ${basic_limit} 203 | 204 | # Simulate rate limit validation 205 | ${basic_rpm}= Get JWT Claim ${basic_token} requests_per_minute 206 | ${premium_rpm}= Get JWT Claim ${premium_token} requests_per_minute 207 | 208 | Should Be True ${premium_rpm} > ${basic_rpm} 209 | 210 | Test JWT RBAC (Role-Based Access Control) 211 | [Documentation] Test JWT tokens for role-based access control 212 | [Tags] jwt integration rbac authorization 213 | 214 | # Admin user token 215 | ${admin_payload}= Create Dictionary 216 | ... user_id=4001 217 | ... role=admin 218 | ... permissions=["users:read", "users:write", "users:delete", "system:config"] 219 | ... scope=admin 220 | 221 | # Regular user token 222 | ${user_payload}= Create Dictionary 223 | ... user_id=4002 224 | ... role=user 225 | ... permissions=["profile:read", "profile:write"] 226 | ... scope=user 227 | 228 | # Read-only user token 229 | ${readonly_payload}= Create Dictionary 230 | ... user_id=4003 231 | ... role=readonly 232 | ... permissions=["profile:read"] 233 | ... scope=readonly 234 | 235 | ${admin_token}= Generate JWT Token ${admin_payload} ${API_SECRET} 236 | ${user_token}= Generate JWT Token ${user_payload} ${API_SECRET} 237 | ${readonly_token}= Generate JWT Token ${readonly_payload} ${API_SECRET} 238 | 239 | # Test admin permissions 240 | ${admin_perms}= Get JWT Claim ${admin_token} permissions 241 | Should Contain ${admin_perms} users:read 242 | Should Contain ${admin_perms} users:write 243 | Should Contain ${admin_perms} users:delete 244 | Should Contain ${admin_perms} system:config 245 | 246 | # Test user permissions 247 | ${user_perms}= Get JWT Claim ${user_token} permissions 248 | Should Contain ${user_perms} profile:read 249 | Should Contain ${user_perms} profile:write 250 | Should Not Contain ${user_perms} users:delete 251 | 252 | # Test readonly permissions 253 | ${readonly_perms}= Get JWT Claim ${readonly_token} permissions 254 | Should Contain ${readonly_perms} profile:read 255 | Should Not Contain ${readonly_perms} profile:write 256 | Should Not Contain ${readonly_perms} users:read 257 | 258 | # Validate role hierarchy 259 | ${admin_role}= Get JWT Claim ${admin_token} role 260 | ${user_role}= Get JWT Claim ${user_token} role 261 | ${readonly_role}= Get JWT Claim ${readonly_token} role 262 | 263 | Should Be Equal ${admin_role} admin 264 | Should Be Equal ${user_role} user 265 | Should Be Equal ${readonly_role} readonly 266 | 267 | Test JWT Session Management 268 | [Documentation] Test JWT tokens for session management scenarios 269 | [Tags] jwt integration session-management 270 | 271 | # Create session token 272 | ${session_id}= Set Variable sess_${EMPTY} 273 | ${timestamp}= Generate Current Timestamp 274 | ${session_id}= Set Variable sess_${timestamp} 275 | 276 | ${session_payload}= Create Dictionary 277 | ... user_id=5001 278 | ... session_id=${session_id} 279 | ... device=web-browser 280 | ... ip_address=192.168.1.100 281 | ... user_agent=Mozilla/5.0... 282 | ... last_activity=${timestamp} 283 | 284 | ${session_token}= Generate JWT Token ${session_payload} ${API_SECRET} expiration_hours=8 285 | 286 | # Verify session information 287 | ${extracted_session_id}= Get JWT Claim ${session_token} session_id 288 | ${device}= Get JWT Claim ${session_token} device 289 | ${last_activity}= Get JWT Claim ${session_token} last_activity 290 | 291 | Should Be Equal ${extracted_session_id} ${session_id} 292 | Should Be Equal ${device} web-browser 293 | Should Be Equal As Integers ${last_activity} ${timestamp} 294 | 295 | # Test session activity update 296 | Sleep 2s 297 | ${new_activity_time}= Generate Current Timestamp 298 | ${updated_payload}= Copy Dictionary ${session_payload} 299 | Set To Dictionary ${updated_payload} last_activity=${new_activity_time} 300 | 301 | ${updated_token}= Generate JWT Token ${updated_payload} ${API_SECRET} expiration_hours=8 302 | 303 | # Compare session tokens 304 | ${session_comparison}= Compare JWT Tokens ${session_token} ${updated_token} 305 | Should Be Equal ${session_comparison['are_identical']} ${False} 306 | Should Contain ${session_comparison['payload_differences']} last_activity 307 | 308 | *** Keywords *** 309 | Simulate API Request With JWT 310 | [Arguments] ${token} ${endpoint} ${method}=GET 311 | [Documentation] Helper keyword to simulate API request with JWT token 312 | 313 | ${headers}= Create Dictionary Authorization=Bearer ${token} 314 | Log Simulating ${method} request to ${endpoint} 315 | Log Headers: ${headers} 316 | 317 | # In real scenario, this would make actual HTTP request 318 | # For integration test, we just validate the token format 319 | Should Not Be Empty ${token} 320 | Should Contain ${token} . # JWT has dots 321 | 322 | RETURN ${headers} 323 | 324 | Validate JWT For Endpoint Access 325 | [Arguments] ${token} ${required_permission} ${secret_key}=${API_SECRET} 326 | [Documentation] Helper to validate JWT token has required permission for endpoint 327 | 328 | ${is_valid}= Verify JWT Token ${token} ${secret_key} 329 | Should Be True ${is_valid} 330 | 331 | ${permissions}= Get JWT Claim ${token} permissions 332 | Should Contain ${permissions} ${required_permission} 333 | 334 | Log Token validated for permission: ${required_permission} 335 | 336 | Create Multi Service Environment 337 | [Documentation] Helper to create multiple service tokens for testing 338 | 339 | # Auth Service 340 | ${auth_service_payload}= Create Dictionary 341 | ... service=auth 342 | ... iss=auth-service 343 | ... capabilities=["authenticate", "authorize"] 344 | 345 | # User Service 346 | ${user_service_payload}= Create Dictionary 347 | ... service=user 348 | ... iss=user-service 349 | ... capabilities=["user:crud", "profile:manage"] 350 | 351 | # Data Service 352 | ${data_service_payload}= Create Dictionary 353 | ... service=data 354 | ... iss=data-service 355 | ... capabilities=["data:read", "data:write", "data:analytics"] 356 | 357 | ${auth_token}= Generate JWT Token ${auth_service_payload} ${SERVICE_SECRET} 358 | ${user_token}= Generate JWT Token ${user_service_payload} ${SERVICE_SECRET} 359 | ${data_token}= Generate JWT Token ${data_service_payload} ${SERVICE_SECRET} 360 | 361 | ${service_tokens}= Create Dictionary 362 | ... auth=${auth_token} 363 | ... user=${user_token} 364 | ... data=${data_token} 365 | 366 | RETURN ${service_tokens} 367 | 368 | Verify Service Communication Chain 369 | [Arguments] ${service_tokens} 370 | [Documentation] Helper to verify service-to-service communication chain 371 | 372 | # Verify each service token 373 | FOR ${service} ${token} IN &{service_tokens} 374 | ${is_valid}= Verify JWT Token ${token} ${SERVICE_SECRET} 375 | Should Be True ${is_valid} 376 | 377 | ${service_name}= Get JWT Claim ${token} service 378 | Should Be Equal ${service_name} ${service} 379 | 380 | Log Service ${service} token validated successfully 381 | END 382 | 383 | # Verify service capabilities 384 | ${auth_caps}= Get JWT Claim ${service_tokens['auth']} capabilities 385 | ${user_caps}= Get JWT Claim ${service_tokens['user']} capabilities 386 | ${data_caps}= Get JWT Claim ${service_tokens['data']} capabilities 387 | 388 | Should Contain ${auth_caps} authenticate 389 | Should Contain ${user_caps} user:crud 390 | Should Contain ${data_caps} data:read 391 | --------------------------------------------------------------------------------