├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── config ├── README.md ├── ec256-private1.pem ├── ec256-private2.pem ├── ec384-private1.pem ├── ec384-public1.pem ├── ecdsa-public1.pem ├── ecdsa-public2.pem ├── ed25519-private1.pem ├── ed25519-private2.pem ├── ed25519-public1.pem ├── ed25519-public2.pem ├── public1.jwks ├── rsa-private1.pem ├── rsa-private2.pem ├── rsa-public1.pem └── rsa-public2.pem ├── demo-server ├── Cargo.toml ├── bruno-e2e │ ├── 401 Invalid Token.bru │ ├── 401 No Token.bru │ ├── Protected EC.bru │ ├── Protected ED.bru │ ├── Protected RSA.bru │ ├── Public URL.bru │ ├── bruno.json │ └── demo-server │ │ ├── Discovery.bru │ │ ├── JWKS Endpoint.bru │ │ └── Test Tokens.bru └── src │ ├── main.rs │ └── oidc_provider │ └── mod.rs ├── jwt-authorizer ├── Cargo.toml ├── clippy.toml ├── docs │ └── README.md ├── src │ ├── authorizer.rs │ ├── builder.rs │ ├── claims.rs │ ├── error.rs │ ├── jwks │ │ ├── key_store_manager.rs │ │ └── mod.rs │ ├── layer.rs │ ├── lib.rs │ ├── oidc.rs │ └── validation.rs └── tests │ ├── common │ └── mod.rs │ ├── integration_tests.rs │ ├── tests.rs │ └── tonic.rs └── rustfmt.toml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | env: 3 | MSRV: '1.75' 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | check: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: taiki-e/install-action@protoc 16 | - uses: dtolnay/rust-toolchain@beta 17 | with: 18 | components: clippy, rustfmt 19 | - uses: Swatinem/rust-cache@v2 20 | - name: clippy 21 | run: cargo clippy --workspace --all-targets --all-features -- -D warnings 22 | - name: rustfmt 23 | run: cargo fmt --all --check 24 | check-docs: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: dtolnay/rust-toolchain@stable 29 | - uses: Swatinem/rust-cache@v2 30 | - name: cargo doc 31 | env: 32 | RUSTDOCFLAGS: "-D rustdoc::all -A rustdoc::private-doc-tests" 33 | run: cargo doc --all-features --no-deps 34 | test-versions: 35 | needs: check 36 | runs-on: ubuntu-latest 37 | strategy: 38 | matrix: 39 | rust: [stable, beta, nightly] 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: dtolnay/rust-toolchain@master 43 | with: 44 | toolchain: ${{ matrix.rust }} 45 | - uses: Swatinem/rust-cache@v2 46 | - name: Build System Info 47 | run: rustc --version 48 | - name: Run tests default features 49 | run: cargo test 50 | - name: Run tests no features 51 | run: cargo test --no-default-features 52 | - name: Run tests all features 53 | run: cargo test --all-features 54 | test-msrv: 55 | needs: check 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v4 59 | - uses: dtolnay/rust-toolchain@master 60 | with: 61 | toolchain: ${{ env.MSRV }} 62 | - name: "install Rust nightly" 63 | uses: dtolnay/rust-toolchain@nightly 64 | - uses: Swatinem/rust-cache@v2 65 | - name: Run tests all features 66 | run: cargo test --all-features 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | Cargo.lock 3 | .idea/ 4 | .vscode 5 | 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | ## 0.15.0 (2024-08-26) 11 | 12 | - tonic support is back 13 | - CI refactoring 14 | - MSRV is bumped to 75 15 | - minor dependencies updates (commit: d87b7ef90912759b0ad608bfcb7b021bb9c14e14) 16 | 17 | ## 0.14.0 (2024-01-22) 18 | 19 | - update to axum 0.7 20 | - tower-http 0.5, header 0.4, http 1.0 21 | - jsonwebtoken 9.2 22 | - tonic support removed temporarily (waiting for tonic migration to axum 0.7) 23 | 24 | ## 0.13.0 (2023-11-20) 25 | 26 | - added support for custom http client in jwks discovery (fixes #41) 27 | - `algs` added to configurable validation options 28 | - missing alg in JWK no longer defaults to RS256 but to all algs of the same alg familly 29 | - jsonwebtoken updated (8.3.0 -> 9.1.0) 30 | - make RegisteredClaims serializable (fixes #38) 31 | 32 | ## 0.12.0 (2023-10-14) 33 | 34 | - internal refactoring (no public breaking changes) 35 | - claim checker allowing closures (#32) 36 | - jwks from file or text (#37) 37 | 38 | ## 0.11.0 (2023-09-06) 39 | 40 | - support for multiple authorizers 41 | - JwtAuthorizer::layer() deprecated in favor of JwtAuthorizer::build() and IntoLayer::into_layer() 42 | - better optional claims extraction (commit: 940acb17a1de82788bc72c3657da87609ce741e9) 43 | - error 401 rather than INTERNAL_SERVER_ERROR, when no claims exist (no authorizer layer) 44 | - do not log error 45 | 46 | ## 0.10.1 (2023-07-11) 47 | 48 | ### Fixed 49 | 50 | - (RegisteredClaims) audience claim, should be a string o an array of strings 51 | 52 | ### Added 53 | 54 | - (NumericDate) optional feature enables `time` dep as an alternative to `chrono` 55 | 56 | ## 0.10.0 (2023-05-19) 57 | 58 | - tonic services support 59 | - choices of TLS support (corresponding to underlying reqwest crate features) 60 | - `RegisteredClaims` added (representing RFC7519 registered claims), used as default for `JwtAuthorizer` 61 | 62 | ## 0.9.0 (2023-04-14) 63 | 64 | ### Added 65 | 66 | - Other sources for jwt token are configurable (#10) 67 | - Cookie 68 | - AuthorizationHeader (default) 69 | - Raw PEM file content as an input for JwtAuthorizer (#15) 70 | 71 | ### Changed 72 | 73 | - Remove 'static lifetime requirement (#8) 74 | 75 | ## 0.8.1 (2023-03-16) 76 | 77 | No public API changes, no new features. 78 | 79 | ### Changed 80 | 81 | - KeyStore, KeySource refactor for better performance and security 82 | 83 | ### Fixed 84 | 85 | - Allow non root OIDC issuer (issue #1) 86 | 87 | ## 0.8.0 (2023-02-28) 88 | 89 | ### Added 90 | 91 | - validation configuration (exp, nbf, aud, iss, disable_validation) 92 | - more integration tests added 93 | 94 | ### Fixed 95 | 96 | - `JwtAuthorizer.from_ec()`, `JwtAuthorizer.from_ed()` imported PEM as DER resulting in failed validations 97 | 98 | ## 0.7.0 (2023-02-14) 99 | 100 | ### Changed 101 | 102 | - Refresh configuration - simplification, minimal_refresh_interval removed (replaced by refresh_interval in KeyNotFound refresh strategy) 103 | 104 | ### Added 105 | 106 | - integration tests, unit tests 107 | 108 | ## 0.6.0 (2023-02-05) 109 | 110 | ### Added 111 | 112 | - JwtAuthorizer::from_oidc(issuer_uri) - building from oidc discovery page 113 | 114 | ### Changed 115 | 116 | - JwtAuthorizer::layer() becomes async 117 | 118 | ### Minor Changes 119 | 120 | - demo-server refactoring 121 | 122 | ## 0.5.0 - (2023-1-28) 123 | 124 | ### Changed 125 | 126 | - JwtAuthorizer creation simplified: 127 | - JwtAuthorizer::from_* creates an instance, new() is not necessary anymore 128 | - with_check() renamed to check() 129 | 130 | ### Added 131 | 132 | - jwks store refresh configuration 133 | 134 | ### Fixed 135 | 136 | - claims extractor (JwtClaims) without authorizer should not panic, should send a 500 error 137 | 138 | ## 0.4.0 - (2023-1-21) 139 | 140 | ### Added 141 | 142 | - claims checker (stabilisation, tests, documentation) 143 | 144 | ### Fixed 145 | 146 | - added missing WWW-Authenticate header to errors 147 | 148 | ## 0.3.2 - (2023-1-18) 149 | 150 | ### Fixed 151 | 152 | - fix: when jwks store endpoint is unavailable response should be an error 500 (not 403) 153 | 154 | ## 0.3.1 - (2023-1-14) 155 | 156 | ### Fixed 157 | 158 | - fix: panicking when a bearer token is missing in protected request (be6bf9fb) 159 | 160 | ## 0.3.0 - (2023-1-13) 161 | 162 | ### Added 163 | 164 | - building the authorizer layer from rsa, ec, ed PEM files and from secret phrase (9bd99b2a) 165 | 166 | ## 0.2.0 - (2023-1-10) 167 | 168 | Initial release 169 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "demo-server", 5 | "jwt-authorizer", 6 | ] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 cduvray 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jwt-authorizer 2 | 3 | JWT authorizer Layer for Axum. 4 | 5 | [![Build status](https://github.com/cduvray/jwt-authorizer/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/tokio-rs/cduvray/jwt-authorizer/workflows/ci.yml) 6 | [![Crates.io](https://img.shields.io/crates/v/jwt-authorizer)](https://crates.io/crates/jwt-authorizer) 7 | [![Documentation](https://docs.rs/jwt-authorizer/badge.svg)](https://docs.rs/jwt-authorizer) 8 | 9 | ## Features 10 | 11 | - JWT token verification (Bearer) 12 | - Algoritms: ECDSA, RSA, EdDSA, HMAC 13 | - JWKS endpoint support 14 | - Configurable refresh 15 | - OpenId Connect Discovery 16 | - Validation 17 | - exp, nbf, iss, aud 18 | - Claims extraction 19 | - into custom deserializable structs or into `RegisteredClaims` (default) 20 | - Claims checker 21 | - Tracing support (error logging) 22 | - *tonic* support 23 | 24 | ## Usage 25 | 26 | See documentation of the [`jwt-authorizer`](./jwt-authorizer/docs/README.md) module or the [`demo-server`](./demo-server/) example. 27 | 28 | ## Development 29 | 30 | Minimum supported Rust version is 1.75. 31 | 32 | ## Contributing 33 | 34 | Contributions are wellcome! 35 | 36 | ## License 37 | 38 | MIT 39 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # Key generation 2 | 3 | ## RSA 4 | 5 | > openssl genrsa -out rsa-private2.pem 1024 6 | > openssl rsa -in rsa-private2.pem -out rsa-public2.pem -pubout -outform PEM 7 | 8 | ## EC (ECDSA) - (algorigthm ES256 - ECDSA using SHA-256) 9 | 10 | curve name: prime256v1 (secp256r1, secp384r1) 11 | 12 | > openssl ecparam -genkey -noout -name prime256v1 | openssl pkcs8 -topk8 -nocrypt -out ec-private1.pem 13 | > openssl ecparam -genkey -noout -name secp384r1 | openssl pkcs8 -topk8 -nocrypt -out ec384-private1.pem 14 | 15 | > openssl ec -in ec-private1.pem -pubout -out ec-public1.pem 16 | > openssl ec -in ec384-private1.pem -pubout -out ec384-public1.pem 17 | 18 | ## EdDSA - Edwards-curve Digital Signature Algorithm 19 | 20 | (Ed25519 - EdDSA signature scheme using SHA-512 (SHA-2) and Curve25519) 21 | 22 | > openssl genpkey -algorithm ed25519 23 | 24 | ## JWK - combined file of above keys 25 | 26 | > rnbyc -j -f rsa-public1.pem -k rsa01 -a RS256 -f ecdsa-public1.pem -k ec01 -a ES256 -f ed25519-public1.pem -k ed01 -a EdDSA -o public1.jw 27 | -------------------------------------------------------------------------------- /config/ec256-private1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgH614zrAA67VgIGav 3 | JQU718PqSvV95Z/QO0I4oxxpekmhRANCAAQxmLBzkRU/8Te+R3agp52viVZUw32+ 4 | B3IEGkEhUUmPBtZ6S1O+QejNJm9Nc1AFaHmCUgzpUCZr4wXaiz4yb8t2 5 | -----END PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /config/ec256-private2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWTFfCGljY6aw3Hrt 3 | kHmPRiazukxPLb6ilpRAewjW8nihRANCAATDskChT+Altkm9X7MI69T3IUmrQU0L 4 | 950IxEzvw/x5BMEINRMrXLBJhqzO9Bm+d6JbqA21YQmd1Kt4RzLJR1W+ 5 | -----END PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /config/ec384-private1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCEPIELj6Yh/C7YPArh 3 | GlU1Hnv85nYTrRKozX4qONvS9RgDHDXalK9yFgUDh7jkIi+hZANiAAQTrPmB0t7h 4 | qDNsoQsDdI6Vx9f07PV3QcKNxbn6/Rs4HcRE3rERUFqinPBdUqTyJ+W/HFbjTkDU 5 | 9JnNRU68B7KVzCMKL/yw+bavLja+a8pBjH+MHVTR+cslxDlD2svexSA= 6 | -----END PRIVATE KEY----- 7 | -------------------------------------------------------------------------------- /config/ec384-public1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEE6z5gdLe4agzbKELA3SOlcfX9Oz1d0HC 3 | jcW5+v0bOB3ERN6xEVBaopzwXVKk8iflvxxW405A1PSZzUVOvAeylcwjCi/8sPm2 4 | ry42vmvKQYx/jB1U0fnLJcQ5Q9rL3sUg 5 | -----END PUBLIC KEY----- 6 | -------------------------------------------------------------------------------- /config/ecdsa-public1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMZiwc5EVP/E3vkd2oKedr4lWVMN9 3 | vgdyBBpBIVFJjwbWektTvkHozSZvTXNQBWh5glIM6VAma+MF2os+Mm/Ldg== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /config/ecdsa-public2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEw7JAoU/gJbZJvV+zCOvU9yFJq0FN 3 | C/edCMRM78P8eQTBCDUTK1ywSYaszvQZvneiW6gNtWEJndSreEcyyUdVvg== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /config/ed25519-private1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MC4CAQAwBQYDK2VwBCIEIEoAdOduNX+lt7bOPEkqAOwYabpsGhyxiAaUbMw184Ca 3 | -----END PRIVATE KEY----- 4 | -------------------------------------------------------------------------------- /config/ed25519-private2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MC4CAQAwBQYDK2VwBCIEIGrD/e7uKYqSY4twDEsRfMMuLSrODf14dpTiTK6K1YI0 3 | -----END PRIVATE KEY----- 4 | -------------------------------------------------------------------------------- /config/ed25519-public1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MCowBQYDK2VwAyEAuWtSkE+I9aTMYTTvuTE1rtu0rNdxp3DU33cJ/ksL1Gk= 3 | -----END PUBLIC KEY----- 4 | -------------------------------------------------------------------------------- /config/ed25519-public2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MCowBQYDK2VwAyEA2+Jj2UvNCvQiUPNYRgSi0cJSPiJI6Rs6D0UTeEpQVj8= 3 | -----END PUBLIC KEY----- 4 | -------------------------------------------------------------------------------- /config/public1.jwks: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "alg": "RS256", 5 | "e": "AQAB", 6 | "kid": "rsa01", 7 | "kty": "RSA", 8 | "n": "2pQeZdxa7q093K7bj5h6-leIpxfTnuAxzXdhjfGEJHxmt2ekHyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy_tw-5e-Px1xFj1PykGEkRlOpYSAeWsNaAWvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV-fcGGLhJnXl0-5_z7tKC7RvBoT3SGwlc_AmJqpFtTpEBn_fDnyqiZbpcjXYLExFpExm41xDitRKHWIwfc3dV8_vlNntlxCPGy_THkjdXJoHv2IJmlhvmr5_h03iGMLWDKSywxOol_4Wc1BT7Hb6byMxW40GKwSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO-XiVShZRVg7JeraGAfWwaIgIJ1D8C1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4NDX-A4KRMgaxcfAcui_x6gybksq6gF90-9nfQfmVMVJctZ6M-FvRr-itd1Nef5WAtwUp1qyZygAXU3cH3rarscajmurOsP6dE1OHl3grY_eZhQxk33VBK9lavqNKPg6Q_PLiq1ojbYBj3bcYifJrsNeQwxldQP83aWt5rGtgZTehKVJwa40Uy_Grae1iRnsDtdSy5sTJIJ6EiShnWAdMoGejdiI8vpkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPWc6JEotQqI0HwhzU0KHyoY4s" 9 | }, 10 | { 11 | "alg": "ES256", 12 | "crv": "P-256", 13 | "kid": "ec01", 14 | "kty": "EC", 15 | "x": "MZiwc5EVP_E3vkd2oKedr4lWVMN9vgdyBBpBIVFJjwY", 16 | "y": "1npLU75B6M0mb01zUAVoeYJSDOlQJmvjBdqLPjJvy3Y" 17 | }, 18 | { 19 | "alg": "EdDSA", 20 | "crv": "Ed25519", 21 | "kid": "ed01", 22 | "kty": "OKP", 23 | "x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /config/rsa-private1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJJwIBAAKCAgEA2pQeZdxa7q093K7bj5h6+leIpxfTnuAxzXdhjfGEJHxmt2ek 3 | HyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy/tw+5e+Px1xFj1PykGEkRlOpYSAeWsNaA 4 | WvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV+fcGGLhJnXl0+5/z7tKC7RvBoT3S 5 | Gwlc/AmJqpFtTpEBn/fDnyqiZbpcjXYLExFpExm41xDitRKHWIwfc3dV8/vlNntl 6 | xCPGy/THkjdXJoHv2IJmlhvmr5/h03iGMLWDKSywxOol/4Wc1BT7Hb6byMxW40GK 7 | wSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO+XiVShZRVg7JeraGAfWwaIgIJ1D8C 8 | 1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4NDX+A4KRMgaxcfAcui/x6gybksq6g 9 | F90+9nfQfmVMVJctZ6M+FvRr+itd1Nef5WAtwUp1qyZygAXU3cH3rarscajmurOs 10 | P6dE1OHl3grY/eZhQxk33VBK9lavqNKPg6Q/PLiq1ojbYBj3bcYifJrsNeQwxldQ 11 | P83aWt5rGtgZTehKVJwa40Uy/Grae1iRnsDtdSy5sTJIJ6EiShnWAdMoGejdiI8v 12 | pkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPWc6JEotQqI0HwhzU0KHyoY4sCAwEA 13 | AQKCAgA6UAG8Ewl/W2CBm3Sf3oIQf4HJciXW4ODoe8ze3Wvvf/C3RUMXushHXT7p 14 | vgB/aXiJHeKjwnj2AjNNmSgcYmmNj8ZZJh6IF85/XB8Ap3Rd4whkrRUZMNOCx/3e 15 | 53J4iaJfIOiAJBlYEQ2Jymcoj43wXeKWfbPF+z0mVAnz0N10/E6wAZon9FuGMdU0 16 | WA/dQfo4/xSFRg6FLS673p4dvCtYGSii17JjtEm/acKKP3AC41THMCx6I0FJ8Ee9 17 | zl3FvCyMil1r9o2YlQLeM+042XPgbDfMkNsKTE8GlYJY8R0GeN1FS5sE42zqtI2L 18 | glrz056oJVdWc2HZPG9M2BmT3KsQXE1C2Bgp6yVLjzL0WtLU3S2M3XM1lXQm+MiA 19 | fZWNastrmRnMA8b+NcEWAsKtl5xgXtyIOTQn6L4n0rJdwyh84LYIOfdT0q/dmc4Y 20 | fl88iJp7w3AZwZN1PH+wJDNYuIHsL2CucOx2vui5zoWSVfb4fxCwv3UCSXe1FHrj 21 | Xlk15Loqz4VzBa9V8eNlUdxtYB9MC0lkQ/u6qj4peBbcHoflCMxCp7WMNhye1HLL 22 | 4utz4FgiXIdNXVLAMIsoyK1ZWx3MFCrR58jbu2smFdqIcnVq5UJHdD9yz3BUjl0x 23 | cicgDpSNt9QeTUrLutAsu08rdAfl9o6Og2QFk80hrxMX1B0eAQKCAQEA/OKzTtnw 24 | BK5ZKze22FYBr+Zpa+J4rD7jLlrbD4AGRRsx3FGI6f7JiW3U98v/uOi8IEgAeGZw 25 | z4C0JUI7jL6LzB67o4L7IqP7fjEegWZlnC36UGLGtldyj2lGgON67vZxw9hq/tat 26 | aIoMSkMVsOXDIMy7p5S1zodF426FC2D8UF+759dZ716U0wjYGAMKZrVIwa8fhEVu 27 | CflUTntq17tPbpr/vjvrC2jqi8hk8ma+lH1vHDCsTiNmm69auGiqEKjTLUJY7AiA 28 | 2r0f7yfAdtpSgOnKQ2+kcOsSVO00EjqD6VpV7/MDok36pLwdiqZ/TrWAjjsYqPHs 29 | 9OSmb2liuCW8ywKCAQEA3UVBS9KURjYALPqnGaoCc3yWD/oxZJGs5B3kPY4tiTJG 30 | 4RZ2K/oYxkp6e1FojlhEjoghn/l+fRHjoZVkj7znHR4JjQYRoU8DgGQ+99+uyt6Q 31 | q6XD1LLqHvU/3As8DwSFrpp4pjCXAGWSwKGp57vax3RegrmC9TvaIkBpye3mUtPp 32 | F/RrFewI8Rhj1SvoCu0qGilYhyItPeSvbTkEcnv9lV5R7mjnehmDwG0stM2TyA6v 33 | 5oBa26ZCEU0TzTnaJodpc4FUXux+AfmctfIpClcSRm8ipOgF17ikH6lVKdmwHJ8x 34 | alV3Uh1MPARZlJwnuQlgRXX93s9cjGCLnKqEiN/cQQKCAQAoeD8px0bZ+OzcNbZV 35 | OK5ccAs+8KdPKWFB8dhMyrg2Jvv7vjCjAdtO2vzSCxuJg/VXVS5+FibHjllF/St6 36 | gqPsrp5otHVsPcHpmALBwplQPStp4eTbGXOD790Qk1cBFv9t0ByPW9u0dyMwXzwB 37 | a0Om5BzD3NCblJpiozU3dPXsBuYTXCtQW1qFy0yJyzLG7QwPsu7gRBwwDG6pgKbA 38 | j4FOug9jakNbOBcQ96jwAfFN4iT95ewtNQ0erRlfmaBduibRf2SroVC9sLaDl2D9 39 | pEK/zqpH0H4IdBYi8TL8F9E0bviBxeo29zO9WT2BCtQkzHceS+bOYqkBJ/ZargrW 40 | XXOxAoIBAHzOoYQJJUVtFDBKuZJKSNOnRGWCs/WMDb8l9SWbWqf2SfCQYNtxWCQQ 41 | woFoa9dOhmz28DBx5BzbyE/OGkjRPnM4DB8Ve0BHdywmXzYlX0xiuat39ru0p0YL 42 | A5g0Zg36eQUBcGgdJC8/G8W36kQhu8ehJeYKiYmV1vZW6tTRcYbqrKGsZfKZjnmf 43 | TkBhYaM4HvVeuOaQKoCsyx6KeK2yrlhgOUqGtXozhhM2AW+CPYcscZ9MavNWFhH4 44 | LeEmbpwo6RwTqOlZ78FhcDlYfDmu30oHSb1GenUxWrHZK4ZNmX6rdI4L4x/YErYP 45 | pg+i/OzsEvdbFHVm9Ubg9h7KN7OUwYECggEAGPsNQTDp9cRle0sGXF5ExaATzLHA 46 | rWtCMN2cv66dq6aDY0qZwdwOSBUoy3lzCxq4nOpSb0hKu1aT2J5pA7dLrLMgQA1N 47 | G0fhVKpGgJh7RtTT+W2CvY6fDB5EjZ4W4o4YBJa0Pgmcrh5waKaDQQtk4dD39iCw 48 | r0h5ahrduf64C5COMQFZiDElRQqY9zxkdWTfHN/3wcVzqmfEX5/kOvteRNNHlVBp 49 | 6NkTKDlY+TKgb3VHic0+KyD7/x7pmTEnbEDkWGWLF3GThKQbBgr2nIvuUpSMleTR 50 | d62haX3C90QHcUBJHhIxcujvwVhbIm3iDA/D9cyswq0iaOM0JKW1Hjk+fA== 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /config/rsa-private2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJETqse41HRBsc 3 | 7cfcq3ak4oZWFCoZlcic525A3FfO4qW9BMtRO/iXiyCCHn8JhiL9y8j5JdVP2Q9Z 4 | IpfElcFd3/guS9w+5RqQGgCR+H56IVUyHZWtTJbKPcwWXQdNUX0rBFcsBzCRESJL 5 | eelOEdHIjG7LRkx5l/FUvlqsyHDVJEQsHwegZ8b8C0fz0EgT2MMEdn10t6Ur1rXz 6 | jMB/wvCg8vG8lvciXmedyo9xJ8oMOh0wUEgxziVDMMovmC+aJctcHUAYubwoGN8T 7 | yzcvnGqL7JSh36Pwy28iPzXZ2RLhAyJFU39vLaHdljwthUaupldlNyCfa6Ofy4qN 8 | ctlUPlN1AgMBAAECggEAdESTQjQ70O8QIp1ZSkCYXeZjuhj081CK7jhhp/4ChK7J 9 | GlFQZMwiBze7d6K84TwAtfQGZhQ7km25E1kOm+3hIDCoKdVSKch/oL54f/BK6sKl 10 | qlIzQEAenho4DuKCm3I4yAw9gEc0DV70DuMTR0LEpYyXcNJY3KNBOTjN5EYQAR9s 11 | 2MeurpgK2MdJlIuZaIbzSGd+diiz2E6vkmcufJLtmYUT/k/ddWvEtz+1DnO6bRHh 12 | xuuDMeJA/lGB/EYloSLtdyCF6sII6C6slJJtgfb0bPy7l8VtL5iDyz46IKyzdyzW 13 | tKAn394dm7MYR1RlUBEfqFUyNK7C+pVMVoTwCC2V4QKBgQD64syfiQ2oeUlLYDm4 14 | CcKSP3RnES02bcTyEDFSuGyyS1jldI4A8GXHJ/lG5EYgiYa1RUivge4lJrlNfjyf 15 | dV230xgKms7+JiXqag1FI+3mqjAgg4mYiNjaao8N8O3/PD59wMPeWYImsWXNyeHS 16 | 55rUKiHERtCcvdzKl4u35ZtTqQKBgQDNKnX2bVqOJ4WSqCgHRhOm386ugPHfy+8j 17 | m6cicmUR46ND6ggBB03bCnEG9OtGisxTo/TuYVRu3WP4KjoJs2LD5fwdwJqpgtHl 18 | yVsk45Y1Hfo+7M6lAuR8rzCi6kHHNb0HyBmZjysHWZsn79ZM+sQnLpgaYgQGRbKV 19 | DZWlbw7g7QKBgQCl1u+98UGXAP1jFutwbPsx40IVszP4y5ypCe0gqgon3UiY/G+1 20 | zTLp79GGe/SjI2VpQ7AlW7TI2A0bXXvDSDi3/5Dfya9ULnFXv9yfvH1QwWToySpW 21 | Kvd1gYSoiX84/WCtjZOr0e0HmLIb0vw0hqZA4szJSqoxQgvF22EfIWaIaQKBgQCf 22 | 34+OmMYw8fEvSCPxDxVvOwW2i7pvV14hFEDYIeZKW2W1HWBhVMzBfFB5SE8yaCQy 23 | pRfOzj9aKOCm2FjjiErVNpkQoi6jGtLvScnhZAt/lr2TXTrl8OwVkPrIaN0bG/AS 24 | aUYxmBPCpXu3UjhfQiWqFq/mFyzlqlgvuCc9g95HPQKBgAscKP8mLxdKwOgX8yFW 25 | GcZ0izY/30012ajdHY+/QK5lsMoxTnn0skdS+spLxaS5ZEO4qvPVb8RAoCkWMMal 26 | 2pOhmquJQVDPDLuZHdrIiKiDM20dy9sMfHygWcZjQ4WSxf/J7T9canLZIXFhHAZT 27 | 3wc9h4G8BBCtWN2TN/LsGZdB 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /config/rsa-public1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2pQeZdxa7q093K7bj5h6 3 | +leIpxfTnuAxzXdhjfGEJHxmt2ekHyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy/tw+5 4 | e+Px1xFj1PykGEkRlOpYSAeWsNaAWvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV 5 | +fcGGLhJnXl0+5/z7tKC7RvBoT3SGwlc/AmJqpFtTpEBn/fDnyqiZbpcjXYLExFp 6 | Exm41xDitRKHWIwfc3dV8/vlNntlxCPGy/THkjdXJoHv2IJmlhvmr5/h03iGMLWD 7 | KSywxOol/4Wc1BT7Hb6byMxW40GKwSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO+ 8 | XiVShZRVg7JeraGAfWwaIgIJ1D8C1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4N 9 | DX+A4KRMgaxcfAcui/x6gybksq6gF90+9nfQfmVMVJctZ6M+FvRr+itd1Nef5WAt 10 | wUp1qyZygAXU3cH3rarscajmurOsP6dE1OHl3grY/eZhQxk33VBK9lavqNKPg6Q/ 11 | PLiq1ojbYBj3bcYifJrsNeQwxldQP83aWt5rGtgZTehKVJwa40Uy/Grae1iRnsDt 12 | dSy5sTJIJ6EiShnWAdMoGejdiI8vpkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPW 13 | c6JEotQqI0HwhzU0KHyoY4sCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /config/rsa-public2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyRE6rHuNR0QbHO3H3Kt2 3 | pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5/CYYi/cvI+SXVT9kPWSKXxJXB 4 | Xd/4LkvcPuUakBoAkfh+eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHR 5 | yIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG/AtH89BIE9jDBHZ9dLelK9a184zAf8Lw 6 | oPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xq 7 | i+yUod+j8MtvIj812dkS4QMiRVN/by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5T 8 | dQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /demo-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "demo-server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.83" 10 | axum = { version = "0.7.5" } 11 | headers = "0.4" 12 | josekit = "0.8.6" 13 | jsonwebtoken = "9.3.0" 14 | once_cell = "1.19.0" 15 | reqwest = { version = "0.12.4", features = ["json"] } 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_json = "1.0" 18 | thiserror = "1.0.60" 19 | tokio = { version = "1.37.0", features = ["full"] } 20 | tower-http = { version = "0.5.2", features = ["trace"] } 21 | tracing = "0.1.40" 22 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 23 | jwt-authorizer = { path = "../jwt-authorizer" } 24 | -------------------------------------------------------------------------------- /demo-server/bruno-e2e/401 Invalid Token.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 401 Invalid Token 3 | type: http 4 | seq: 6 5 | } 6 | 7 | get { 8 | url: http://localhost:3000/api/protected 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: blablabla.xxxx.xxxx 15 | } 16 | 17 | assert { 18 | res.status: eq 401 19 | res.headers['www-authenticate']: eq Bearer error="invalid_token" 20 | } 21 | -------------------------------------------------------------------------------- /demo-server/bruno-e2e/401 No Token.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: 401 No Token 3 | type: http 4 | seq: 5 5 | } 6 | 7 | get { 8 | url: http://localhost:3000/api/protected 9 | body: none 10 | auth: none 11 | } 12 | 13 | assert { 14 | res.status: eq 401 15 | res.headers['www-authenticate']: eq Bearer 16 | } 17 | -------------------------------------------------------------------------------- /demo-server/bruno-e2e/Protected EC.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Protected EC 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: http://localhost:3000/api/protected 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImVjMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.YMQHWpGLJ3P59SvPX-RIW3uT5rfzShzcP1TNcaXr0VnsxCXYO0og0c3_O30no0D_ct0hOUJINY5tBsok-66Gzw 15 | } 16 | 17 | assert { 18 | res.status: eq 200 19 | res.body: contains b@b.com 20 | } 21 | -------------------------------------------------------------------------------- /demo-server/bruno-e2e/Protected ED.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Protected ED 3 | type: http 4 | seq: 3 5 | } 6 | 7 | get { 8 | url: http://localhost:3000/api/protected 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImVkMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.5bFOZqc-lBFy4gFifQ_CTx1A3R6Nry71gdi7KH2GGvTZQC_ZI1vNbqGnWQhpR6n_jUd9ICUc0pPI5iLCB6K1Bg 15 | } 16 | 17 | assert { 18 | res.status: eq 200 19 | res.body: contains b@b.com 20 | } 21 | -------------------------------------------------------------------------------- /demo-server/bruno-e2e/Protected RSA.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Protected RSA 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: http://localhost:3000/api/protected 9 | body: none 10 | auth: bearer 11 | } 12 | 13 | auth:bearer { 14 | token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InJzYTAxIn0.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.pmm8Kdk-SvycXIGpWb1R0DuP5nlB7w4QQS7trhN_OjOpbk0A8F_lC4BdClz3rol2Pgo61lcFckJgjNBj34DQGeTGOtvxdiUXNgi1aKiXH4AyPzZeZx30PgFxa1fxhuZhBAj6xIZKBSBQvVyjeVQzAScINRCBX8zfCaXSU1ZCUkJl5vbD7zT-cYIFU76we9HcIYKRXwTiAyoNn3Lixa1H3_t5sbx3om2WlIB2x-sGpoDFDjorcuJT1yQx3grTRTBzHyRBRjZ3e8wrMbiacy-m3WoEFdkssQgYi_dSQH0hvxgacvGWayK0UqD7O5UL6EzTA2feXbgA_68o5gfvSnM8CUsPut5gZr-gwVbQKPbBdCQtl_wXIMot7UNKYEiFV38x5EmUr-ShzQcditW6fciguuY1Qav502UE1UMXvt5p8-kYxw2AaaVd6iTgQBzkBrtvywMYWzIwzGNA70RvUhI2rlgcn8GEU_51Tv_NMHjp6CjDbAxQVKa0PlcRE4pd6yk_IJSR4Nska_8BQZdPbsFn--z_XHEDoRZQ1C1M6m77xVndg3zX0sNQPXfWsttCbBmaHvMKTOp0cH9rlWB9r9nTo9fn8jcfqlak2O2IAzfzsOdVfUrES6T1UWkWobs9usGgqJuIkZHbDd4tmXyPRT4wrU7hxEyE9cuvuZPAi8GYt80 15 | } 16 | 17 | assert { 18 | res.status: eq 200 19 | res.body: contains b@b.com 20 | } 21 | -------------------------------------------------------------------------------- /demo-server/bruno-e2e/Public URL.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Public URL 3 | type: http 4 | seq: 4 5 | } 6 | 7 | get { 8 | url: http://localhost:3000/public 9 | body: none 10 | auth: none 11 | } 12 | 13 | assert { 14 | res.status: eq 200 15 | } 16 | -------------------------------------------------------------------------------- /demo-server/bruno-e2e/bruno.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "name": "jwt-authorizer E2E", 4 | "type": "collection", 5 | "ignore": [ 6 | "node_modules", 7 | ".git" 8 | ] 9 | } -------------------------------------------------------------------------------- /demo-server/bruno-e2e/demo-server/Discovery.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Discovery 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: http://localhost:3001/.well-known/openid-configuration 9 | body: none 10 | auth: none 11 | } 12 | 13 | assert { 14 | res.status: eq 200 15 | } 16 | -------------------------------------------------------------------------------- /demo-server/bruno-e2e/demo-server/JWKS Endpoint.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: JWKS Endpoint 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: http://localhost:3001/jwks 9 | body: none 10 | auth: none 11 | } 12 | 13 | assert { 14 | res.status: eq 200 15 | } 16 | -------------------------------------------------------------------------------- /demo-server/bruno-e2e/demo-server/Test Tokens.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Test Tokens 3 | type: http 4 | seq: 3 5 | } 6 | 7 | get { 8 | url: http://localhost:3001/tokens 9 | body: none 10 | auth: none 11 | } 12 | 13 | assert { 14 | res.status: eq 200 15 | } 16 | -------------------------------------------------------------------------------- /demo-server/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Demo server - for demo and testing purposes 2 | 3 | use axum::{routing::get, Router}; 4 | use jwt_authorizer::{ 5 | error::InitError, AuthError, Authorizer, IntoLayer, JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy, 6 | }; 7 | use serde::Deserialize; 8 | use tokio::net::TcpListener; 9 | use tower_http::trace::TraceLayer; 10 | use tracing::info; 11 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 12 | 13 | mod oidc_provider; 14 | 15 | /// Object representing claims 16 | /// (a subset of deserialized claims) 17 | #[derive(Debug, Deserialize, Clone)] 18 | struct User { 19 | sub: String, 20 | } 21 | 22 | #[tokio::main] 23 | async fn main() -> Result<(), InitError> { 24 | tracing_subscriber::registry() 25 | .with(tracing_subscriber::EnvFilter::new( 26 | std::env::var("RUST_LOG").unwrap_or_else(|_| "info,jwt_authorizer=debug,tower_http=debug".into()), 27 | )) 28 | .with(tracing_subscriber::fmt::layer()) 29 | .init(); 30 | 31 | // claims checker function 32 | fn claim_checker(u: &User) -> bool { 33 | info!("checking claims: {} -> {}", u.sub, u.sub.contains('@')); 34 | 35 | u.sub.contains('@') // must be an email 36 | } 37 | 38 | // starting oidc provider (discovery is needed by from_oidc()) 39 | let issuer_uri = oidc_provider::run_server(); 40 | 41 | // First let's create an authorizer builder from a Oidc Discovery 42 | // User is a struct deserializable from JWT claims representing the authorized user 43 | // let jwt_auth: JwtAuthorizer = JwtAuthorizer::from_oidc("https://accounts.google.com/") 44 | let auth: Authorizer = JwtAuthorizer::from_oidc(issuer_uri) 45 | // .no_refresh() 46 | .refresh(Refresh { 47 | strategy: RefreshStrategy::Interval, 48 | ..Default::default() 49 | }) 50 | .check(claim_checker) 51 | .build() 52 | .await?; 53 | 54 | // actual router demo 55 | let api = Router::new() 56 | .route("/protected", get(protected)) 57 | // adding the authorizer layer 58 | .layer(auth.into_layer()); 59 | 60 | let app = Router::new() 61 | // public endpoint 62 | .route("/public", get(public_handler)) 63 | // protected APIs 64 | .nest("/api", api) 65 | .layer(TraceLayer::new_for_http()); 66 | 67 | let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap(); 68 | tracing::info!("listening on {:?}", listener.local_addr()); 69 | 70 | axum::serve(listener, app.into_make_service()).await.unwrap(); 71 | 72 | Ok(()) 73 | } 74 | 75 | /// handler with injected claims object 76 | async fn protected(JwtClaims(user): JwtClaims) -> Result { 77 | // Send the protected data to the user 78 | Ok(format!("Welcome: {}", user.sub)) 79 | } 80 | 81 | // public url handler 82 | async fn public_handler() -> &'static str { 83 | "Public URL!" 84 | } 85 | -------------------------------------------------------------------------------- /demo-server/src/oidc_provider/mod.rs: -------------------------------------------------------------------------------- 1 | use axum::{routing::get, Json, Router}; 2 | use josekit::jwk::{ 3 | alg::{ec::EcCurve, ec::EcKeyPair, ed::EdKeyPair, rsa::RsaKeyPair}, 4 | Jwk, 5 | }; 6 | use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; 7 | use jwt_authorizer::{NumericDate, OneOrArray, RegisteredClaims}; 8 | use serde::{Deserialize, Serialize}; 9 | use serde_json::{json, Value}; 10 | use std::{thread, time::Duration}; 11 | use tokio::net::TcpListener; 12 | 13 | const ISSUER_URI: &str = "http://localhost:3001"; 14 | 15 | /// OpenId Connect discovery (simplified for test purposes) 16 | #[derive(Serialize, Clone)] 17 | struct OidcDiscovery { 18 | issuer: String, 19 | jwks_uri: String, 20 | } 21 | 22 | /// discovery url handler 23 | async fn discovery() -> Json { 24 | let d = OidcDiscovery { 25 | issuer: ISSUER_URI.to_owned(), 26 | jwks_uri: format!("{ISSUER_URI}/jwks"), 27 | }; 28 | Json(json!(d)) 29 | } 30 | 31 | #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] 32 | struct JwkSet { 33 | keys: Vec, 34 | } 35 | 36 | /// jwk set endpoint handler 37 | async fn jwks() -> Json { 38 | let mut kset = JwkSet { keys: Vec::::new() }; 39 | 40 | let keypair = RsaKeyPair::from_pem(include_bytes!("../../../config/rsa-private1.pem")).unwrap(); 41 | let mut pk = keypair.to_jwk_public_key(); 42 | pk.set_key_id("rsa01"); 43 | pk.set_algorithm("RS256"); 44 | pk.set_key_use("sig"); 45 | kset.keys.push(pk); 46 | 47 | let keypair = RsaKeyPair::from_pem(include_bytes!("../../../config/rsa-private2.pem")).unwrap(); 48 | let mut pk = keypair.to_jwk_public_key(); 49 | pk.set_key_id("rsa02"); 50 | pk.set_algorithm("RS256"); 51 | pk.set_key_use("sig"); 52 | kset.keys.push(pk); 53 | 54 | let keypair = EcKeyPair::from_pem(include_bytes!("../../../config/ec256-private1.pem"), Some(EcCurve::P256)).unwrap(); 55 | let mut pk = keypair.to_jwk_public_key(); 56 | pk.set_key_id("ec01"); 57 | pk.set_algorithm("ES256"); 58 | pk.set_key_use("sig"); 59 | kset.keys.push(pk); 60 | 61 | let keypair = EcKeyPair::from_pem(include_bytes!("../../../config/ec256-private2.pem"), Some(EcCurve::P256)).unwrap(); 62 | let mut pk = keypair.to_jwk_public_key(); 63 | pk.set_key_id("ec02"); 64 | pk.set_algorithm("ES256"); 65 | pk.set_key_use("sig"); 66 | kset.keys.push(pk); 67 | 68 | let keypair = EcKeyPair::from_pem(include_bytes!("../../../config/ec384-private1.pem"), Some(EcCurve::P384)).unwrap(); 69 | let mut pk = keypair.to_jwk_public_key(); 70 | pk.set_key_id("ec01-es384"); 71 | pk.set_algorithm("ES384"); 72 | pk.set_key_use("sig"); 73 | kset.keys.push(pk); 74 | 75 | let keypair = EdKeyPair::from_pem(include_bytes!("../../../config/ed25519-private1.pem")).unwrap(); 76 | let mut pk = keypair.to_jwk_public_key(); 77 | pk.set_key_id("ed01"); 78 | pk.set_algorithm("EdDSA"); 79 | pk.set_key_use("sig"); 80 | kset.keys.push(pk); 81 | 82 | let keypair = EdKeyPair::from_pem(include_bytes!("../../../config/ed25519-private2.pem")).unwrap(); 83 | let mut pk = keypair.to_jwk_public_key(); 84 | pk.set_key_id("ed02"); 85 | pk.set_algorithm("EdDSA"); 86 | pk.set_key_use("sig"); 87 | kset.keys.push(pk); 88 | 89 | Json(json!(kset)) 90 | } 91 | 92 | /// build a minimal JWT header 93 | fn build_header(alg: Algorithm, kid: &str) -> Header { 94 | Header { 95 | typ: Some("JWT".to_string()), 96 | alg, 97 | kid: Some(kid.to_owned()), 98 | cty: None, 99 | jku: None, 100 | jwk: None, 101 | x5u: None, 102 | x5c: None, 103 | x5t: None, 104 | x5t_s256: None, 105 | } 106 | } 107 | 108 | /// token claims 109 | #[derive(Debug, Serialize, Deserialize)] 110 | struct Claims { 111 | iss: &'static str, 112 | sub: &'static str, 113 | exp: usize, 114 | nbf: usize, 115 | } 116 | 117 | /// handler issuing test tokens (this is not a standard endpoint) 118 | pub async fn tokens() -> Json { 119 | let claims = Claims { 120 | iss: ISSUER_URI, 121 | sub: "b@b.com", 122 | exp: 2000000000, // May 2033 123 | nbf: 1516239022, // Jan 2018 124 | }; 125 | 126 | let claims_with_aud = RegisteredClaims { 127 | iss: Some(ISSUER_URI.to_owned()), 128 | sub: Some("b@b.com".to_owned()), 129 | aud: Some(OneOrArray::Array(vec!["aud1".to_owned(), "aud2".to_owned()])), 130 | exp: Some(NumericDate(2000000000)), // May 2033 131 | nbf: Some(NumericDate(1516239022)), // Jan 2018 132 | iat: None, 133 | jti: None, 134 | }; 135 | 136 | let rsa1_key = EncodingKey::from_rsa_pem(include_bytes!("../../../config/rsa-private1.pem")).unwrap(); 137 | let rsa2_key = EncodingKey::from_rsa_pem(include_bytes!("../../../config/rsa-private2.pem")).unwrap(); 138 | let ec1_key = EncodingKey::from_ec_pem(include_bytes!("../../../config/ec256-private1.pem")).unwrap(); 139 | let ec2_key = EncodingKey::from_ec_pem(include_bytes!("../../../config/ec256-private2.pem")).unwrap(); 140 | let ec1_es384_key = EncodingKey::from_ec_pem(include_bytes!("../../../config/ec384-private1.pem")).unwrap(); 141 | let ed1_key = EncodingKey::from_ed_pem(include_bytes!("../../../config/ed25519-private1.pem")).unwrap(); 142 | let ed2_key = EncodingKey::from_ed_pem(include_bytes!("../../../config/ed25519-private2.pem")).unwrap(); 143 | 144 | let rsa1_token = encode(&build_header(Algorithm::RS256, "rsa01"), &claims, &rsa1_key).unwrap(); 145 | let rsa1_token_aud = encode(&build_header(Algorithm::RS256, "rsa01"), &claims_with_aud, &rsa1_key).unwrap(); 146 | let rsa2_token = encode(&build_header(Algorithm::RS256, "rsa02"), &claims, &rsa2_key).unwrap(); 147 | let ec1_token_aud = encode(&build_header(Algorithm::ES256, "ec01"), &claims_with_aud, &ec1_key).unwrap(); 148 | let ec1_token = encode(&build_header(Algorithm::ES256, "ec01"), &claims, &ec1_key).unwrap(); 149 | let ec2_token = encode(&build_header(Algorithm::ES256, "ec02"), &claims, &ec2_key).unwrap(); 150 | let ec1_es384_token = encode(&build_header(Algorithm::ES384, "ec01-es384"), &claims, &ec1_es384_key).unwrap(); 151 | let ed1_token = encode(&build_header(Algorithm::EdDSA, "ed01"), &claims, &ed1_key).unwrap(); 152 | let ed2_token = encode(&build_header(Algorithm::EdDSA, "ed02"), &claims, &ed2_key).unwrap(); 153 | 154 | Json(json!({ 155 | "rsa01": rsa1_token, 156 | "rsa01_aud": rsa1_token_aud, 157 | "rsa02": rsa2_token, 158 | "ec01": ec1_token, 159 | "ec01_aud": ec1_token_aud, 160 | "ec02": ec2_token, 161 | "ec01_es384": ec1_es384_token, 162 | "ed01": ed1_token, 163 | "ed02": ed2_token, 164 | })) 165 | } 166 | 167 | /// exposes some oidc "like" endpoints for test purposes 168 | pub fn run_server() -> &'static str { 169 | let app = Router::new() 170 | .route("/.well-known/openid-configuration", get(discovery)) 171 | .route("/jwks", get(jwks)) 172 | .route("/tokens", get(tokens)); 173 | 174 | tokio::spawn(async move { 175 | let listener = TcpListener::bind("127.0.0.1:3001").await.unwrap(); 176 | tracing::info!("oidc provider starting on: {:?}", listener.local_addr()); 177 | axum::serve(listener, app.into_make_service()).await.unwrap(); 178 | }); 179 | 180 | thread::sleep(Duration::from_millis(200)); // waiting oidc to start 181 | 182 | ISSUER_URI 183 | } 184 | -------------------------------------------------------------------------------- /jwt-authorizer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jwt-authorizer" 3 | description = "jwt authorizer middleware for axum and tonic" 4 | version = "0.15.0" 5 | edition = "2021" 6 | authors = ["cduvray "] 7 | license = "MIT" 8 | readme = "docs/README.md" 9 | repository = "https://github.com/cduvray/jwt-authorizer" 10 | keywords = ["jwt", "axum", "authorisation", "jwks"] 11 | 12 | [dependencies] 13 | axum = { version = "0.7" } 14 | chrono = { version = "0.4", optional = true } 15 | futures-util = "0.3" 16 | futures-core = "0.3" 17 | headers = "0.4" 18 | jsonwebtoken = "9.3" 19 | http = "1.1" 20 | pin-project = "1.1" 21 | reqwest = { version = "0.12.4", default-features = false, features = ["json"] } 22 | serde = { version = "1.0", features = ["derive"] } 23 | serde_json = "1.0" 24 | thiserror = "1.0" 25 | tokio = { version = "1.37", features = ["full"] } 26 | tower-http = { version = "0.5", features = ["trace", "auth"] } 27 | tower-layer = "0.3" 28 | tower-service = "0.3" 29 | tracing = "0.1" 30 | tonic = { version = "0.12", optional = true } 31 | time = { version = "0.3", optional = true } 32 | http-body-util = "0.1.1" 33 | 34 | [dev-dependencies] 35 | hyper = { version = "1.3.1", features = ["full"] } 36 | lazy_static = "1.4.0" 37 | prost = "0.13" 38 | tower = { version = "0.4.13", features = ["util", "buffer"] } 39 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 40 | wiremock = "0.6.1" 41 | 42 | [features] 43 | default = ["default-tls", "chrono"] 44 | default-tls = ["reqwest/default-tls"] 45 | native-tls = ["reqwest/native-tls"] 46 | native-tls-vendored = ["reqwest/native-tls-vendored"] 47 | native-tls-alpn = ["reqwest/native-tls-alpn"] 48 | rustls-tls = ["reqwest/rustls-tls"] 49 | rustls-tls-manual-roots = ["reqwest/rustls-tls-manual-roots"] 50 | rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"] 51 | rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"] 52 | time = ["dep:time"] 53 | chrono = ["dep:chrono"] 54 | 55 | [[test]] 56 | name = "tonic" 57 | required-features = ["tonic"] 58 | -------------------------------------------------------------------------------- /jwt-authorizer/clippy.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cduvray/jwt-authorizer/10a926c25b717f3bf7f62c406fa1eda300c2b698/jwt-authorizer/clippy.toml -------------------------------------------------------------------------------- /jwt-authorizer/docs/README.md: -------------------------------------------------------------------------------- 1 | # jwt-authorizer 2 | 3 | JWT authoriser Layer for Axum and Tonic. 4 | 5 | ## Features 6 | 7 | - JWT token verification (Bearer) 8 | - Algoritms: ECDSA, RSA, EdDSA, HMAC 9 | - JWKS endpoint support 10 | - Configurable refresh 11 | - OpenId Connect Discovery 12 | - Validation 13 | - exp, nbf, iss, aud 14 | - Claims extraction 15 | - Claims checker 16 | - Tracing support (error logging) 17 | - *tonic* support 18 | - multiple authorizers 19 | 20 | 21 | ## Usage Example 22 | 23 | ```rust 24 | # use jwt_authorizer::{AuthError, Authorizer, JwtAuthorizer, JwtClaims, RegisteredClaims, IntoLayer}; 25 | # use axum::{routing::get, Router}; 26 | # use serde::Deserialize; 27 | # use tokio::net::TcpListener; 28 | # async { 29 | 30 | // let's create an authorizer builder from a JWKS Endpoint 31 | // (a serializable struct can be used to represent jwt claims, JwtAuthorizer is the default) 32 | let auth: Authorizer = 33 | JwtAuthorizer::from_jwks_url("http://localhost:3000/oidc/jwks").build().await.unwrap(); 34 | 35 | // adding the authorization layer 36 | let app = Router::new().route("/protected", get(protected)) 37 | .layer(auth.into_layer()); 38 | 39 | // proteced handler with user injection (mapping some jwt claims) 40 | async fn protected(JwtClaims(user): JwtClaims) -> Result { 41 | // Send the protected data to the user 42 | Ok(format!("Welcome: {:?}", user.sub)) 43 | } 44 | let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap(); 45 | axum::serve(listener, app.into_make_service()).await.expect("server failed"); 46 | # }; 47 | ``` 48 | 49 | ## Multiple Authorizers 50 | 51 | A layer can be built using multiple authorizers (`IntoLayer` is implemented for `[Authorizer; N]` and for `Vec>`). 52 | The authorizers are sequentially applied until one of them validates the token. If no authorizer validates it the request is rejected. 53 | 54 | ## Validation 55 | 56 | Validation configuration object. 57 | 58 | If no validation configuration is provided default values will be applyed. 59 | 60 | docs: [`jwt-authorizer::Validation`] 61 | 62 | ```rust 63 | # use jwt_authorizer::{JwtAuthorizer, Validation}; 64 | # use serde_json::Value; 65 | 66 | let validation = Validation::new() 67 | .iss(&["https://issuer1", "https://issuer2"]) 68 | .aud(&["audience1"]) 69 | .nbf(true) 70 | .leeway(20); 71 | 72 | let jwt_auth: JwtAuthorizer = JwtAuthorizer::from_oidc("https://accounts.google.com") 73 | .validation(validation); 74 | 75 | ``` 76 | 77 | 78 | ## ClaimsChecker 79 | 80 | A check function (mapping deserialized claims to boolean) can be added to the authorizer. 81 | 82 | A check failure results in a 403 (WWW-Authenticate: Bearer error="insufficient_scope") error. 83 | 84 | Example: 85 | 86 | ```rust 87 | 88 | use jwt_authorizer::{JwtAuthorizer}; 89 | use serde::Deserialize; 90 | 91 | // Authorized entity, struct deserializable from JWT claims 92 | #[derive(Debug, Deserialize, Clone)] 93 | struct User { 94 | sub: String, 95 | } 96 | 97 | let authorizer = JwtAuthorizer::from_rsa_pem("../config/jwtRS256.key.pub") 98 | .check( 99 | |claims: &User| claims.sub.contains('@') // must be an email 100 | ); 101 | ``` 102 | 103 | ## JWKS Refresh 104 | 105 | By default the jwks keys are reloaded when a request token is signed with a key (`kid` jwt header) that is not present in the store (a minimal intervale between 2 reloads is 10s by default, can be configured). 106 | 107 | - [`JwtAuthorizer::no_refresh()`] configures one and unique reload of jwks keys 108 | - [`JwtAuthorizer::refresh(refresh_configuration)`] allows to define a finer configuration for jwks refreshing, for more details see the documentation of `Refresh` struct. 109 | 110 | [`jwt-authorizer::Validation`]: https://docs.rs/jwt-authorizer/latest/jwt_authorizer/validation/struct.Validation.html 111 | [`JwtAuthorizer::no_refresh()`]: https://docs.rs/jwt-authorizer/latest/jwt_authorizer/layer/struct.JwtAuthorizer.html#method.no_refresh 112 | [`JwtAuthorizer::refresh(refresh_configuration)`]: https://docs.rs/jwt-authorizer/latest/jwt_authorizer/layer/struct.JwtAuthorizer.html#method.refresh 113 | -------------------------------------------------------------------------------- /jwt-authorizer/src/authorizer.rs: -------------------------------------------------------------------------------- 1 | use std::{io::Read, sync::Arc}; 2 | 3 | use headers::{authorization::Bearer, Authorization, HeaderMapExt}; 4 | use http::HeaderMap; 5 | use jsonwebtoken::{decode, decode_header, jwk::JwkSet, Algorithm, DecodingKey, TokenData}; 6 | use reqwest::{Client, Url}; 7 | use serde::de::DeserializeOwned; 8 | 9 | use crate::{ 10 | error::{AuthError, InitError}, 11 | jwks::{key_store_manager::KeyStoreManager, KeyData, KeySource}, 12 | layer::{self, AuthorizationLayer, JwtSource}, 13 | oidc, Refresh, RegisteredClaims, 14 | }; 15 | 16 | pub type ClaimsCheckerFn = Arc bool + Send + Sync>>; 17 | 18 | pub struct Authorizer 19 | where 20 | C: Clone + Send, 21 | { 22 | pub key_source: KeySource, 23 | pub claims_checker: Option>, 24 | pub validation: crate::validation::Validation, 25 | pub jwt_source: JwtSource, 26 | } 27 | 28 | fn read_data(path: &str) -> Result, InitError> { 29 | let mut data = Vec::::new(); 30 | let mut f = std::fs::File::open(path)?; 31 | f.read_to_end(&mut data)?; 32 | Ok(data) 33 | } 34 | 35 | pub enum KeySourceType { 36 | RSA(String), 37 | RSAString(String), 38 | EC(String), 39 | ECString(String), 40 | ED(String), 41 | EDString(String), 42 | Secret(String), 43 | Jwks(String), 44 | JwksPath(String), 45 | JwksString(String), // TODO: expose JwksString in JwtAuthorizer or remove it 46 | Discovery(String), 47 | } 48 | 49 | impl Authorizer 50 | where 51 | C: DeserializeOwned + Clone + Send, 52 | { 53 | pub(crate) async fn build( 54 | key_source_type: KeySourceType, 55 | claims_checker: Option>, 56 | refresh: Option, 57 | validation: crate::validation::Validation, 58 | jwt_source: JwtSource, 59 | http_client: Option, 60 | ) -> Result, InitError> { 61 | Ok(match key_source_type { 62 | KeySourceType::RSA(path) => { 63 | let key = DecodingKey::from_rsa_pem(&read_data(path.as_str())?)?; 64 | Authorizer { 65 | key_source: KeySource::SingleKeySource(Arc::new(KeyData { 66 | kid: None, 67 | algs: vec![ 68 | Algorithm::RS256, 69 | Algorithm::RS384, 70 | Algorithm::RS512, 71 | Algorithm::PS256, 72 | Algorithm::PS384, 73 | Algorithm::PS512, 74 | ], 75 | key, 76 | })), 77 | claims_checker, 78 | validation, 79 | jwt_source, 80 | } 81 | } 82 | KeySourceType::RSAString(text) => { 83 | let key = DecodingKey::from_rsa_pem(text.as_bytes())?; 84 | Authorizer { 85 | key_source: KeySource::SingleKeySource(Arc::new(KeyData { 86 | kid: None, 87 | algs: vec![ 88 | Algorithm::RS256, 89 | Algorithm::RS384, 90 | Algorithm::RS512, 91 | Algorithm::PS256, 92 | Algorithm::PS384, 93 | Algorithm::PS512, 94 | ], 95 | key, 96 | })), 97 | claims_checker, 98 | validation, 99 | jwt_source, 100 | } 101 | } 102 | KeySourceType::EC(path) => { 103 | let key = DecodingKey::from_ec_pem(&read_data(path.as_str())?)?; 104 | Authorizer { 105 | key_source: KeySource::SingleKeySource(Arc::new(KeyData { 106 | kid: None, 107 | algs: vec![Algorithm::ES256, Algorithm::ES384], 108 | key, 109 | })), 110 | claims_checker, 111 | validation, 112 | jwt_source, 113 | } 114 | } 115 | KeySourceType::ECString(text) => { 116 | let key = DecodingKey::from_ec_pem(text.as_bytes())?; 117 | Authorizer { 118 | key_source: KeySource::SingleKeySource(Arc::new(KeyData { 119 | kid: None, 120 | algs: vec![Algorithm::ES256, Algorithm::ES384], 121 | key, 122 | })), 123 | claims_checker, 124 | validation, 125 | jwt_source, 126 | } 127 | } 128 | KeySourceType::ED(path) => { 129 | let key = DecodingKey::from_ed_pem(&read_data(path.as_str())?)?; 130 | Authorizer { 131 | key_source: KeySource::SingleKeySource(Arc::new(KeyData { 132 | kid: None, 133 | algs: vec![Algorithm::EdDSA], 134 | key, 135 | })), 136 | claims_checker, 137 | validation, 138 | jwt_source, 139 | } 140 | } 141 | KeySourceType::EDString(text) => { 142 | let key = DecodingKey::from_ed_pem(text.as_bytes())?; 143 | Authorizer { 144 | key_source: KeySource::SingleKeySource(Arc::new(KeyData { 145 | kid: None, 146 | algs: vec![Algorithm::EdDSA], 147 | key, 148 | })), 149 | claims_checker, 150 | validation, 151 | jwt_source, 152 | } 153 | } 154 | KeySourceType::Secret(secret) => { 155 | let key = DecodingKey::from_secret(secret.as_bytes()); 156 | Authorizer { 157 | key_source: KeySource::SingleKeySource(Arc::new(KeyData { 158 | kid: None, 159 | algs: vec![Algorithm::HS256, Algorithm::HS384, Algorithm::HS512], 160 | key, 161 | })), 162 | claims_checker, 163 | validation, 164 | jwt_source, 165 | } 166 | } 167 | KeySourceType::JwksPath(path) => { 168 | let set: JwkSet = serde_json::from_slice(&read_data(path.as_str())?)?; 169 | let keys = set 170 | .keys 171 | .iter() 172 | .map(|k| match KeyData::from_jwk(k) { 173 | Ok(kdata) => Ok(Arc::new(kdata)), 174 | Err(err) => Err(InitError::KeyDecodingError(err)), 175 | }) 176 | .collect::, _>>()?; 177 | Authorizer { 178 | key_source: KeySource::MultiKeySource(keys.into()), 179 | claims_checker, 180 | validation, 181 | jwt_source, 182 | } 183 | } 184 | KeySourceType::JwksString(jwks_str) => { 185 | // TODO: expose it in JwtAuthorizer or remove 186 | let set: JwkSet = serde_json::from_str(jwks_str.as_str())?; 187 | let keys = set 188 | .keys 189 | .iter() 190 | .map(|k| match KeyData::from_jwk(k) { 191 | Ok(kdata) => Ok(Arc::new(kdata)), 192 | Err(err) => Err(InitError::KeyDecodingError(err)), 193 | }) 194 | .collect::, _>>()?; 195 | Authorizer { 196 | key_source: KeySource::MultiKeySource(keys.into()), 197 | claims_checker, 198 | validation, 199 | jwt_source, 200 | } 201 | } 202 | KeySourceType::Jwks(url) => { 203 | let jwks_url = Url::parse(url.as_str()).map_err(|e| InitError::JwksUrlError(e.to_string()))?; 204 | let key_store_manager = KeyStoreManager::new(jwks_url, refresh.unwrap_or_default()); 205 | Authorizer { 206 | key_source: KeySource::KeyStoreSource(key_store_manager), 207 | claims_checker, 208 | validation, 209 | jwt_source, 210 | } 211 | } 212 | KeySourceType::Discovery(issuer_url) => { 213 | let jwks_url = Url::parse(&oidc::discover_jwks(issuer_url.as_str(), http_client).await?) 214 | .map_err(|e| InitError::JwksUrlError(e.to_string()))?; 215 | 216 | let key_store_manager = KeyStoreManager::new(jwks_url, refresh.unwrap_or_default()); 217 | Authorizer { 218 | key_source: KeySource::KeyStoreSource(key_store_manager), 219 | claims_checker, 220 | validation, 221 | jwt_source, 222 | } 223 | } 224 | }) 225 | } 226 | 227 | pub async fn check_auth(&self, token: &str) -> Result, AuthError> { 228 | let header = decode_header(token)?; 229 | // TODO: (optimisation) build & store jwt_validation in key data, to avoid rebuilding it for each check 230 | let val_key = self.key_source.get_key(header).await?; 231 | let jwt_validation = &self.validation.to_jwt_validation(&val_key.algs); 232 | let token_data = decode::(token, &val_key.key, jwt_validation)?; 233 | 234 | if let Some(ref checker) = self.claims_checker { 235 | if !checker(&token_data.claims) { 236 | return Err(AuthError::InvalidClaims()); 237 | } 238 | } 239 | 240 | Ok(token_data) 241 | } 242 | 243 | pub fn extract_token(&self, h: &HeaderMap) -> Option { 244 | match &self.jwt_source { 245 | layer::JwtSource::AuthorizationHeader => { 246 | let bearer_o: Option> = h.typed_get(); 247 | bearer_o.map(|b| String::from(b.0.token())) 248 | } 249 | layer::JwtSource::Cookie(name) => h 250 | .typed_get::() 251 | .and_then(|c| c.get(name.as_str()).map(String::from)), 252 | } 253 | } 254 | } 255 | 256 | pub trait IntoLayer 257 | where 258 | C: Clone + DeserializeOwned + Send, 259 | { 260 | fn into_layer(self) -> AuthorizationLayer; 261 | } 262 | 263 | impl IntoLayer for Vec> 264 | where 265 | C: Clone + DeserializeOwned + Send, 266 | { 267 | fn into_layer(self) -> AuthorizationLayer { 268 | AuthorizationLayer::new(self.into_iter().map(Arc::new).collect()) 269 | } 270 | } 271 | 272 | impl IntoLayer for Vec>> 273 | where 274 | C: Clone + DeserializeOwned + Send, 275 | { 276 | fn into_layer(self) -> AuthorizationLayer { 277 | AuthorizationLayer::new(self.into_iter().collect()) 278 | } 279 | } 280 | 281 | impl IntoLayer for [Authorizer; N] 282 | where 283 | C: Clone + DeserializeOwned + Send, 284 | { 285 | fn into_layer(self) -> AuthorizationLayer { 286 | AuthorizationLayer::new(self.into_iter().map(Arc::new).collect()) 287 | } 288 | } 289 | 290 | impl IntoLayer for [Arc>; N] 291 | where 292 | C: Clone + DeserializeOwned + Send, 293 | { 294 | fn into_layer(self) -> AuthorizationLayer { 295 | AuthorizationLayer::new(self.into_iter().collect()) 296 | } 297 | } 298 | 299 | impl IntoLayer for Authorizer 300 | where 301 | C: Clone + DeserializeOwned + Send, 302 | { 303 | fn into_layer(self) -> AuthorizationLayer { 304 | AuthorizationLayer::new(vec![Arc::new(self)]) 305 | } 306 | } 307 | 308 | impl IntoLayer for Arc> 309 | where 310 | C: Clone + DeserializeOwned + Send, 311 | { 312 | fn into_layer(self) -> AuthorizationLayer { 313 | AuthorizationLayer::new(vec![self]) 314 | } 315 | } 316 | 317 | #[cfg(test)] 318 | mod tests { 319 | 320 | use jsonwebtoken::{Algorithm, Header}; 321 | use serde_json::Value; 322 | 323 | use crate::{layer::JwtSource, validation::Validation}; 324 | 325 | use super::{Authorizer, KeySourceType}; 326 | 327 | #[tokio::test] 328 | async fn build_from_secret() { 329 | let h = Header::new(Algorithm::HS256); 330 | let a = Authorizer::::build( 331 | KeySourceType::Secret("xxxxxx".to_owned()), 332 | None, 333 | None, 334 | Validation::new(), 335 | JwtSource::AuthorizationHeader, 336 | None, 337 | ) 338 | .await 339 | .unwrap(); 340 | let k = a.key_source.get_key(h); 341 | assert!(k.await.is_ok()); 342 | } 343 | 344 | #[tokio::test] 345 | async fn build_from_jwks_string() { 346 | let jwks = r#" 347 | {"keys": [{ 348 | "kid": "1", 349 | "kty": "RSA", 350 | "alg": "RS256", 351 | "use": "sig", 352 | "n": "2pQeZdxa7q093K7bj5h6-leIpxfTnuAxzXdhjfGEJHxmt2ekHyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy_tw-5e-Px1xFj1PykGEkRlOpYSAeWsNaAWvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV-fcGGLhJnXl0-5_z7tKC7RvBoT3SGwlc_AmJqpFtTpEBn_fDnyqiZbpcjXYLExFpExm41xDitRKHWIwfc3dV8_vlNntlxCPGy_THkjdXJoHv2IJmlhvmr5_h03iGMLWDKSywxOol_4Wc1BT7Hb6byMxW40GKwSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO-XiVShZRVg7JeraGAfWwaIgIJ1D8C1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4NDX-A4KRMgaxcfAcui_x6gybksq6gF90-9nfQfmVMVJctZ6M-FvRr-itd1Nef5WAtwUp1qyZygAXU3cH3rarscajmurOsP6dE1OHl3grY_eZhQxk33VBK9lavqNKPg6Q_PLiq1ojbYBj3bcYifJrsNeQwxldQP83aWt5rGtgZTehKVJwa40Uy_Grae1iRnsDtdSy5sTJIJ6EiShnWAdMoGejdiI8vpkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPWc6JEotQqI0HwhzU0KHyoY4s", 353 | "e": "AQAB" 354 | }]} 355 | "#; 356 | let a = Authorizer::::build( 357 | KeySourceType::JwksString(jwks.to_owned()), 358 | None, 359 | None, 360 | Validation::new(), 361 | JwtSource::AuthorizationHeader, 362 | None, 363 | ) 364 | .await 365 | .unwrap(); 366 | let k = a.key_source.get_key(Header::new(Algorithm::RS256)); 367 | assert!(k.await.is_ok()); 368 | } 369 | 370 | #[tokio::test] 371 | async fn build_from_file() { 372 | let a = Authorizer::::build( 373 | KeySourceType::RSA("../config/rsa-public1.pem".to_owned()), 374 | None, 375 | None, 376 | Validation::new(), 377 | JwtSource::AuthorizationHeader, 378 | None, 379 | ) 380 | .await 381 | .unwrap(); 382 | let k = a.key_source.get_key(Header::new(Algorithm::RS256)); 383 | assert!(k.await.is_ok()); 384 | 385 | let a = Authorizer::::build( 386 | KeySourceType::EC("../config/ecdsa-public1.pem".to_owned()), 387 | None, 388 | None, 389 | Validation::new(), 390 | JwtSource::AuthorizationHeader, 391 | None, 392 | ) 393 | .await 394 | .unwrap(); 395 | let k = a.key_source.get_key(Header::new(Algorithm::ES256)); 396 | assert!(k.await.is_ok()); 397 | 398 | let a = Authorizer::::build( 399 | KeySourceType::ED("../config/ed25519-public1.pem".to_owned()), 400 | None, 401 | None, 402 | Validation::new(), 403 | JwtSource::AuthorizationHeader, 404 | None, 405 | ) 406 | .await 407 | .unwrap(); 408 | let k = a.key_source.get_key(Header::new(Algorithm::EdDSA)); 409 | assert!(k.await.is_ok()); 410 | 411 | let a = Authorizer::::build( 412 | KeySourceType::JwksPath("../config/public1.jwks".to_owned()), 413 | None, 414 | None, 415 | Validation::new(), 416 | JwtSource::AuthorizationHeader, 417 | None, 418 | ) 419 | .await 420 | .unwrap(); 421 | a.key_source 422 | .get_key(Header::new(Algorithm::RS256)) 423 | .await 424 | .expect("Couldn't get RS256 key from jwk"); 425 | a.key_source 426 | .get_key(Header::new(Algorithm::ES256)) 427 | .await 428 | .expect("Couldn't get ES256 key from jwk"); 429 | a.key_source 430 | .get_key(Header::new(Algorithm::EdDSA)) 431 | .await 432 | .expect("Couldn't get EdDSA key from jwk"); 433 | } 434 | 435 | #[tokio::test] 436 | async fn build_from_text() { 437 | let a = Authorizer::::build( 438 | KeySourceType::RSAString(include_str!("../../config/rsa-public1.pem").to_owned()), 439 | None, 440 | None, 441 | Validation::new(), 442 | JwtSource::AuthorizationHeader, 443 | None, 444 | ) 445 | .await 446 | .unwrap(); 447 | let k = a.key_source.get_key(Header::new(Algorithm::RS256)); 448 | assert!(k.await.is_ok()); 449 | 450 | let a = Authorizer::::build( 451 | KeySourceType::ECString(include_str!("../../config/ecdsa-public1.pem").to_owned()), 452 | None, 453 | None, 454 | Validation::new(), 455 | JwtSource::AuthorizationHeader, 456 | None, 457 | ) 458 | .await 459 | .unwrap(); 460 | let k = a.key_source.get_key(Header::new(Algorithm::ES256)); 461 | assert!(k.await.is_ok()); 462 | 463 | let a = Authorizer::::build( 464 | KeySourceType::EDString(include_str!("../../config/ed25519-public1.pem").to_owned()), 465 | None, 466 | None, 467 | Validation::new(), 468 | JwtSource::AuthorizationHeader, 469 | None, 470 | ) 471 | .await 472 | .unwrap(); 473 | let k = a.key_source.get_key(Header::new(Algorithm::EdDSA)); 474 | assert!(k.await.is_ok()); 475 | } 476 | 477 | #[tokio::test] 478 | async fn build_file_errors() { 479 | let a = Authorizer::::build( 480 | KeySourceType::RSA("./config/does-not-exist.pem".to_owned()), 481 | None, 482 | None, 483 | Validation::new(), 484 | JwtSource::AuthorizationHeader, 485 | None, 486 | ) 487 | .await; 488 | println!("{:?}", a.as_ref().err()); 489 | assert!(a.is_err()); 490 | } 491 | 492 | #[tokio::test] 493 | async fn build_jwks_url_error() { 494 | let a = Authorizer::::build( 495 | KeySourceType::Jwks("://xxxx".to_owned()), 496 | None, 497 | None, 498 | Validation::default(), 499 | JwtSource::AuthorizationHeader, 500 | None, 501 | ) 502 | .await; 503 | println!("{:?}", a.as_ref().err()); 504 | assert!(a.is_err()); 505 | } 506 | 507 | #[tokio::test] 508 | async fn build_discovery_url_error() { 509 | let a = Authorizer::::build( 510 | KeySourceType::Discovery("://xxxx".to_owned()), 511 | None, 512 | None, 513 | Validation::default(), 514 | JwtSource::AuthorizationHeader, 515 | None, 516 | ) 517 | .await; 518 | println!("{:?}", a.as_ref().err()); 519 | assert!(a.is_err()); 520 | } 521 | } 522 | -------------------------------------------------------------------------------- /jwt-authorizer/src/builder.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use serde::de::DeserializeOwned; 4 | 5 | use crate::{ 6 | authorizer::{ClaimsCheckerFn, KeySourceType}, 7 | error::InitError, 8 | layer::{AuthorizationLayer, JwtSource}, 9 | Authorizer, Refresh, RefreshStrategy, RegisteredClaims, Validation, 10 | }; 11 | 12 | use reqwest::Client; 13 | 14 | /// Authorizer Layer builder 15 | /// 16 | /// - initialisation of the Authorizer from jwks, rsa, ed, ec or secret 17 | /// - can define a checker (jwt claims check) 18 | pub struct AuthorizerBuilder 19 | where 20 | C: Clone + DeserializeOwned, 21 | { 22 | key_source_type: KeySourceType, 23 | refresh: Option, 24 | claims_checker: Option>, 25 | validation: Option, 26 | jwt_source: JwtSource, 27 | http_client: Option, 28 | } 29 | 30 | /// alias for AuthorizerBuidler (backwards compatibility) 31 | pub type JwtAuthorizer = AuthorizerBuilder; 32 | 33 | /// authorization layer builder 34 | impl AuthorizerBuilder 35 | where 36 | C: Clone + DeserializeOwned + Send + Sync, 37 | { 38 | /// Builds Authorizer Layer from a OpenId Connect discover metadata 39 | pub fn from_oidc(issuer: &str) -> AuthorizerBuilder { 40 | AuthorizerBuilder { 41 | key_source_type: KeySourceType::Discovery(issuer.to_string()), 42 | refresh: Default::default(), 43 | claims_checker: None, 44 | validation: None, 45 | jwt_source: JwtSource::AuthorizationHeader, 46 | http_client: None, 47 | } 48 | } 49 | 50 | /// Builds Authorizer Layer from a JWKS endpoint 51 | pub fn from_jwks_url(url: &str) -> AuthorizerBuilder { 52 | AuthorizerBuilder { 53 | key_source_type: KeySourceType::Jwks(url.to_owned()), 54 | refresh: Default::default(), 55 | claims_checker: None, 56 | validation: None, 57 | jwt_source: JwtSource::AuthorizationHeader, 58 | http_client: None, 59 | } 60 | } 61 | 62 | pub fn from_jwks(path: &str) -> AuthorizerBuilder { 63 | AuthorizerBuilder { 64 | key_source_type: KeySourceType::JwksPath(path.to_owned()), 65 | refresh: Default::default(), 66 | claims_checker: None, 67 | validation: None, 68 | jwt_source: JwtSource::AuthorizationHeader, 69 | http_client: None, 70 | } 71 | } 72 | 73 | pub fn from_jwks_text(text: &str) -> AuthorizerBuilder { 74 | AuthorizerBuilder { 75 | key_source_type: KeySourceType::JwksString(text.to_owned()), 76 | refresh: Default::default(), 77 | claims_checker: None, 78 | validation: None, 79 | jwt_source: JwtSource::AuthorizationHeader, 80 | http_client: None, 81 | } 82 | } 83 | 84 | /// Builds Authorizer Layer from a RSA PEM file 85 | pub fn from_rsa_pem(path: &str) -> AuthorizerBuilder { 86 | AuthorizerBuilder { 87 | key_source_type: KeySourceType::RSA(path.to_owned()), 88 | refresh: Default::default(), 89 | claims_checker: None, 90 | validation: None, 91 | jwt_source: JwtSource::AuthorizationHeader, 92 | http_client: None, 93 | } 94 | } 95 | 96 | /// Builds Authorizer Layer from an RSA PEM raw text 97 | pub fn from_rsa_pem_text(text: &str) -> AuthorizerBuilder { 98 | AuthorizerBuilder { 99 | key_source_type: KeySourceType::RSAString(text.to_owned()), 100 | refresh: Default::default(), 101 | claims_checker: None, 102 | validation: None, 103 | jwt_source: JwtSource::AuthorizationHeader, 104 | http_client: None, 105 | } 106 | } 107 | 108 | /// Builds Authorizer Layer from a EC PEM file 109 | pub fn from_ec_pem(path: &str) -> AuthorizerBuilder { 110 | AuthorizerBuilder { 111 | key_source_type: KeySourceType::EC(path.to_owned()), 112 | refresh: Default::default(), 113 | claims_checker: None, 114 | validation: None, 115 | jwt_source: JwtSource::AuthorizationHeader, 116 | http_client: None, 117 | } 118 | } 119 | 120 | /// Builds Authorizer Layer from a EC PEM raw text 121 | pub fn from_ec_pem_text(text: &str) -> AuthorizerBuilder { 122 | AuthorizerBuilder { 123 | key_source_type: KeySourceType::ECString(text.to_owned()), 124 | refresh: Default::default(), 125 | claims_checker: None, 126 | validation: None, 127 | jwt_source: JwtSource::AuthorizationHeader, 128 | http_client: None, 129 | } 130 | } 131 | 132 | /// Builds Authorizer Layer from a EC PEM file 133 | pub fn from_ed_pem(path: &str) -> AuthorizerBuilder { 134 | AuthorizerBuilder { 135 | key_source_type: KeySourceType::ED(path.to_owned()), 136 | refresh: Default::default(), 137 | claims_checker: None, 138 | validation: None, 139 | jwt_source: JwtSource::AuthorizationHeader, 140 | http_client: None, 141 | } 142 | } 143 | 144 | /// Builds Authorizer Layer from a EC PEM raw text 145 | pub fn from_ed_pem_text(text: &str) -> AuthorizerBuilder { 146 | AuthorizerBuilder { 147 | key_source_type: KeySourceType::EDString(text.to_owned()), 148 | refresh: Default::default(), 149 | claims_checker: None, 150 | validation: None, 151 | jwt_source: JwtSource::AuthorizationHeader, 152 | http_client: None, 153 | } 154 | } 155 | 156 | /// Builds Authorizer Layer from a secret phrase 157 | pub fn from_secret(secret: &str) -> AuthorizerBuilder { 158 | AuthorizerBuilder { 159 | key_source_type: KeySourceType::Secret(secret.to_owned()), 160 | refresh: Default::default(), 161 | claims_checker: None, 162 | validation: None, 163 | jwt_source: JwtSource::AuthorizationHeader, 164 | http_client: None, 165 | } 166 | } 167 | 168 | /// Refreshes configuration for jwk store 169 | pub fn refresh(mut self, refresh: Refresh) -> AuthorizerBuilder { 170 | if self.refresh.is_some() { 171 | tracing::warn!("More than one refresh configuration found!"); 172 | } 173 | self.refresh = Some(refresh); 174 | self 175 | } 176 | 177 | /// no refresh, jwks will be loaded juste once 178 | pub fn no_refresh(mut self) -> AuthorizerBuilder { 179 | if self.refresh.is_some() { 180 | tracing::warn!("More than one refresh configuration found!"); 181 | } 182 | self.refresh = Some(Refresh { 183 | strategy: RefreshStrategy::NoRefresh, 184 | ..Default::default() 185 | }); 186 | self 187 | } 188 | 189 | /// configures token content check (custom function), if false a 403 will be sent. 190 | /// (AuthError::InvalidClaims()) 191 | pub fn check(mut self, checker_fn: F) -> AuthorizerBuilder 192 | where 193 | F: Fn(&C) -> bool + Send + Sync + 'static, 194 | { 195 | self.claims_checker = Some(Arc::new(Box::new(checker_fn))); 196 | 197 | self 198 | } 199 | 200 | pub fn validation(mut self, validation: Validation) -> AuthorizerBuilder { 201 | self.validation = Some(validation); 202 | 203 | self 204 | } 205 | 206 | /// configures the source of the bearer token 207 | /// 208 | /// (default: AuthorizationHeader) 209 | pub fn jwt_source(mut self, src: JwtSource) -> AuthorizerBuilder { 210 | self.jwt_source = src; 211 | 212 | self 213 | } 214 | 215 | /// provide a custom http client for oicd requests 216 | /// if not called, uses a default configured client 217 | /// 218 | /// (default: None) 219 | pub fn http_client(mut self, http_client: Client) -> AuthorizerBuilder { 220 | self.http_client = Some(http_client); 221 | 222 | self 223 | } 224 | 225 | /// Build layer 226 | #[deprecated(since = "0.10.0", note = "please use `IntoLayer::into_layer()` instead")] 227 | pub async fn layer(self) -> Result, InitError> { 228 | let val = self.validation.unwrap_or_default(); 229 | let auth = Arc::new( 230 | Authorizer::build( 231 | self.key_source_type, 232 | self.claims_checker, 233 | self.refresh, 234 | val, 235 | self.jwt_source, 236 | None, 237 | ) 238 | .await?, 239 | ); 240 | Ok(AuthorizationLayer::new(vec![auth])) 241 | } 242 | 243 | pub async fn build(self) -> Result, InitError> { 244 | let val = self.validation.unwrap_or_default(); 245 | 246 | Authorizer::build( 247 | self.key_source_type, 248 | self.claims_checker, 249 | self.refresh, 250 | val, 251 | self.jwt_source, 252 | self.http_client, 253 | ) 254 | .await 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /jwt-authorizer/src/claims.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// The number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time ignoring leap seconds. 4 | /// () 5 | #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, Debug)] 6 | pub struct NumericDate(pub i64); 7 | 8 | /// accesses the underlying value 9 | impl From for i64 { 10 | fn from(t: NumericDate) -> Self { 11 | t.0 12 | } 13 | } 14 | 15 | #[cfg(feature = "chrono")] 16 | use chrono::{DateTime, TimeZone, Utc}; 17 | 18 | #[cfg(feature = "chrono")] 19 | impl From for DateTime { 20 | fn from(t: NumericDate) -> Self { 21 | Utc.timestamp_opt(t.0, 0).unwrap() 22 | } 23 | } 24 | 25 | #[cfg(feature = "time")] 26 | use time::OffsetDateTime; 27 | 28 | #[cfg(feature = "time")] 29 | impl From for OffsetDateTime { 30 | fn from(t: NumericDate) -> Self { 31 | OffsetDateTime::from_unix_timestamp(t.0).unwrap() 32 | } 33 | } 34 | 35 | #[derive(PartialEq, Debug, Clone, Deserialize, Serialize)] 36 | #[serde(untagged)] 37 | pub enum OneOrArray { 38 | One(T), 39 | Array(Vec), 40 | } 41 | 42 | impl OneOrArray { 43 | pub fn iter<'a>(&'a self) -> Box + 'a> { 44 | match self { 45 | OneOrArray::One(v) => Box::new(std::iter::once(v)), 46 | OneOrArray::Array(vector) => Box::new(vector.iter()), 47 | } 48 | } 49 | } 50 | 51 | #[derive(PartialEq, Debug, Clone)] 52 | pub struct StringList(Vec); 53 | 54 | /// Claims mentioned in the JWT specifications. 55 | /// 56 | /// 57 | #[derive(Deserialize, Serialize, Clone, Debug)] 58 | pub struct RegisteredClaims { 59 | #[serde(skip_serializing_if = "Option::is_none")] 60 | pub iss: Option, 61 | #[serde(skip_serializing_if = "Option::is_none")] 62 | pub sub: Option, 63 | #[serde(skip_serializing_if = "Option::is_none")] 64 | pub aud: Option>, 65 | #[serde(skip_serializing_if = "Option::is_none")] 66 | pub exp: Option, 67 | #[serde(skip_serializing_if = "Option::is_none")] 68 | pub nbf: Option, 69 | #[serde(skip_serializing_if = "Option::is_none")] 70 | pub iat: Option, 71 | #[serde(skip_serializing_if = "Option::is_none")] 72 | pub jti: Option, 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | 78 | use chrono::{DateTime, TimeZone, Utc}; 79 | use serde::Deserialize; 80 | use serde_json::json; 81 | 82 | use crate::claims::{NumericDate, OneOrArray, RegisteredClaims}; 83 | 84 | #[derive(Deserialize)] 85 | struct TestStruct { 86 | v: OneOrArray, 87 | } 88 | 89 | #[test] 90 | fn one_or_array_iter() { 91 | let o = OneOrArray::One("aaa".to_owned()); 92 | let mut i = o.iter(); 93 | assert_eq!(Some(&"aaa".to_owned()), i.next()); 94 | 95 | let a = OneOrArray::Array(vec!["aaa".to_owned()]); 96 | let mut i = a.iter(); 97 | assert_eq!(Some(&"aaa".to_owned()), i.next()); 98 | 99 | let a = OneOrArray::Array(vec!["aaa".to_owned(), "bbb".to_owned()]); 100 | let mut i = a.iter(); 101 | assert_eq!(Some(&"aaa".to_owned()), i.next()); 102 | assert_eq!(Some(&"bbb".to_owned()), i.next()); 103 | assert_eq!(None, i.next()); 104 | } 105 | 106 | #[test] 107 | fn rfc_claims_aud() { 108 | let a: TestStruct = serde_json::from_str(r#"{"v":"a"}"#).unwrap(); 109 | assert_eq!(a.v, OneOrArray::One("a".to_owned())); 110 | 111 | let a: TestStruct = serde_json::from_str(r#"{"v":["a", "b"]}"#).unwrap(); 112 | assert_eq!(a.v, OneOrArray::Array(vec!["a".to_owned(), "b".to_owned()])); 113 | } 114 | 115 | #[test] 116 | fn from_numeric_date() { 117 | let exp: i64 = NumericDate(1516239022).into(); 118 | assert_eq!(exp, 1516239022); 119 | } 120 | 121 | #[test] 122 | fn chrono_from_numeric_date() { 123 | let exp: DateTime = NumericDate(1516239022).into(); 124 | assert_eq!(exp, Utc.timestamp_opt(1516239022, 0).unwrap()); 125 | assert_eq!(exp, DateTime::parse_from_rfc3339("2018-01-18T01:30:22.000Z").unwrap()); 126 | } 127 | 128 | #[cfg(feature = "time")] 129 | #[test] 130 | fn time_from_numeric_date() { 131 | use time::macros::datetime; 132 | use time::OffsetDateTime; 133 | 134 | let exp: OffsetDateTime = NumericDate(1516239022).into(); 135 | assert_eq!(exp, datetime!(2018-01-18 01:30:22 UTC)); 136 | } 137 | 138 | #[test] 139 | fn rfc_claims() { 140 | let jwt_json = json!({ 141 | "iss": "http://localhost:3001", 142 | "aud": ["aud1", "aud2"], 143 | "sub": "bob", 144 | "exp": 1516240122, 145 | "iat": 1516239022, 146 | } 147 | ); 148 | 149 | let claims: RegisteredClaims = serde_json::from_value(jwt_json).expect("Failed RfcClaims deserialisation"); 150 | assert_eq!(claims.iss.unwrap(), "http://localhost:3001"); 151 | assert_eq!( 152 | claims.aud.unwrap(), 153 | OneOrArray::Array(vec!["aud1".to_owned(), "aud2".to_owned()]) 154 | ); 155 | assert_eq!(claims.exp.unwrap(), NumericDate(1516240122)); 156 | assert_eq!(claims.nbf, None); 157 | 158 | let dt: DateTime = claims.iat.unwrap().into(); 159 | assert_eq!(dt, Utc.timestamp_opt(1516239022, 0).unwrap()); 160 | } 161 | 162 | #[test] 163 | fn rfc_claims_serde() { 164 | let claims_str = r#"{ 165 | "iss": "http://localhost:3001", 166 | "sub": "bob", 167 | "aud": ["aud1", "aud2"], 168 | "exp": 1516240122, 169 | "iat": 1516239022 170 | }"#; 171 | 172 | let claims: RegisteredClaims = serde_json::from_str(claims_str).expect("Failed RfcClaims deserialisation"); 173 | 174 | let jwt_serd = serde_json::to_string(&claims).unwrap(); 175 | let mut trimed_claims = claims_str.to_owned(); 176 | trimed_claims.retain(|c| !c.is_whitespace()); 177 | assert_eq!(trimed_claims, jwt_serd); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /jwt-authorizer/src/error.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | body::Body, 3 | http::StatusCode, 4 | response::{IntoResponse, Response}, 5 | }; 6 | use http::header; 7 | use jsonwebtoken::Algorithm; 8 | use thiserror::Error; 9 | 10 | use tracing::debug; 11 | 12 | #[derive(Debug, Error)] 13 | pub enum InitError { 14 | #[error("Builder Error {0}")] 15 | BuilderError(String), 16 | 17 | #[error(transparent)] 18 | KeyFileError(#[from] std::io::Error), 19 | 20 | #[error(transparent)] 21 | KeyDecodingError(#[from] jsonwebtoken::errors::Error), 22 | 23 | #[error("Builder Error {0}")] 24 | DiscoveryError(String), 25 | 26 | #[error("Builder Error {0}")] 27 | JwksUrlError(String), 28 | 29 | #[error("Jwks Parsing Error {0}")] 30 | JwksParsingError(#[from] serde_json::Error), 31 | } 32 | 33 | #[derive(Debug, Error)] 34 | pub enum AuthError { 35 | #[error(transparent)] 36 | JwksSerialisationError(#[from] serde_json::Error), 37 | 38 | #[error("JwksRefreshError {0}")] 39 | JwksRefreshError(String), 40 | 41 | #[error("InvalidKey {0}")] 42 | InvalidKey(String), 43 | 44 | #[error("Invalid Kid {0}")] 45 | InvalidKid(String), 46 | 47 | #[error("Invalid Key Algorithm {0:?}")] 48 | InvalidKeyAlg(Algorithm), 49 | 50 | #[error("Missing Token")] 51 | MissingToken(), 52 | 53 | #[error(transparent)] 54 | InvalidToken(#[from] jsonwebtoken::errors::Error), 55 | 56 | #[error("Invalid Claim")] 57 | InvalidClaims(), 58 | 59 | #[error("No Authorizer")] 60 | NoAuthorizer(), 61 | 62 | /// Used when a claim extractor is used and no authorization layer is in front the handler 63 | #[error("No Authorizer Layer")] 64 | NoAuthorizerLayer(), 65 | } 66 | 67 | fn response_wwwauth(status: StatusCode, bearer: &str) -> Response { 68 | let mut res = Response::new(Body::empty()); 69 | *res.status_mut() = status; 70 | let h = if bearer.is_empty() { 71 | "Bearer".to_owned() 72 | } else { 73 | format!("Bearer {bearer}") 74 | }; 75 | res.headers_mut().insert(header::WWW_AUTHENTICATE, h.parse().unwrap()); 76 | 77 | res 78 | } 79 | 80 | fn response_500() -> Response { 81 | let mut res = Response::new(Body::empty()); 82 | *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; 83 | 84 | res 85 | } 86 | 87 | #[cfg(feature = "tonic")] 88 | impl From for Response { 89 | fn from(e: AuthError) -> Self { 90 | match e { 91 | AuthError::JwksRefreshError(err) => { 92 | tracing::error!("AuthErrors::JwksRefreshError: {}", err); 93 | tonic::Status::internal("") 94 | } 95 | AuthError::InvalidKey(err) => { 96 | tracing::error!("AuthErrors::InvalidKey: {}", err); 97 | tonic::Status::internal("") 98 | } 99 | AuthError::JwksSerialisationError(err) => { 100 | tracing::error!("AuthErrors::JwksSerialisationError: {}", err); 101 | tonic::Status::internal("") 102 | } 103 | AuthError::InvalidKeyAlg(err) => { 104 | debug!("AuthErrors::InvalidKeyAlg: {:?}", err); 105 | tonic::Status::unauthenticated("error=\"invalid_token\", error_description=\"invalid key algorithm\"") 106 | } 107 | AuthError::InvalidKid(err) => { 108 | debug!("AuthErrors::InvalidKid: {}", err); 109 | tonic::Status::unauthenticated("error=\"invalid_token\", error_description=\"invalid kid\"") 110 | } 111 | AuthError::InvalidToken(err) => { 112 | debug!("AuthErrors::InvalidToken: {}", err); 113 | tonic::Status::unauthenticated("error=\"invalid_token\"") 114 | } 115 | AuthError::MissingToken() => { 116 | debug!("AuthErrors::MissingToken"); 117 | tonic::Status::unauthenticated("") 118 | } 119 | AuthError::InvalidClaims() => { 120 | debug!("AuthErrors::InvalidClaims"); 121 | tonic::Status::unauthenticated("error=\"insufficient_scope\"") 122 | } 123 | AuthError::NoAuthorizer() => { 124 | debug!("AuthErrors::NoAuthorizer"); 125 | tonic::Status::unauthenticated("error=\"invalid_token\"") 126 | } 127 | AuthError::NoAuthorizerLayer() => { 128 | debug!("AuthErrors::NoAuthorizerLayer"); 129 | tonic::Status::unauthenticated("error=\"no_authorizer_layer\"") 130 | } 131 | } 132 | .into_http() 133 | } 134 | } 135 | 136 | impl From for Response { 137 | fn from(e: AuthError) -> Self { 138 | e.into_response() 139 | } 140 | } 141 | 142 | /// () 143 | impl IntoResponse for AuthError { 144 | fn into_response(self) -> Response { 145 | match self { 146 | AuthError::JwksRefreshError(err) => { 147 | tracing::error!("AuthErrors::JwksRefreshError: {}", err); 148 | response_500() 149 | } 150 | AuthError::InvalidKey(err) => { 151 | tracing::error!("AuthErrors::InvalidKey: {}", err); 152 | response_500() 153 | } 154 | AuthError::JwksSerialisationError(err) => { 155 | tracing::error!("AuthErrors::JwksSerialisationError: {}", err); 156 | response_500() 157 | } 158 | AuthError::InvalidKeyAlg(err) => { 159 | debug!("AuthErrors::InvalidKeyAlg: {:?}", err); 160 | response_wwwauth( 161 | StatusCode::UNAUTHORIZED, 162 | "error=\"invalid_token\", error_description=\"invalid key algorithm\"", 163 | ) 164 | } 165 | AuthError::InvalidKid(err) => { 166 | debug!("AuthErrors::InvalidKid: {}", err); 167 | response_wwwauth( 168 | StatusCode::UNAUTHORIZED, 169 | "error=\"invalid_token\", error_description=\"invalid kid\"", 170 | ) 171 | } 172 | AuthError::InvalidToken(err) => { 173 | debug!("AuthErrors::InvalidToken: {}", err); 174 | response_wwwauth(StatusCode::UNAUTHORIZED, "error=\"invalid_token\"") 175 | } 176 | AuthError::MissingToken() => { 177 | debug!("AuthErrors::MissingToken"); 178 | response_wwwauth(StatusCode::UNAUTHORIZED, "") 179 | } 180 | AuthError::InvalidClaims() => { 181 | debug!("AuthErrors::InvalidClaims"); 182 | response_wwwauth(StatusCode::FORBIDDEN, "error=\"insufficient_scope\"") 183 | } 184 | AuthError::NoAuthorizer() => { 185 | debug!("AuthErrors::NoAuthorizer"); 186 | response_wwwauth(StatusCode::FORBIDDEN, "error=\"invalid_token\"") 187 | } 188 | AuthError::NoAuthorizerLayer() => { 189 | debug!("AuthErrors::NoAuthorizerLayer"); 190 | // TODO: should it be a standard error? 191 | response_wwwauth(StatusCode::UNAUTHORIZED, "error=\"no_authorizer_layer\"") 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /jwt-authorizer/src/jwks/key_store_manager.rs: -------------------------------------------------------------------------------- 1 | use jsonwebtoken::{jwk::JwkSet, Algorithm}; 2 | use reqwest::Url; 3 | use std::{ 4 | sync::Arc, 5 | time::{Duration, Instant}, 6 | }; 7 | use tokio::sync::Mutex; 8 | 9 | use crate::error::AuthError; 10 | 11 | use super::{KeyData, KeySet}; 12 | 13 | /// Defines the strategy for the JWKS refresh. 14 | #[derive(Clone)] 15 | pub enum RefreshStrategy { 16 | /// refresh periodicaly 17 | Interval, 18 | 19 | /// refresh when kid not found in the store 20 | KeyNotFound, 21 | 22 | /// loading is triggered only once by the first use 23 | NoRefresh, 24 | } 25 | 26 | /// JWKS Refresh configuration 27 | #[derive(Clone)] 28 | pub struct Refresh { 29 | pub strategy: RefreshStrategy, 30 | /// After the refresh interval the store will/can be refreshed. 31 | /// 32 | /// - RefreshStrategy::KeyNotFound - refresh will be performed only if a kid is not found in the store 33 | /// (if no kid is in the token header the alg is looked up) 34 | /// - RefreshStrategy::Interval - refresh will be performed each time the refresh interval has elapsed 35 | /// (before checking a new token -> lazy behaviour) 36 | pub refresh_interval: Duration, 37 | /// don't refresh before (after an error or jwks is unawailable) 38 | /// (we let a little bit of time to the jwks endpoint to recover) 39 | pub retry_interval: Duration, 40 | } 41 | 42 | impl Default for Refresh { 43 | fn default() -> Self { 44 | Self { 45 | strategy: RefreshStrategy::KeyNotFound, 46 | refresh_interval: Duration::from_secs(600), 47 | retry_interval: Duration::from_secs(10), 48 | } 49 | } 50 | } 51 | 52 | #[derive(Clone)] 53 | pub struct KeyStoreManager { 54 | key_url: Url, 55 | /// in case of fail loading (error or key not found), minimal interval 56 | refresh: Refresh, 57 | keystore: Arc>, 58 | } 59 | 60 | pub struct KeyStore { 61 | /// key set 62 | keys: KeySet, 63 | /// time of the last successfully loaded jwkset 64 | load_time: Option, 65 | /// time of the last failed load 66 | fail_time: Option, 67 | } 68 | 69 | impl KeyStoreManager { 70 | pub(crate) fn new(key_url: Url, refresh: Refresh) -> KeyStoreManager { 71 | KeyStoreManager { 72 | key_url, 73 | refresh, 74 | keystore: Arc::new(Mutex::new(KeyStore { 75 | keys: KeySet::default(), 76 | load_time: None, 77 | fail_time: None, 78 | })), 79 | } 80 | } 81 | 82 | pub(crate) async fn get_key(&self, header: &jsonwebtoken::Header) -> Result, AuthError> { 83 | let kstore = self.keystore.clone(); 84 | let mut ks_gard = kstore.lock().await; 85 | let key = match self.refresh.strategy { 86 | RefreshStrategy::Interval => { 87 | if ks_gard.can_refresh(self.refresh.refresh_interval, self.refresh.retry_interval) { 88 | ks_gard.refresh(&self.key_url, &[]).await?; 89 | } 90 | ks_gard.get_key(header)? 91 | } 92 | RefreshStrategy::KeyNotFound => { 93 | if let Some(ref kid) = header.kid { 94 | let jwk_opt = ks_gard.find_kid(kid); 95 | if let Some(jwk) = jwk_opt { 96 | jwk 97 | } else if ks_gard.can_refresh(self.refresh.refresh_interval, self.refresh.retry_interval) { 98 | ks_gard.refresh(&self.key_url, &[("kid", kid)]).await?; 99 | ks_gard.find_kid(kid).ok_or_else(|| AuthError::InvalidKid(kid.to_owned()))? 100 | } else { 101 | return Err(AuthError::InvalidKid(kid.to_owned())); 102 | } 103 | } else { 104 | let jwk_opt = ks_gard.find_alg(&header.alg); 105 | if let Some(jwk) = jwk_opt { 106 | jwk 107 | } else if ks_gard.can_refresh(self.refresh.refresh_interval, self.refresh.retry_interval) { 108 | ks_gard 109 | .refresh( 110 | &self.key_url, 111 | &[( 112 | "alg", 113 | &serde_json::to_string(&header.alg).map_err(|_| AuthError::InvalidKeyAlg(header.alg))?, 114 | )], 115 | ) 116 | .await?; 117 | ks_gard 118 | .find_alg(&header.alg) 119 | .ok_or_else(|| AuthError::InvalidKeyAlg(header.alg))? 120 | } else { 121 | return Err(AuthError::InvalidKeyAlg(header.alg)); 122 | } 123 | } 124 | } 125 | RefreshStrategy::NoRefresh => { 126 | if ks_gard.load_time.is_none() 127 | // if jwks endpoint is down for the loading, respect retry_interval 128 | && ks_gard.can_refresh(self.refresh.refresh_interval, self.refresh.retry_interval) 129 | { 130 | ks_gard.refresh(&self.key_url, &[]).await?; 131 | } 132 | ks_gard.get_key(header)? 133 | } 134 | }; 135 | Ok(key.clone()) 136 | } 137 | } 138 | 139 | impl KeyStore { 140 | fn can_refresh(&self, refresh_interval: Duration, minimal_retry: Duration) -> bool { 141 | if let Some(fail_tm) = self.fail_time { 142 | if let Some(load_tm) = self.load_time { 143 | fail_tm.elapsed() > minimal_retry && load_tm.elapsed() > refresh_interval 144 | } else { 145 | fail_tm.elapsed() > minimal_retry 146 | } 147 | } else if let Some(load_tm) = self.load_time { 148 | load_tm.elapsed() > refresh_interval 149 | } else { 150 | true 151 | } 152 | } 153 | 154 | async fn refresh(&mut self, key_url: &Url, qparam: &[(&str, &str)]) -> Result<(), AuthError> { 155 | reqwest::Client::new() 156 | .get(key_url.as_ref()) 157 | .query(qparam) 158 | .send() 159 | .await 160 | .map_err(|e| { 161 | self.fail_time = Some(Instant::now()); 162 | AuthError::JwksRefreshError(e.to_string()) 163 | })? 164 | .error_for_status() 165 | .map_err(|e| { 166 | self.fail_time = Some(Instant::now()); 167 | AuthError::JwksRefreshError(e.to_string()) 168 | })? 169 | .json::() 170 | .await 171 | .map(|jwks| { 172 | self.load_time = Some(Instant::now()); 173 | // self.jwks = jwks; 174 | let mut keys: Vec> = Vec::with_capacity(jwks.keys.len()); 175 | for jwk in jwks.keys { 176 | match KeyData::from_jwk(&jwk) { 177 | Ok(kdata) => keys.push(Arc::new(kdata)), 178 | Err(err) => { 179 | tracing::warn!("Jwk decoding error, the key will be ignored! ({})", err); 180 | } 181 | }; 182 | } 183 | if keys.is_empty() { 184 | Err(AuthError::JwksRefreshError("No valid keys in the Jwk Set!".to_owned())) 185 | } else { 186 | self.keys = keys.into(); 187 | self.fail_time = None; 188 | Ok(()) 189 | } 190 | }) 191 | .map_err(|e| { 192 | self.fail_time = Some(Instant::now()); 193 | AuthError::JwksRefreshError(e.to_string()) 194 | })? 195 | } 196 | 197 | /// Find the key in the set that matches the given key id, if any. 198 | pub fn find_kid(&self, kid: &str) -> Option<&Arc> { 199 | self.keys.find_kid(kid) 200 | } 201 | 202 | /// Find the key in the set that matches the given key id, if any. 203 | pub fn find_alg(&self, alg: &Algorithm) -> Option<&Arc> { 204 | self.keys.find_alg(alg) 205 | } 206 | 207 | fn get_key(&self, header: &jsonwebtoken::Header) -> Result<&Arc, AuthError> { 208 | self.keys.get_key(header) 209 | } 210 | 211 | /// Find first key. 212 | pub fn find_first(&self) -> Option<&Arc> { 213 | self.keys.first() 214 | } 215 | } 216 | 217 | #[cfg(test)] 218 | mod tests { 219 | use std::sync::Arc; 220 | use std::time::{Duration, Instant}; 221 | 222 | use jsonwebtoken::Algorithm; 223 | use jsonwebtoken::{jwk::Jwk, Header}; 224 | use reqwest::Url; 225 | use wiremock::{ 226 | matchers::{method, path}, 227 | Mock, MockServer, ResponseTemplate, 228 | }; 229 | 230 | use crate::jwks::key_store_manager::{KeyStore, KeyStoreManager}; 231 | use crate::jwks::{KeyData, KeySet}; 232 | use crate::{Refresh, RefreshStrategy}; 233 | 234 | const JWK_RSA01: &str = r#"{ 235 | "kty": "RSA", 236 | "n": "2pQeZdxa7q093K7bj5h6-leIpxfTnuAxzXdhjfGEJHxmt2ekHyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy_tw-5e-Px1xFj1PykGEkRlOpYSAeWsNaAWvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV-fcGGLhJnXl0-5_z7tKC7RvBoT3SGwlc_AmJqpFtTpEBn_fDnyqiZbpcjXYLExFpExm41xDitRKHWIwfc3dV8_vlNntlxCPGy_THkjdXJoHv2IJmlhvmr5_h03iGMLWDKSywxOol_4Wc1BT7Hb6byMxW40GKwSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO-XiVShZRVg7JeraGAfWwaIgIJ1D8C1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4NDX-A4KRMgaxcfAcui_x6gybksq6gF90-9nfQfmVMVJctZ6M-FvRr-itd1Nef5WAtwUp1qyZygAXU3cH3rarscajmurOsP6dE1OHl3grY_eZhQxk33VBK9lavqNKPg6Q_PLiq1ojbYBj3bcYifJrsNeQwxldQP83aWt5rGtgZTehKVJwa40Uy_Grae1iRnsDtdSy5sTJIJ6EiShnWAdMoGejdiI8vpkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPWc6JEotQqI0HwhzU0KHyoY4s", 237 | "e": "AQAB", 238 | "kid": "rsa01", 239 | "alg": "RS256", 240 | "use": "sig" 241 | }"#; 242 | 243 | const JWK_ED01: &str = r#"{ 244 | "kty": "OKP", 245 | "use": "sig", 246 | "crv": "Ed25519", 247 | "x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk", 248 | "kid": "ed01", 249 | "alg": "EdDSA" 250 | }"#; 251 | 252 | const JWK_ED02: &str = r#"{ 253 | "kty": "OKP", 254 | "use": "sig", 255 | "crv": "Ed25519", 256 | "x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk", 257 | "kid": "ed02", 258 | "alg": "EdDSA" 259 | }"#; 260 | 261 | const JWK_EC01: &str = r#"{ 262 | "kty": "EC", 263 | "crv": "P-256", 264 | "x": "w7JAoU_gJbZJvV-zCOvU9yFJq0FNC_edCMRM78P8eQQ", 265 | "y": "wQg1EytcsEmGrM70Gb53oluoDbVhCZ3Uq3hHMslHVb4", 266 | "kid": "ec01", 267 | "alg": "ES256", 268 | "use": "sig" 269 | }"#; 270 | 271 | const JWK_EC02: &str = r#"{ 272 | "kty": "EC", 273 | "crv": "P-256", 274 | "x": "w7JAoU_gJbZJvV-zCOvU9yFJq0FNC_edCMRM78P8eQQ", 275 | "y": "wQg1EytcsEmGrM70Gb53oluoDbVhCZ3Uq3hHMslHVb4", 276 | "kid": "ec02", 277 | "alg": "ES256", 278 | "use": "sig" 279 | }"#; 280 | 281 | #[test] 282 | fn keystore_can_refresh() { 283 | // FAIL, NO LOAD 284 | let ks = KeyStore { 285 | keys: KeySet::default(), 286 | fail_time: Instant::now().checked_sub(Duration::from_secs(5)), 287 | load_time: None, 288 | }; 289 | assert!(ks.can_refresh(Duration::from_secs(0), Duration::from_secs(4))); 290 | assert!(!ks.can_refresh(Duration::from_secs(0), Duration::from_secs(6))); 291 | 292 | // NO FAIL, LOAD 293 | let ks = KeyStore { 294 | keys: KeySet::default(), 295 | fail_time: None, 296 | load_time: Instant::now().checked_sub(Duration::from_secs(5)), 297 | }; 298 | assert!(ks.can_refresh(Duration::from_secs(4), Duration::from_secs(0))); 299 | assert!(!ks.can_refresh(Duration::from_secs(6), Duration::from_secs(0))); 300 | 301 | // FAIL, LOAD 302 | let ks = KeyStore { 303 | keys: KeySet::default(), 304 | fail_time: Instant::now().checked_sub(Duration::from_secs(5)), 305 | load_time: Instant::now().checked_sub(Duration::from_secs(10)), 306 | }; 307 | assert!(ks.can_refresh(Duration::from_secs(6), Duration::from_secs(4))); 308 | assert!(!ks.can_refresh(Duration::from_secs(11), Duration::from_secs(4))); 309 | assert!(!ks.can_refresh(Duration::from_secs(6), Duration::from_secs(6))); 310 | } 311 | 312 | #[test] 313 | fn find_kid() { 314 | let jwk0: Jwk = serde_json::from_str(JWK_RSA01).unwrap(); 315 | let jwk1: Jwk = serde_json::from_str(JWK_EC01).unwrap(); 316 | let ks = KeyStore { 317 | load_time: None, 318 | fail_time: None, 319 | keys: vec![ 320 | Arc::new(KeyData::from_jwk(&jwk0).unwrap()), 321 | Arc::new(KeyData::from_jwk(&jwk1).unwrap()), 322 | ] 323 | .into(), 324 | }; 325 | assert!(ks.find_kid("rsa01").is_some()); 326 | assert!(ks.find_kid("ec01").is_some()); 327 | assert!(ks.find_kid("3").is_none()); 328 | } 329 | 330 | #[test] 331 | fn find_alg() { 332 | let jwk0: Jwk = serde_json::from_str(JWK_RSA01).unwrap(); 333 | let ks = KeyStore { 334 | load_time: None, 335 | fail_time: None, 336 | keys: vec![Arc::new(KeyData::from_jwk(&jwk0).unwrap())].into(), 337 | }; 338 | assert!(ks.find_alg(&Algorithm::RS256).is_some()); 339 | assert!(ks.find_alg(&Algorithm::EdDSA).is_none()); 340 | } 341 | 342 | async fn mock_jwks_response_once(mock_server: &MockServer, jwk: &str) { 343 | let jwk0: Jwk = serde_json::from_str(jwk).unwrap(); 344 | let jwks = jsonwebtoken::jwk::JwkSet { keys: vec![jwk0] }; 345 | Mock::given(method("GET")) 346 | .and(path("/")) 347 | .respond_with(ResponseTemplate::new(200).set_body_json(&jwks)) 348 | .expect(1) 349 | .mount(mock_server) 350 | .await; 351 | } 352 | 353 | async fn mock_jwks_response_fail_once(mock_server: &MockServer) { 354 | Mock::given(method("GET")) 355 | .and(path("/")) 356 | .respond_with(ResponseTemplate::new(500)) 357 | .expect(1) 358 | .mount(mock_server) 359 | .await; 360 | } 361 | 362 | fn build_header(kid: &str, alg: Algorithm) -> Header { 363 | let mut header = Header::new(alg); 364 | header.kid = Some(kid.to_owned()); 365 | header 366 | } 367 | 368 | #[tokio::test] 369 | async fn strategy_interval() { 370 | let mock_server = MockServer::start().await; 371 | mock_jwks_response_once(&mock_server, JWK_ED01).await; 372 | 373 | let ksm = KeyStoreManager::new( 374 | Url::parse(&mock_server.uri()).unwrap(), 375 | Refresh { 376 | strategy: RefreshStrategy::Interval, 377 | refresh_interval: Duration::from_millis(10), 378 | retry_interval: Duration::from_millis(5), 379 | }, 380 | ); 381 | 382 | // 1st RELOAD 383 | let r = ksm.get_key(&Header::new(Algorithm::EdDSA)).await; 384 | assert!(r.is_ok()); 385 | mock_server.verify().await; 386 | 387 | // NO RELOAD - inteval not elapsed 388 | assert!(ksm.get_key(&Header::new(Algorithm::EdDSA)).await.is_ok()); 389 | 390 | // RELOAD - interval elapsed 391 | mock_server.reset().await; 392 | tokio::time::sleep(Duration::from_millis(11)).await; 393 | mock_jwks_response_once(&mock_server, JWK_ED01).await; 394 | assert!(ksm.get_key(&Header::new(Algorithm::EdDSA)).await.is_ok()); 395 | mock_server.verify().await; 396 | 397 | // RELOAD - with fail 398 | mock_server.reset().await; 399 | tokio::time::sleep(Duration::from_millis(11)).await; 400 | mock_jwks_response_fail_once(&mock_server).await; 401 | assert!(ksm.get_key(&Header::new(Algorithm::EdDSA)).await.is_err()); 402 | mock_server.verify().await; 403 | 404 | // NO RELOAD - retry not ellapsed 405 | assert!(ksm.get_key(&Header::new(Algorithm::EdDSA)).await.is_ok()); 406 | 407 | // RELOAD - retry elapsed 408 | mock_server.reset().await; 409 | tokio::time::sleep(Duration::from_millis(6)).await; 410 | mock_jwks_response_once(&mock_server, JWK_ED01).await; 411 | assert!(ksm.get_key(&Header::new(Algorithm::EdDSA)).await.is_ok()); 412 | mock_server.verify().await; 413 | } 414 | 415 | #[tokio::test] 416 | async fn strategy_key_not_found_with_refresh() { 417 | let mock_server = MockServer::start().await; 418 | mock_jwks_response_once(&mock_server, JWK_ED01).await; 419 | 420 | let ksm = KeyStoreManager::new( 421 | Url::parse(&mock_server.uri()).unwrap(), 422 | Refresh { 423 | strategy: RefreshStrategy::KeyNotFound, 424 | refresh_interval: Duration::from_millis(10), 425 | retry_interval: Duration::from_millis(5), 426 | }, 427 | ); 428 | 429 | // STEP 1: initial (lazy) reloading 430 | let r = ksm.get_key(&build_header("ed01", Algorithm::EdDSA)).await; 431 | assert!(r.is_ok()); 432 | mock_server.verify().await; 433 | 434 | // STEP2: new kid, < refresh_interval -> reloading ksm 435 | mock_server.reset().await; 436 | mock_jwks_response_once(&mock_server, JWK_ED02).await; 437 | let h = build_header("ed02", Algorithm::EdDSA); 438 | assert!(ksm.get_key(&h).await.is_err()); 439 | 440 | // ksm.refresh.refresh_interval = Duration::from_millis(10); 441 | tokio::time::sleep(Duration::from_millis(11)).await; 442 | assert!(ksm.get_key(&h).await.is_ok()); 443 | 444 | mock_server.verify().await; 445 | 446 | // STEP3: new algorithm -> try to reload 447 | mock_server.reset().await; 448 | mock_jwks_response_once(&mock_server, JWK_EC01).await; 449 | let h = Header::new(Algorithm::ES256); 450 | assert!(ksm.get_key(&h).await.is_err()); 451 | 452 | tokio::time::sleep(Duration::from_millis(11)).await; 453 | assert!(ksm.get_key(&h).await.is_ok()); 454 | 455 | mock_server.verify().await; 456 | 457 | // STEP4: new key, refresh elapsed, FAIL 458 | mock_server.reset().await; 459 | tokio::time::sleep(Duration::from_millis(11)).await; 460 | mock_jwks_response_fail_once(&mock_server).await; 461 | let h = build_header("ec02", Algorithm::EdDSA); 462 | assert!(ksm.get_key(&h).await.is_err()); 463 | mock_server.verify().await; 464 | 465 | // STEP5: retry elapsed -> reload 466 | mock_server.reset().await; 467 | tokio::time::sleep(Duration::from_millis(6)).await; 468 | mock_jwks_response_once(&mock_server, JWK_EC02).await; 469 | let h = build_header("ec02", Algorithm::EdDSA); 470 | assert!(ksm.get_key(&h).await.is_ok()); 471 | mock_server.verify().await; 472 | } 473 | 474 | #[tokio::test] 475 | async fn strategy_no_refresh() { 476 | let mock_server = MockServer::start().await; 477 | mock_jwks_response_once(&mock_server, JWK_ED01).await; 478 | 479 | let ksm = KeyStoreManager::new( 480 | Url::parse(&mock_server.uri()).unwrap(), 481 | Refresh { 482 | strategy: RefreshStrategy::NoRefresh, 483 | ..Default::default() 484 | }, 485 | ); 486 | 487 | // STEP 1: initial (lazy) reloading 488 | let r = ksm.get_key(&build_header("ed01", Algorithm::EdDSA)).await; 489 | assert!(r.is_ok()); 490 | mock_server.verify().await; 491 | 492 | // STEP2: new kid -> reloading ksm 493 | let h = build_header("ed02", Algorithm::EdDSA); 494 | assert!(ksm.get_key(&h).await.is_err()); 495 | 496 | mock_server.verify().await; 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /jwt-authorizer/src/jwks/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, sync::Arc}; 2 | 3 | use jsonwebtoken::{ 4 | jwk::{AlgorithmParameters, Jwk}, 5 | Algorithm, DecodingKey, Header, 6 | }; 7 | 8 | use crate::error::AuthError; 9 | 10 | use self::key_store_manager::KeyStoreManager; 11 | 12 | pub mod key_store_manager; 13 | 14 | #[derive(Clone)] 15 | pub enum KeySource { 16 | /// KeyDataSource managing a refreshable key sets 17 | KeyStoreSource(KeyStoreManager), 18 | /// Manages public key sets, initialized on startup 19 | MultiKeySource(KeySet), 20 | /// Manages one public key, initialized on startup 21 | SingleKeySource(Arc), 22 | } 23 | 24 | #[derive(Clone)] 25 | pub struct KeyData { 26 | pub kid: Option, 27 | /// valid algorithms 28 | pub algs: Vec, 29 | pub key: DecodingKey, 30 | } 31 | 32 | fn get_valid_algs(key: &Jwk) -> Vec { 33 | if let Some(key_alg) = key.common.key_algorithm { 34 | // if alg is not correct => no valid algs => empty array 35 | Algorithm::from_str(key_alg.to_string().as_str()).map_or(vec![], |a| vec![a]) 36 | } else { 37 | // guessing valid algs from key structure 38 | match key.algorithm { 39 | AlgorithmParameters::EllipticCurve(_) => { 40 | vec![Algorithm::ES256, Algorithm::ES384] 41 | } 42 | AlgorithmParameters::RSA(_) => vec![ 43 | Algorithm::RS256, 44 | Algorithm::RS384, 45 | Algorithm::RS512, 46 | Algorithm::PS256, 47 | Algorithm::PS384, 48 | Algorithm::PS512, 49 | ], 50 | AlgorithmParameters::OctetKey(_) => vec![Algorithm::EdDSA], 51 | AlgorithmParameters::OctetKeyPair(_) => vec![Algorithm::HS256, Algorithm::HS384, Algorithm::HS512], 52 | } 53 | } 54 | } 55 | 56 | impl KeyData { 57 | pub fn from_jwk(key: &Jwk) -> Result { 58 | Ok(KeyData { 59 | kid: key.common.key_id.clone(), 60 | algs: get_valid_algs(key), 61 | key: DecodingKey::from_jwk(key)?, 62 | }) 63 | } 64 | } 65 | 66 | #[derive(Clone, Default)] 67 | pub struct KeySet(Vec>); 68 | 69 | impl From>> for KeySet { 70 | fn from(value: Vec>) -> Self { 71 | KeySet(value) 72 | } 73 | } 74 | 75 | impl KeySet { 76 | /// Find the key in the set that matches the given key id, if any. 77 | pub fn find_kid(&self, kid: &str) -> Option<&Arc> { 78 | self.0.iter().find(|k| match &k.kid { 79 | Some(k) => k == kid, 80 | None => false, 81 | }) 82 | } 83 | 84 | /// Find the key in the set that matches the given key id, if any. 85 | pub fn find_alg(&self, alg: &Algorithm) -> Option<&Arc> { 86 | self.0.iter().find(|k| k.algs.contains(alg)) 87 | } 88 | 89 | /// Find first key. 90 | pub fn first(&self) -> Option<&Arc> { 91 | self.0.first() 92 | } 93 | 94 | pub(crate) fn get_key(&self, header: &Header) -> Result<&Arc, AuthError> { 95 | let key = if let Some(ref kid) = header.kid { 96 | self.find_kid(kid).ok_or_else(|| AuthError::InvalidKid(kid.to_owned()))? 97 | } else { 98 | self.find_alg(&header.alg).ok_or(AuthError::InvalidKeyAlg(header.alg))? 99 | }; 100 | Ok(key) 101 | } 102 | } 103 | 104 | impl KeySource { 105 | pub async fn get_key(&self, header: Header) -> Result, AuthError> { 106 | match self { 107 | KeySource::KeyStoreSource(kstore) => kstore.get_key(&header).await, 108 | KeySource::MultiKeySource(keys) => keys.get_key(&header).cloned(), 109 | KeySource::SingleKeySource(key) => Ok(key.clone()), 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /jwt-authorizer/src/layer.rs: -------------------------------------------------------------------------------- 1 | use futures_core::ready; 2 | use futures_util::future::{self, BoxFuture}; 3 | use http::Request; 4 | use jsonwebtoken::TokenData; 5 | use pin_project::pin_project; 6 | use serde::de::DeserializeOwned; 7 | use std::future::Future; 8 | use std::pin::Pin; 9 | use std::sync::Arc; 10 | use std::task::{Context, Poll}; 11 | use tower_layer::Layer; 12 | use tower_service::Service; 13 | 14 | use crate::authorizer::Authorizer; 15 | use crate::AuthError; 16 | 17 | /// Trait for authorizing requests. 18 | pub trait Authorize { 19 | type Future: Future, AuthError>>; 20 | 21 | /// Authorize the request. 22 | /// 23 | /// If the future resolves to `Ok(request)` then the request is allowed through, otherwise not. 24 | fn authorize(&self, request: Request) -> Self::Future; 25 | } 26 | 27 | impl Authorize for AuthorizationService 28 | where 29 | B: Send + 'static, 30 | C: Clone + DeserializeOwned + Send + Sync + 'static, 31 | { 32 | type Future = BoxFuture<'static, Result, AuthError>>; 33 | 34 | /// The authorizers are sequentially applied (check_auth) until one of them validates the token. 35 | /// If no authorizer validates the token the request is rejected. 36 | /// 37 | fn authorize(&self, mut request: Request) -> Self::Future { 38 | let tkns_auths: Vec<(String, Arc>)> = self 39 | .auths 40 | .iter() 41 | .filter_map(|a| a.extract_token(request.headers()).map(|t| (t, a.clone()))) 42 | .collect(); 43 | 44 | if tkns_auths.is_empty() { 45 | return Box::pin(future::ready(Err(AuthError::MissingToken()))); 46 | } 47 | 48 | Box::pin(async move { 49 | let mut token_data: Result, AuthError> = Err(AuthError::NoAuthorizer()); 50 | for (token, auth) in tkns_auths { 51 | token_data = auth.check_auth(token.as_str()).await; 52 | if token_data.is_ok() { 53 | break; 54 | } 55 | } 56 | match token_data { 57 | Ok(tdata) => { 58 | // Set `token_data` as a request extension so it can be accessed by other 59 | // services down the stack. 60 | 61 | request.extensions_mut().insert(tdata); 62 | 63 | Ok(request) 64 | } 65 | Err(err) => Err(err), // TODO: error containing all errors (not just the last one) or to choose one? 66 | } 67 | }) 68 | } 69 | } 70 | 71 | // -------------- Layer ----------------- 72 | 73 | #[derive(Clone)] 74 | pub struct AuthorizationLayer 75 | where 76 | C: Clone + DeserializeOwned + Send, 77 | { 78 | auths: Vec>>, 79 | } 80 | 81 | impl AuthorizationLayer 82 | where 83 | C: Clone + DeserializeOwned + Send, 84 | { 85 | pub fn new(auths: Vec>>) -> AuthorizationLayer { 86 | Self { auths } 87 | } 88 | } 89 | 90 | impl Layer for AuthorizationLayer 91 | where 92 | C: Clone + DeserializeOwned + Send + Sync, 93 | { 94 | type Service = AuthorizationService; 95 | 96 | fn layer(&self, inner: S) -> Self::Service { 97 | AuthorizationService::new(inner, self.auths.clone()) 98 | } 99 | } 100 | 101 | // ---------- AuthorizationService -------- 102 | 103 | /// Source of the bearer token 104 | #[derive(Clone)] 105 | pub enum JwtSource { 106 | /// Storing the bearer token in Authorization header 107 | /// 108 | /// (default) 109 | AuthorizationHeader, 110 | /// Cookies 111 | /// 112 | /// (be careful when using cookies, some precautions must be taken, cf. RFC6750) 113 | Cookie(String), 114 | // TODO: "Form-Encoded Content Parameter" may be added in the future (OAuth 2.1 / 5.2.1.2) 115 | // FormParam, 116 | } 117 | 118 | #[derive(Clone)] 119 | pub struct AuthorizationService 120 | where 121 | C: Clone + DeserializeOwned + Send, 122 | { 123 | pub inner: S, 124 | pub auths: Vec>>, 125 | } 126 | 127 | impl AuthorizationService 128 | where 129 | C: Clone + DeserializeOwned + Send, 130 | { 131 | pub fn get_ref(&self) -> &S { 132 | &self.inner 133 | } 134 | 135 | /// Gets a mutable reference to the underlying service. 136 | pub fn get_mut(&mut self) -> &mut S { 137 | &mut self.inner 138 | } 139 | 140 | /// Consumes `self`, returning the underlying service. 141 | pub fn into_inner(self) -> S { 142 | self.inner 143 | } 144 | } 145 | 146 | impl AuthorizationService 147 | where 148 | C: Clone + DeserializeOwned + Send + Sync, 149 | { 150 | /// Authorize requests using a custom scheme. 151 | /// 152 | /// The `Authorization` header is required to have the value provided. 153 | pub fn new(inner: S, auths: Vec>>) -> AuthorizationService { 154 | Self { inner, auths } 155 | } 156 | } 157 | 158 | impl Service> for AuthorizationService 159 | where 160 | B: Send + 'static, 161 | S: Service> + Clone, 162 | S::Response: From, 163 | C: Clone + DeserializeOwned + Send + Sync + 'static, 164 | { 165 | type Response = S::Response; 166 | type Error = S::Error; 167 | type Future = ResponseFuture; 168 | 169 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 170 | self.inner.poll_ready(cx) 171 | } 172 | 173 | fn call(&mut self, req: Request) -> Self::Future { 174 | let inner = self.inner.clone(); 175 | // take the service that was ready 176 | let inner = std::mem::replace(&mut self.inner, inner); 177 | 178 | let auth_fut = self.authorize(req); 179 | 180 | ResponseFuture { 181 | state: State::Authorize { auth_fut }, 182 | service: inner, 183 | } 184 | } 185 | } 186 | 187 | #[pin_project] 188 | /// Response future for [`AuthorizationService`]. 189 | pub struct ResponseFuture 190 | where 191 | B: Send + 'static, 192 | S: Service>, 193 | C: Clone + DeserializeOwned + Send + Sync + 'static, 194 | { 195 | #[pin] 196 | state: State< as Authorize>::Future, S::Future>, 197 | service: S, 198 | } 199 | 200 | #[pin_project(project = StateProj)] 201 | enum State { 202 | Authorize { 203 | #[pin] 204 | auth_fut: A, 205 | }, 206 | Authorized { 207 | #[pin] 208 | svc_fut: SFut, 209 | }, 210 | } 211 | 212 | impl Future for ResponseFuture 213 | where 214 | B: Send, 215 | S: Service>, 216 | S::Response: From, 217 | C: Clone + DeserializeOwned + Send + Sync, 218 | { 219 | type Output = Result; 220 | 221 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 222 | let mut this = self.project(); 223 | 224 | loop { 225 | match this.state.as_mut().project() { 226 | StateProj::Authorize { auth_fut } => { 227 | let auth = ready!(auth_fut.poll(cx)); 228 | match auth { 229 | Ok(req) => { 230 | let svc_fut = this.service.call(req); 231 | this.state.set(State::Authorized { svc_fut }) 232 | } 233 | Err(res) => { 234 | tracing::info!("err: {:?}", res); 235 | return Poll::Ready(Ok(res.into())); 236 | } 237 | }; 238 | } 239 | StateProj::Authorized { svc_fut } => { 240 | return svc_fut.poll(cx); 241 | } 242 | } 243 | } 244 | } 245 | } 246 | 247 | #[cfg(test)] 248 | mod tests { 249 | use crate::{authorizer::Authorizer, IntoLayer, JwtAuthorizer, RegisteredClaims}; 250 | 251 | use super::AuthorizationLayer; 252 | 253 | #[tokio::test] 254 | async fn auth_into_layer() { 255 | let auth1: Authorizer = JwtAuthorizer::from_secret("aaa").build().await.unwrap(); 256 | let layer = auth1.into_layer(); 257 | assert_eq!(1, layer.auths.len()); 258 | } 259 | 260 | #[tokio::test] 261 | async fn auths_into_layer() { 262 | let auth1 = JwtAuthorizer::from_secret("aaa").build().await.unwrap(); 263 | let auth2 = JwtAuthorizer::from_secret("bbb").build().await.unwrap(); 264 | 265 | let layer: AuthorizationLayer = [auth1, auth2].into_layer(); 266 | assert_eq!(2, layer.auths.len()); 267 | } 268 | 269 | #[tokio::test] 270 | async fn vec_auths_into_layer() { 271 | let auth1 = JwtAuthorizer::from_secret("aaa").build().await.unwrap(); 272 | let auth2 = JwtAuthorizer::from_secret("bbb").build().await.unwrap(); 273 | 274 | let layer: AuthorizationLayer = vec![auth1, auth2].into_layer(); 275 | assert_eq!(2, layer.auths.len()); 276 | } 277 | 278 | #[tokio::test] 279 | async fn jwt_auth_to_layer() { 280 | let auth1: JwtAuthorizer = JwtAuthorizer::from_secret("aaa"); 281 | #[allow(deprecated)] 282 | let layer = auth1.layer().await.unwrap(); 283 | assert_eq!(1, layer.auths.len()); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /jwt-authorizer/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../docs/README.md")] 2 | 3 | use axum::{async_trait, extract::FromRequestParts, http::request::Parts}; 4 | use jsonwebtoken::TokenData; 5 | use serde::de::DeserializeOwned; 6 | 7 | pub use self::error::AuthError; 8 | pub use authorizer::{Authorizer, IntoLayer}; 9 | pub use builder::{AuthorizerBuilder, JwtAuthorizer}; 10 | pub use claims::{NumericDate, OneOrArray, RegisteredClaims}; 11 | pub use jwks::key_store_manager::{Refresh, RefreshStrategy}; 12 | pub use validation::Validation; 13 | 14 | pub mod authorizer; 15 | pub mod builder; 16 | pub mod claims; 17 | pub mod error; 18 | pub mod jwks; 19 | pub mod layer; 20 | mod oidc; 21 | pub mod validation; 22 | 23 | /// Claims serialized using T 24 | #[derive(Debug, Clone, Copy, Default)] 25 | pub struct JwtClaims(pub T); 26 | 27 | #[async_trait] 28 | impl FromRequestParts for JwtClaims 29 | where 30 | T: DeserializeOwned + Send + Sync + Clone + 'static, 31 | S: Send + Sync, 32 | { 33 | type Rejection = AuthError; 34 | 35 | async fn from_request_parts(parts: &mut Parts, _: &S) -> Result { 36 | if let Some(claims) = parts.extensions.get::>() { 37 | Ok(JwtClaims(claims.claims.clone())) 38 | } else { 39 | Err(AuthError::NoAuthorizerLayer()) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /jwt-authorizer/src/oidc.rs: -------------------------------------------------------------------------------- 1 | use reqwest::{Client, Url}; 2 | use serde::Deserialize; 3 | 4 | use crate::error::InitError; 5 | 6 | /// OpenId Connect discovery (simplified for test purposes) 7 | #[derive(Deserialize, Clone)] 8 | pub struct OidcDiscovery { 9 | pub jwks_uri: String, 10 | } 11 | 12 | fn discovery_url(issuer: &str) -> Result { 13 | let mut url = Url::parse(issuer).map_err(|e| InitError::DiscoveryError(e.to_string()))?; 14 | 15 | url.path_segments_mut() 16 | .map_err(|_| InitError::DiscoveryError(format!("Issuer URL error! ('{issuer}' cannot be a base)")))? 17 | .pop_if_empty() 18 | .extend(&[".well-known", "openid-configuration"]); 19 | 20 | Ok(url) 21 | } 22 | 23 | pub async fn discover_jwks(issuer: &str, client: Option) -> Result { 24 | let client = client.unwrap_or_default(); 25 | 26 | client 27 | .get(discovery_url(issuer)?) 28 | .send() 29 | .await 30 | .map_err(|e| InitError::DiscoveryError(e.to_string()))? 31 | .json::() 32 | .await 33 | .map_err(|e| InitError::DiscoveryError(e.to_string())) 34 | .map(|d| d.jwks_uri) 35 | } 36 | 37 | #[test] 38 | fn discovery() { 39 | assert_eq!( 40 | Url::parse("http://host.com:99/xx/.well-known/openid-configuration").unwrap(), 41 | discovery_url("http://host.com:99/xx").unwrap() 42 | ); 43 | assert_eq!( 44 | Url::parse("http://host.com:99/xx/.well-known/openid-configuration").unwrap(), 45 | discovery_url("http://host.com:99/xx/").unwrap() 46 | ); 47 | assert_eq!( 48 | Url::parse("http://host.com:99/xx/yy/.well-known/openid-configuration").unwrap(), 49 | discovery_url("http://host.com:99/xx/yy").unwrap() 50 | ); 51 | assert_eq!( 52 | Url::parse("http://host.com:99/.well-known/openid-configuration").unwrap(), 53 | discovery_url("http://host.com:99").unwrap() 54 | ); 55 | assert!(discovery_url("xxx").is_err()); 56 | } 57 | -------------------------------------------------------------------------------- /jwt-authorizer/src/validation.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use jsonwebtoken::Algorithm; 4 | 5 | /// Defines the jwt validation parameters (with defaults simplifying configuration). 6 | pub struct Validation { 7 | /// Add some leeway (in seconds) to the `exp` and `nbf` validation to 8 | /// account for clock skew. 9 | /// 10 | /// Defaults to `60`. 11 | pub leeway: u64, 12 | /// Whether to validate the `exp` field. 13 | /// 14 | /// Defaults to `true`. 15 | pub validate_exp: bool, 16 | /// Whether to validate the `nbf` field. 17 | /// 18 | /// Defaults to `false`. 19 | pub validate_nbf: bool, 20 | /// If it contains a value, the validation will check that the `aud` claim value is in the values provided. 21 | /// 22 | /// Defaults to `None`. 23 | pub aud: Option>, 24 | /// If it contains a value, the validation will check that the `iss` claim value is in the values provided. 25 | /// 26 | /// Defaults to `None`. 27 | pub iss: Option>, 28 | 29 | /// Whether to validate the JWT signature. Very insecure to turn that off! 30 | /// 31 | /// Defaults to true. 32 | pub validate_signature: bool, 33 | 34 | /// Accepted algorithms 35 | /// 36 | /// If empty anly the algorithms matching key will be authorized 37 | pub algs: Vec, 38 | } 39 | 40 | impl Validation { 41 | /// new Validation with default values 42 | pub fn new() -> Self { 43 | Default::default() 44 | } 45 | 46 | /// check that the `iss` claim is a member of the values provided 47 | pub fn iss(mut self, items: &[T]) -> Self { 48 | self.iss = Some(items.iter().map(|x| x.to_string()).collect()); 49 | 50 | self 51 | } 52 | 53 | /// check that the `aud` claim is a member of the items provided 54 | pub fn aud(mut self, items: &[T]) -> Self { 55 | self.aud = Some(items.iter().map(|x| x.to_string()).collect()); 56 | 57 | self 58 | } 59 | 60 | /// enables or disables exp validation 61 | pub fn exp(mut self, val: bool) -> Self { 62 | self.validate_exp = val; 63 | 64 | self 65 | } 66 | 67 | /// enables or disables nbf validation 68 | pub fn nbf(mut self, val: bool) -> Self { 69 | self.validate_nbf = val; 70 | 71 | self 72 | } 73 | 74 | /// Add some leeway (in seconds) to the `exp` and `nbf` validation to 75 | /// account for clock skew. 76 | pub fn leeway(mut self, value: u64) -> Self { 77 | self.leeway = value; 78 | 79 | self 80 | } 81 | 82 | /// Whether to validate the JWT cryptographic signature 83 | /// Very insecure to turn that off, only do it if you know what you're doing. 84 | pub fn disable_validation(mut self) -> Self { 85 | self.validate_signature = false; 86 | 87 | self 88 | } 89 | 90 | /// Authorized algorithms. 91 | /// 92 | /// If no algs are supplied default algs for the key will be used 93 | /// (example for a EC key, algs = [ES256, ES384]). 94 | pub fn algs(mut self, algs: Vec) -> Self { 95 | self.algs = algs; 96 | 97 | self 98 | } 99 | 100 | pub(crate) fn to_jwt_validation(&self, default_algs: &[Algorithm]) -> jsonwebtoken::Validation { 101 | let required_claims = if self.validate_exp { 102 | let mut claims = HashSet::with_capacity(1); 103 | claims.insert("exp".to_owned()); 104 | claims 105 | } else { 106 | HashSet::with_capacity(0) 107 | }; 108 | 109 | let aud = self.aud.clone().map(HashSet::from_iter); 110 | let iss = self.iss.clone().map(HashSet::from_iter); 111 | 112 | let mut jwt_validation = jsonwebtoken::Validation::default(); 113 | 114 | jwt_validation.required_spec_claims = required_claims; 115 | jwt_validation.leeway = self.leeway; 116 | jwt_validation.validate_exp = self.validate_exp; 117 | jwt_validation.validate_nbf = self.validate_nbf; 118 | jwt_validation.iss = iss; 119 | jwt_validation.aud = aud; 120 | jwt_validation.sub = None; 121 | jwt_validation.algorithms = if self.algs.is_empty() { 122 | default_algs.to_owned() 123 | } else { 124 | self.algs.clone() 125 | }; 126 | if !self.validate_signature { 127 | jwt_validation.insecure_disable_signature_validation(); 128 | } 129 | 130 | jwt_validation 131 | } 132 | } 133 | 134 | impl Default for Validation { 135 | fn default() -> Self { 136 | Validation { 137 | leeway: 60, 138 | 139 | validate_exp: true, 140 | validate_nbf: false, 141 | 142 | iss: None, 143 | aud: None, 144 | 145 | validate_signature: true, 146 | algs: vec![], 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /jwt-authorizer/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use lazy_static::lazy_static; 4 | use serde_json::{json, Value}; 5 | 6 | lazy_static! { 7 | pub static ref JWKS_RSA1: Value = json!({ 8 | "keys": [{ 9 | "kty": "RSA", 10 | "n": "2pQeZdxa7q093K7bj5h6-leIpxfTnuAxzXdhjfGEJHxmt2ekHyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy_tw-5e-Px1xFj1PykGEkRlOpYSAeWsNaAWvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV-fcGGLhJnXl0-5_z7tKC7RvBoT3SGwlc_AmJqpFtTpEBn_fDnyqiZbpcjXYLExFpExm41xDitRKHWIwfc3dV8_vlNntlxCPGy_THkjdXJoHv2IJmlhvmr5_h03iGMLWDKSywxOol_4Wc1BT7Hb6byMxW40GKwSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO-XiVShZRVg7JeraGAfWwaIgIJ1D8C1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4NDX-A4KRMgaxcfAcui_x6gybksq6gF90-9nfQfmVMVJctZ6M-FvRr-itd1Nef5WAtwUp1qyZygAXU3cH3rarscajmurOsP6dE1OHl3grY_eZhQxk33VBK9lavqNKPg6Q_PLiq1ojbYBj3bcYifJrsNeQwxldQP83aWt5rGtgZTehKVJwa40Uy_Grae1iRnsDtdSy5sTJIJ6EiShnWAdMoGejdiI8vpkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPWc6JEotQqI0HwhzU0KHyoY4s", 11 | "e": "AQAB", 12 | "kid": "rsa01", 13 | "alg": "RS256", 14 | "use": "sig" 15 | }] 16 | }); 17 | pub static ref JWKS_RSA2: Value = json!({ 18 | "keys": [{ 19 | "kty": "RSA", 20 | "n": "yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ", 21 | "e": "AQAB", 22 | "kid": "rsa02", 23 | "alg": "RS256", 24 | "use": "sig" 25 | }] 26 | }); 27 | pub static ref JWKS_EC1: Value = json!({ 28 | "keys": [{ 29 | "kty": "EC", 30 | "crv": "P-256", 31 | "x": "MZiwc5EVP_E3vkd2oKedr4lWVMN9vgdyBBpBIVFJjwY", 32 | "y": "1npLU75B6M0mb01zUAVoeYJSDOlQJmvjBdqLPjJvy3Y", 33 | "kid": "ec01", 34 | "alg": "ES256", 35 | "use": "sig" 36 | }] 37 | }); 38 | pub static ref JWKS_EC1_ES384: Value = json!({ 39 | "keys": [{ 40 | "kty": "EC", 41 | "crv": "P-384", 42 | "x": "E6z5gdLe4agzbKELA3SOlcfX9Oz1d0HCjcW5-v0bOB3ERN6xEVBaopzwXVKk8ifl", 43 | "y": "vxxW405A1PSZzUVOvAeylcwjCi_8sPm2ry42vmvKQYx_jB1U0fnLJcQ5Q9rL3sUg", 44 | "kid": "ec01-es384", 45 | "alg": "ES384", 46 | "use": "sig" 47 | }] 48 | }); 49 | } 50 | 51 | pub const JWT_RSA1_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InJzYTAxIn0.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.pmm8Kdk-SvycXIGpWb1R0DuP5nlB7w4QQS7trhN_OjOpbk0A8F_lC4BdClz3rol2Pgo61lcFckJgjNBj34DQGeTGOtvxdiUXNgi1aKiXH4AyPzZeZx30PgFxa1fxhuZhBAj6xIZKBSBQvVyjeVQzAScINRCBX8zfCaXSU1ZCUkJl5vbD7zT-cYIFU76we9HcIYKRXwTiAyoNn3Lixa1H3_t5sbx3om2WlIB2x-sGpoDFDjorcuJT1yQx3grTRTBzHyRBRjZ3e8wrMbiacy-m3WoEFdkssQgYi_dSQH0hvxgacvGWayK0UqD7O5UL6EzTA2feXbgA_68o5gfvSnM8CUsPut5gZr-gwVbQKPbBdCQtl_wXIMot7UNKYEiFV38x5EmUr-ShzQcditW6fciguuY1Qav502UE1UMXvt5p8-kYxw2AaaVd6iTgQBzkBrtvywMYWzIwzGNA70RvUhI2rlgcn8GEU_51Tv_NMHjp6CjDbAxQVKa0PlcRE4pd6yk_IJSR4Nska_8BQZdPbsFn--z_XHEDoRZQ1C1M6m77xVndg3zX0sNQPXfWsttCbBmaHvMKTOp0cH9rlWB9r9nTo9fn8jcfqlak2O2IAzfzsOdVfUrES6T1UWkWobs9usGgqJuIkZHbDd4tmXyPRT4wrU7hxEyE9cuvuZPAi8GYt80"; 52 | pub const JWT_RSA1_AUD1_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InJzYTAxIn0.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiYXVkIjpbImF1ZDEiLCJhdWQyIl0sImV4cCI6MjAwMDAwMDAwMCwibmJmIjoxNTE2MjM5MDIyfQ.Wzf2NZWdngKEGGkSP42sWxD9zw8rjarslbjtflQ1UQ4TsbhDgasoLUhL6D483xmt30vRQIjzLeTWlsERva1rhyeZuif0sr9wqsQge5VEBDEt5CUwwi2KVpNhC75leChCN1VcA9IKJ3LodICaCw4ks6wrAM_29AbbH8jxlyZc25d0uAGdbc99c6-aQhfRmW68GMN7dryGTXfAoIsl70AHrMOt-1Csn8qoMsBUE1uKOFsnA6c8rGzVeeHx5N6dvCpXEsE7_rP6GClGa0qBkb2v8llgSPpPZlIklf2NnZYr3WW_hy__-VGitJXiniUfhzWqqDv-K773aQ0532V8SdBHZ9r6Ib7gtRCUqRX7VcK-HdMM9SPyGCXb1qSwOD_XuqGJ58IInzb-B7zde4d18Fy6jVmf27FXRZYAMX4YMVeEZgXnurGtghRqboxGy9nFznOK_uK9XSJmDjsHrLSIKqat158OhDvPj0tDCz_a7fn3fk2Yd8-QPSJIFQanInHahlBMlSLS4F2p5QM48ynoIl56bjam7XOO8A6hQipBQDHkQ5IWJaKtckRIf7wzhfp9ptOsB2MYqVO9mX0IcOQB7ydpxuj0AWacp7Z5JjdrZDekKJIEoBEEIzoxGqnJsg9fu8jkx287jy8WxaJ13uMm7ql1zqDLWXQb_PCVwW9t-99hDyM"; 53 | pub const JWT_RSA2_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InJzYTAyIn0.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.tWyA4ve2CY6GruBch_qIf8f1PgCEhqmrZ1J5XBuwO_v-P-PSLe3MWpkPAMdIDE5QE19ItUcGdJblhiyPb0tJJtrDHVYER7q8X4fOjQjY_NlFK6Bd1GtZS2DCA5EPxIX8l7Jpn8fPvbyamagLwnB_waQaYBteTGnOkLmz3F3sqC8KdO9lyu5v7BknC1f56ZOvr_DiInkTiAsTWqX4nS2KYRjcz4HcxcPO7O0CFXqcOTF_e3ntmq4rQV9LHCaEnuXj2WZtnX423CMkcG0uYzsnmWAMPB6IlDKejPnAJThMjjuJhze1gGbP1U8c53UbEhfHEZgJ2N634YEXMfsojZ5VzQ"; 54 | pub const JWT_EC1_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImVjMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.MvZm3Cxf78OQYpPkVGPAHaNf7GasHcvlF7ONJRxKVAntXbTru_dIdTRH0gz4xMIDg3a7HyfHWRLRhdxSNPjMPQ"; 55 | pub const JWT_EC1_ES384_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzM4NCIsImtpZCI6ImVjMDEtZXMzODQifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.IsGT5Zw4V_igQOGnk5KqyHDIUnEaqNU-1TEWFG0GDXf-vqkUqHg9iX0OJpt6iCJoio8srzNHivJ-JXoYG33olE71uv7AITPYEHS8yMMs53uIKP7LT-oq13-eHSmA9lIV"; 56 | pub const JWT_EC1_AUD1_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImVjMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiYXVkIjpbImF1ZDEiLCJhdWQyIl0sImV4cCI6MjAwMDAwMDAwMCwibmJmIjoxNTE2MjM5MDIyfQ.mFveRLl0SiceOPmv2UKZwaUUqVO-q7NcDkjcEUU4aoBz_YR2UuHtKnYw_TsYIkCz5uCCuwGgGRUeC9_-14GrWQ"; 57 | pub const JWT_EC2_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImVjMDIifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.IRW3iOr-pwlDW-rFH_WRAwXZlk4qbxRRqrdJfm0XsGYmvp1Beqnj8L8jsMHtsJzs9PDsCEbwYXiU_u5vnOsIJA"; 58 | pub const JWT_EC1_EXP_KO: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImVjMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJib2IiLCJleHAiOjE1MTYyMzkwMjIsIm5iZiI6MTUxNjIzOTAyMn0.MNmY66S3NgSAbWwZP0hfC5pme3SM7B3yvFhBFLQH-cU3enP0G8bBzDOhpjmli9uKQitkIQxffwu2Au9wTUraTQ"; 59 | pub const JWT_EC1_NBF_KO: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImVjMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJib2IiLCJleHAiOjIwMDAwMDAwMDAsIm5iZiI6MjAwMDAwMDAwMH0.d5MRfwcToMxR7O7NEt3qUj-MUKKpG9BZW1w6ihyfN95ZULoMajr7mtYY2R2LS96oBYgp3OdlR4tkHmdqDpvCSA"; 60 | pub const JWT_ED1_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImVkMDEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.5bFOZqc-lBFy4gFifQ_CTx1A3R6Nry71gdi7KH2GGvTZQC_ZI1vNbqGnWQhpR6n_jUd9ICUc0pPI5iLCB6K1Bg"; 61 | pub const JWT_ED2_OK: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImVkMDIifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDEiLCJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwLCJuYmYiOjE1MTYyMzkwMjJ9.UMdAP1cJJmP7Bc-Um0-6oB4Bn52MwlhAzCTd5_-CiL6DjcJFzDq4rhBFz3HZbX4k0K228cCQosY9K8YfRWpjBQ"; 62 | -------------------------------------------------------------------------------- /jwt-authorizer/tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | net::SocketAddr, 3 | sync::{ 4 | atomic::{AtomicI16, Ordering}, 5 | Arc, Once, 6 | }, 7 | thread, 8 | time::Duration, 9 | }; 10 | 11 | use axum::{body::Body, response::Response, routing::get, Json, Router}; 12 | use http::{header::AUTHORIZATION, Request, StatusCode}; 13 | use jwt_authorizer::{IntoLayer, JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy, Validation}; 14 | use lazy_static::lazy_static; 15 | use serde::{Deserialize, Serialize}; 16 | use serde_json::Value; 17 | use tokio::net::TcpListener; 18 | use tower::Service; 19 | use tower::ServiceExt; 20 | 21 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 22 | 23 | use crate::common::{JWT_RSA1_OK, JWT_RSA2_OK}; 24 | 25 | mod common; 26 | 27 | /// Static variable to ensure that logging is only initialized once. 28 | pub static INITIALIZED: Once = Once::new(); 29 | 30 | #[derive(Debug, Deserialize, Serialize, Clone)] 31 | struct User { 32 | sub: String, 33 | } 34 | 35 | lazy_static! { 36 | static ref DISCOVERY_COUNTER: Arc = Arc::new(AtomicI16::new(0)); 37 | static ref JWKS_COUNTER: Arc = Arc::new(AtomicI16::new(0)); 38 | } 39 | 40 | struct Stats {} 41 | 42 | impl Stats { 43 | fn reset() { 44 | Arc::clone(&DISCOVERY_COUNTER).store(0, Ordering::Relaxed); 45 | Arc::clone(&JWKS_COUNTER).store(0, Ordering::Relaxed); 46 | } 47 | fn jwks_counter() -> i16 { 48 | Arc::clone(&JWKS_COUNTER).load(Ordering::Relaxed) 49 | } 50 | fn discovery_counter() -> i16 { 51 | Arc::clone(&DISCOVERY_COUNTER).load(Ordering::Relaxed) 52 | } 53 | } 54 | 55 | fn discovery(uri: &str) -> Json { 56 | Arc::clone(&DISCOVERY_COUNTER).fetch_add(1, Ordering::Relaxed); 57 | let d = serde_json::json!({ "jwks_uri": format!("{uri}/jwks") }); 58 | 59 | Json(d) 60 | } 61 | 62 | async fn jwks() -> Json { 63 | Arc::clone(&JWKS_COUNTER).fetch_add(1, Ordering::Relaxed); 64 | 65 | Json(common::JWKS_RSA1.clone()) 66 | } 67 | 68 | async fn run_jwks_server() -> String { 69 | let listener = TcpListener::bind("0.0.0.0:0".parse::().unwrap()).await.unwrap(); 70 | let addr = listener.local_addr().unwrap(); 71 | let url = format!("http://{}:{}", addr.ip(), addr.port()); 72 | 73 | let url2 = url.clone(); 74 | 75 | let app = Router::new() 76 | .route("/.well-known/openid-configuration", get(|| async move { discovery(&url2) })) 77 | .route("/jwks", get(jwks)); 78 | 79 | tokio::spawn(async move { 80 | axum::serve(listener, app.into_make_service()).await.unwrap(); 81 | }); 82 | 83 | url 84 | } 85 | 86 | async fn app(jwt_auth: JwtAuthorizer) -> Router { 87 | async fn public_handler() -> &'static str { 88 | "public" 89 | } 90 | 91 | async fn protected_handler() -> &'static str { 92 | "protected" 93 | } 94 | 95 | async fn protected_with_user(JwtClaims(user): JwtClaims) -> Json { 96 | Json(user) 97 | } 98 | 99 | let pub_route: Router = Router::new().route("/public", get(public_handler)); 100 | let protected_route: Router = Router::new() 101 | .route("/protected", get(protected_handler)) 102 | .route("/protected-with-user", get(protected_with_user)) 103 | .layer( 104 | jwt_auth 105 | .validation(Validation::new().aud(&["aud1"])) 106 | .build() 107 | .await 108 | .unwrap() 109 | .into_layer(), 110 | ); 111 | 112 | Router::new().merge(pub_route).merge(protected_route) 113 | } 114 | 115 | fn init_test() { 116 | INITIALIZED.call_once(|| { 117 | tracing_subscriber::registry() 118 | .with(tracing_subscriber::EnvFilter::new( 119 | std::env::var("RUST_LOG").unwrap_or_else(|_| "info,jwt-authorizer=debug,tower_http=debug".into()), 120 | )) 121 | .with(tracing_subscriber::fmt::layer()) 122 | .init(); 123 | }); 124 | // reset counters 125 | Stats::reset(); 126 | } 127 | 128 | async fn make_proteced_request(app: &mut Router, bearer: &str) -> Response { 129 | app.as_service() 130 | .ready() 131 | .await 132 | .unwrap() 133 | .call( 134 | Request::builder() 135 | .uri("/protected") 136 | .header(AUTHORIZATION.as_str(), format!("Bearer {bearer}")) 137 | .body(Body::empty()) 138 | .unwrap(), 139 | ) 140 | .await 141 | .unwrap() 142 | } 143 | 144 | async fn make_public_request(app: &mut Router) -> Response { 145 | app.as_service() 146 | .ready() 147 | .await 148 | .unwrap() 149 | .call(Request::builder().uri("/public").body(Body::empty()).unwrap()) 150 | .await 151 | .unwrap() 152 | } 153 | 154 | #[tokio::test] 155 | async fn sequential_tests() { 156 | // these tests must be executed sequentially 157 | scenario1().await; 158 | scenario2().await; 159 | scenario3().await; 160 | scenario4().await; 161 | } 162 | 163 | async fn scenario1() { 164 | init_test(); 165 | let url = run_jwks_server().await; 166 | let auth: JwtAuthorizer = JwtAuthorizer::from_oidc(&url); 167 | let mut app = app(auth).await; 168 | assert_eq!(1, Stats::discovery_counter()); 169 | assert_eq!(0, Stats::jwks_counter()); 170 | // NO LOADING when public request 171 | let r = make_public_request(&mut app).await; 172 | assert_eq!(StatusCode::OK, r.status()); 173 | assert_eq!(0, Stats::jwks_counter(), "sc1: public -> no loading"); 174 | // LOADING - first jwt check 175 | let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; 176 | assert_eq!(StatusCode::OK, r.status()); 177 | assert_eq!(1, Stats::jwks_counter(), "sc1: 1st check -> loading"); 178 | // NO RELOADING same kid with OK 179 | let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; 180 | assert_eq!(StatusCode::OK, r.status()); 181 | assert_eq!(1, Stats::jwks_counter(), "sc1: 2st check -> no loading"); 182 | // NO RELEOADING, invalid kid, 401 183 | let r = make_proteced_request(&mut app, JWT_RSA2_OK).await; 184 | assert_eq!(StatusCode::UNAUTHORIZED, r.status()); 185 | assert_eq!(1, Stats::jwks_counter(), "sc1: 3st check (invalid kid) -> no loading"); 186 | } 187 | 188 | /// SCENARIO2 189 | /// 190 | /// Refresh strategy: INTERVAL 191 | async fn scenario2() { 192 | init_test(); 193 | let url = run_jwks_server().await; 194 | let refresh = Refresh { 195 | refresh_interval: Duration::from_millis(40), 196 | retry_interval: Duration::from_millis(0), 197 | strategy: RefreshStrategy::Interval, 198 | }; 199 | let auth: JwtAuthorizer = JwtAuthorizer::from_oidc(&url).refresh(refresh); 200 | let mut app = app(auth).await; 201 | assert_eq!(1, Stats::discovery_counter()); 202 | assert_eq!(0, Stats::jwks_counter()); 203 | let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; 204 | assert_eq!(StatusCode::OK, r.status()); 205 | assert_eq!(1, Stats::jwks_counter()); 206 | // NO RELOADING same kid 207 | let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; 208 | assert_eq!(StatusCode::OK, r.status()); 209 | assert_eq!(1, Stats::jwks_counter()); 210 | // RELEOADING, same kid, refresh_interval elapsed 211 | thread::sleep(Duration::from_millis(41)); 212 | let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; 213 | assert_eq!(StatusCode::OK, r.status()); 214 | assert_eq!(2, Stats::jwks_counter()); 215 | } 216 | 217 | /// SCENARIO3 218 | /// 219 | /// Refresh strategy: KeyNotFound 220 | async fn scenario3() { 221 | init_test(); 222 | let url = run_jwks_server().await; 223 | let refresh = Refresh { 224 | strategy: RefreshStrategy::KeyNotFound, 225 | refresh_interval: Duration::from_millis(40), 226 | retry_interval: Duration::from_millis(0), 227 | }; 228 | let auth: JwtAuthorizer = JwtAuthorizer::from_oidc(&url).refresh(refresh); 229 | let mut app = app(auth).await; 230 | assert_eq!(1, Stats::discovery_counter()); 231 | assert_eq!(0, Stats::jwks_counter()); 232 | // RELOADING getting keys first time 233 | let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; 234 | assert_eq!(StatusCode::OK, r.status()); 235 | assert_eq!(1, Stats::jwks_counter()); 236 | thread::sleep(Duration::from_millis(21)); 237 | // NO RELOADING refresh interval elapsed, kid OK 238 | let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; 239 | assert_eq!(StatusCode::OK, r.status()); 240 | assert_eq!(1, Stats::jwks_counter()); 241 | // RELEOADING, unknown kid, refresh_interval elapsed 242 | thread::sleep(Duration::from_millis(41)); 243 | let r = make_proteced_request(&mut app, JWT_RSA2_OK).await; 244 | assert_eq!(StatusCode::UNAUTHORIZED, r.status()); 245 | assert_eq!(2, Stats::jwks_counter()); 246 | } 247 | 248 | /// SCENARIO4 249 | /// 250 | /// Refresh strategy: NoRefresh 251 | async fn scenario4() { 252 | init_test(); 253 | let url = run_jwks_server().await; 254 | let refresh = Refresh { 255 | strategy: RefreshStrategy::NoRefresh, 256 | refresh_interval: Duration::from_millis(0), 257 | retry_interval: Duration::from_millis(0), 258 | }; 259 | let auth: JwtAuthorizer = JwtAuthorizer::from_oidc(&url).refresh(refresh); 260 | let mut app = app(auth).await; 261 | assert_eq!(1, Stats::discovery_counter()); 262 | assert_eq!(0, Stats::jwks_counter()); 263 | // RELOADING getting keys first time 264 | let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; 265 | assert_eq!(StatusCode::OK, r.status()); 266 | assert_eq!(1, Stats::jwks_counter()); 267 | thread::sleep(Duration::from_millis(21)); 268 | // NO RELOADING kid OK 269 | let r = make_proteced_request(&mut app, JWT_RSA1_OK).await; 270 | assert_eq!(StatusCode::OK, r.status()); 271 | assert_eq!(1, Stats::jwks_counter()); 272 | // NO RELEOADING, unknown kid 273 | thread::sleep(Duration::from_millis(41)); 274 | let r = make_proteced_request(&mut app, JWT_RSA2_OK).await; 275 | assert_eq!(StatusCode::UNAUTHORIZED, r.status()); 276 | assert_eq!(1, Stats::jwks_counter()); 277 | } 278 | -------------------------------------------------------------------------------- /jwt-authorizer/tests/tests.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | #[cfg(test)] 4 | mod tests { 5 | use std::{convert::Infallible, sync::Arc}; 6 | 7 | use axum::{ 8 | body::Body, 9 | http::{Request, StatusCode}, 10 | response::Response, 11 | routing::get, 12 | BoxError, Router, 13 | }; 14 | use http::{header, HeaderValue}; 15 | use jsonwebtoken::Algorithm; 16 | use jwt_authorizer::{ 17 | authorizer::Authorizer, 18 | layer::{AuthorizationLayer, JwtSource}, 19 | validation::Validation, 20 | IntoLayer, JwtAuthorizer, JwtClaims, 21 | }; 22 | use serde::Deserialize; 23 | use tower::{util::MapErrLayer, ServiceExt}; 24 | 25 | use crate::common; 26 | use http_body_util::BodyExt; 27 | 28 | #[derive(Debug, Deserialize, Clone)] 29 | struct User { 30 | sub: String, 31 | } 32 | 33 | async fn app(layer: AuthorizationLayer) -> Router { 34 | Router::new().route("/public", get(|| async { "hello" })).route( 35 | "/protected", 36 | get(|JwtClaims(user): JwtClaims| async move { format!("hello: {}", user.sub) }).layer( 37 | tower_layer::Stack::new( 38 | tower_layer::Stack::new( 39 | tower::buffer::BufferLayer::new(1), 40 | MapErrLayer::new(|e: BoxError| -> Infallible { panic!("{}", e) }), 41 | ), 42 | layer, 43 | ), 44 | ), 45 | ) 46 | } 47 | 48 | async fn proteced_request_with_header(jwt_auth: JwtAuthorizer, header_name: &str, header_value: &str) -> Response { 49 | proteced_request_with_header_and_layer(jwt_auth.build().await.unwrap().into_layer(), header_name, header_value).await 50 | } 51 | 52 | async fn proteced_request_with_header_and_layer( 53 | layer: AuthorizationLayer, 54 | header_name: &str, 55 | header_value: &str, 56 | ) -> Response { 57 | app(layer) 58 | .await 59 | .oneshot( 60 | Request::builder() 61 | .uri("/protected") 62 | .header(header_name, header_value) 63 | .body(Body::empty()) 64 | .unwrap(), 65 | ) 66 | .await 67 | .unwrap() 68 | } 69 | 70 | async fn make_proteced_request(jwt_auth: JwtAuthorizer, bearer: &str) -> Response { 71 | proteced_request_with_header(jwt_auth, "Authorization", &format!("Bearer {bearer}")).await 72 | } 73 | 74 | #[tokio::test] 75 | async fn protected_without_jwt() { 76 | let auth: Authorizer = JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem") 77 | .build() 78 | .await 79 | .unwrap(); 80 | 81 | let response = app(auth.into_layer()) 82 | .await 83 | .oneshot(Request::builder().uri("/protected").body(Body::empty()).unwrap()) 84 | .await 85 | .unwrap(); 86 | 87 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 88 | 89 | assert!( 90 | response.headers().get(header::WWW_AUTHENTICATE).is_some(), 91 | "Must have a WWW-Authenticate header!" 92 | ); 93 | assert_eq!(response.headers().get(header::WWW_AUTHENTICATE).unwrap(), &"Bearer"); 94 | // TODO: realm="example" 95 | } 96 | 97 | #[tokio::test] 98 | async fn protected_with_jwt() { 99 | // ED PEM 100 | let response = make_proteced_request( 101 | JwtAuthorizer::from_ed_pem("../config/ed25519-public2.pem").validation(Validation::new().aud(&["aud1"])), 102 | common::JWT_ED2_OK, 103 | ) 104 | .await; 105 | assert_eq!(response.status(), StatusCode::OK); 106 | 107 | let body = response.into_body().collect().await.unwrap().to_bytes(); 108 | 109 | assert_eq!(&body[..], b"hello: b@b.com"); 110 | 111 | // ECDSA PEM 112 | let response = make_proteced_request( 113 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public2.pem").validation(Validation::new().aud(&["aud1"])), 114 | common::JWT_EC2_OK, 115 | ) 116 | .await; 117 | assert_eq!(response.status(), StatusCode::OK); 118 | let body = response.into_body().collect().await.unwrap().to_bytes(); 119 | assert_eq!(&body[..], b"hello: b@b.com"); 120 | 121 | // RSA PEM 122 | let response = 123 | make_proteced_request(JwtAuthorizer::from_rsa_pem("../config/rsa-public2.pem"), common::JWT_RSA2_OK).await; 124 | assert_eq!(response.status(), StatusCode::OK); 125 | let body = response.into_body().collect().await.unwrap().to_bytes(); 126 | assert_eq!(&body[..], b"hello: b@b.com"); 127 | 128 | // JWKS 129 | let response = make_proteced_request( 130 | JwtAuthorizer::from_jwks("../config/public1.jwks").validation(Validation::new().aud(&["aud1"])), 131 | common::JWT_RSA1_OK, 132 | ) 133 | .await; 134 | assert_eq!(response.status(), StatusCode::OK); 135 | let body = response.into_body().collect().await.unwrap().to_bytes(); 136 | assert_eq!(&body[..], b"hello: b@b.com"); 137 | 138 | let response = make_proteced_request( 139 | JwtAuthorizer::from_jwks("../config/public1.jwks").validation(Validation::new().aud(&["aud1"])), 140 | common::JWT_EC1_OK, 141 | ) 142 | .await; 143 | assert_eq!(response.status(), StatusCode::OK); 144 | let body = response.into_body().collect().await.unwrap().to_bytes(); 145 | assert_eq!(&body[..], b"hello: b@b.com"); 146 | 147 | let response = make_proteced_request( 148 | JwtAuthorizer::from_jwks("../config/public1.jwks").validation(Validation::new().aud(&["aud1"])), 149 | common::JWT_ED1_OK, 150 | ) 151 | .await; 152 | assert_eq!(response.status(), StatusCode::OK); 153 | let body = response.into_body().collect().await.unwrap().to_bytes(); 154 | assert_eq!(&body[..], b"hello: b@b.com"); 155 | 156 | // JWKS TEXT 157 | let response = make_proteced_request( 158 | JwtAuthorizer::from_jwks_text(include_str!("../../config/public1.jwks")) 159 | .validation(Validation::new().aud(&["aud1"])), 160 | common::JWT_ED1_OK, 161 | ) 162 | .await; 163 | assert_eq!(response.status(), StatusCode::OK); 164 | let body = response.into_body().collect().await.unwrap().to_bytes(); 165 | assert_eq!(&body[..], b"hello: b@b.com"); 166 | } 167 | 168 | #[tokio::test] 169 | async fn protected_with_bad_jwt() { 170 | let response = make_proteced_request(JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem"), "xxx.xxx.xxx").await; 171 | 172 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 173 | // TODO: check error code (https://datatracker.ietf.org/doc/html/rfc6750#section-3.1) 174 | } 175 | 176 | #[tokio::test] 177 | async fn protected_with_claims_check() { 178 | let b = true; // to test closures 179 | let rsp_ok = make_proteced_request( 180 | JwtAuthorizer::from_rsa_pem("../config/rsa-public2.pem").check(move |_| b), 181 | common::JWT_RSA2_OK, 182 | ) 183 | .await; 184 | 185 | assert_eq!(rsp_ok.status(), StatusCode::OK); 186 | 187 | let rsp_ko = make_proteced_request( 188 | JwtAuthorizer::from_rsa_pem("../config/rsa-public2.pem").check(|_| false), 189 | common::JWT_RSA2_OK, 190 | ) 191 | .await; 192 | 193 | assert_eq!(rsp_ko.status(), StatusCode::FORBIDDEN); 194 | 195 | let h = rsp_ko.headers().get(http::header::WWW_AUTHENTICATE); 196 | assert!(h.is_some(), "WWW-AUTHENTICATE header missing!"); 197 | assert_eq!( 198 | h.unwrap(), 199 | HeaderValue::from_static("Bearer error=\"insufficient_scope\""), 200 | "Bad WWW-AUTHENTICATE header!" 201 | ); 202 | } 203 | 204 | // Unreachable jwks endpoint, should build (endpoint can comme on line later ), 205 | // but should be 500 when checking. 206 | #[tokio::test] 207 | async fn protected_with_bad_jwks_url() { 208 | let response = 209 | make_proteced_request(JwtAuthorizer::from_jwks_url("http://bad-url/xxx/yyy"), common::JWT_RSA1_OK).await; 210 | 211 | assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); 212 | } 213 | 214 | #[tokio::test] 215 | async fn extract_from_public_401() { 216 | let app = Router::new().route( 217 | "/public", 218 | get(|JwtClaims(user): JwtClaims| async move { format!("hello: {}", user.sub) }), 219 | ); 220 | let response = app 221 | .oneshot(Request::builder().uri("/public").body(Body::empty()).unwrap()) 222 | .await 223 | .unwrap(); 224 | 225 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 226 | } 227 | 228 | #[tokio::test] 229 | async fn extract_from_public_optional() { 230 | let app = Router::new().route( 231 | "/public", 232 | get(|user: Option>| async move { format!("option: {}", user.is_none()) }), 233 | ); 234 | let response = app 235 | .oneshot(Request::builder().uri("/public").body(Body::empty()).unwrap()) 236 | .await 237 | .unwrap(); 238 | 239 | assert_eq!(response.status(), StatusCode::OK); 240 | let body = response.into_body().collect().await.unwrap().to_bytes(); 241 | assert_eq!(&body[..], b"option: true"); 242 | } 243 | 244 | // -------------------- 245 | // VALIDATION 246 | // --------------------- 247 | #[tokio::test] 248 | async fn validate_signature() { 249 | let response = make_proteced_request( 250 | JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem") 251 | .validation(Validation::new().aud(&["aud1"]).disable_validation()), 252 | common::JWT_EC2_OK, 253 | ) 254 | .await; 255 | assert_eq!(response.status(), StatusCode::OK); 256 | 257 | let response = make_proteced_request( 258 | JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").validation(Validation::new()), 259 | common::JWT_EC2_OK, 260 | ) 261 | .await; 262 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 263 | } 264 | 265 | #[tokio::test] 266 | async fn validate_iss() { 267 | let response = make_proteced_request( 268 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().iss(&["bad-iss"])), 269 | common::JWT_EC1_OK, 270 | ) 271 | .await; 272 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 273 | 274 | let response = make_proteced_request( 275 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().aud(&["aud1"])), 276 | common::JWT_EC1_OK, 277 | ) 278 | .await; 279 | assert_eq!(response.status(), StatusCode::OK); 280 | 281 | let response = make_proteced_request( 282 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem") 283 | .validation(Validation::new().iss(&["http://localhost:3001"]).aud(&["aud1"])), 284 | common::JWT_EC1_OK, 285 | ) 286 | .await; 287 | assert_eq!(response.status(), StatusCode::OK); 288 | } 289 | 290 | #[tokio::test] 291 | async fn validate_aud() { 292 | let response = make_proteced_request( 293 | JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").validation(Validation::new().aud(&["bad-aud"])), 294 | common::JWT_RSA1_AUD1_OK, 295 | ) 296 | .await; 297 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 298 | 299 | let response = make_proteced_request( 300 | JwtAuthorizer::from_ed_pem("../config/ed25519-public1.pem").validation(Validation::new().aud(&["aud1"])), 301 | common::JWT_ED1_OK, 302 | ) 303 | .await; 304 | assert_eq!(response.status(), StatusCode::OK); 305 | 306 | let response = make_proteced_request( 307 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().aud(&["aud1"])), 308 | common::JWT_EC1_AUD1_OK, 309 | ) 310 | .await; 311 | assert_eq!(response.status(), StatusCode::OK); 312 | } 313 | 314 | #[tokio::test] 315 | async fn validate_exp() { 316 | // DEFAULT -> ENABLED 317 | let response = make_proteced_request( 318 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new()), 319 | common::JWT_EC1_EXP_KO, 320 | ) 321 | .await; 322 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 323 | 324 | // DISABLED 325 | let response = make_proteced_request( 326 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().exp(false)), 327 | common::JWT_EC1_EXP_KO, 328 | ) 329 | .await; 330 | assert_eq!(response.status(), StatusCode::OK); 331 | 332 | // ENABLED 333 | let response = make_proteced_request( 334 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().exp(true)), 335 | common::JWT_EC1_EXP_KO, 336 | ) 337 | .await; 338 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 339 | let response = make_proteced_request( 340 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().exp(true).aud(&["aud1"])), 341 | common::JWT_EC1_OK, 342 | ) 343 | .await; 344 | assert_eq!(response.status(), StatusCode::OK); 345 | } 346 | 347 | #[tokio::test] 348 | async fn validate_nbf() { 349 | // DEFAULT -> DISABLED 350 | let response = make_proteced_request( 351 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new()), 352 | common::JWT_EC1_NBF_KO, 353 | ) 354 | .await; 355 | assert_eq!(response.status(), StatusCode::OK); 356 | 357 | // DISABLED 358 | let response = make_proteced_request( 359 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().nbf(false)), 360 | common::JWT_EC1_NBF_KO, 361 | ) 362 | .await; 363 | assert_eq!(response.status(), StatusCode::OK); 364 | 365 | // ENABLED 366 | let response = make_proteced_request( 367 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().nbf(true)), 368 | common::JWT_EC1_NBF_KO, 369 | ) 370 | .await; 371 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 372 | 373 | let response = make_proteced_request( 374 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem").validation(Validation::new().nbf(true).aud(&["aud1"])), 375 | common::JWT_EC1_OK, 376 | ) 377 | .await; 378 | assert_eq!(response.status(), StatusCode::OK); 379 | } 380 | 381 | #[tokio::test] 382 | async fn validate_algs() { 383 | // OK 384 | let response = make_proteced_request( 385 | JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem") 386 | .validation(Validation::new().algs(vec![Algorithm::RS256, Algorithm::RS384])), 387 | common::JWT_RSA1_OK, 388 | ) 389 | .await; 390 | assert_eq!(response.status(), StatusCode::OK); 391 | 392 | let response = make_proteced_request( 393 | JwtAuthorizer::from_ec_pem("../config/ec384-public1.pem") 394 | .validation(Validation::new().algs(vec![Algorithm::ES256, Algorithm::ES384])), 395 | common::JWT_EC1_ES384_OK, 396 | ) 397 | .await; 398 | assert_eq!(response.status(), StatusCode::OK); 399 | 400 | // NOK - Invalid Alg 401 | let response = make_proteced_request( 402 | JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem") 403 | .validation(Validation::new().algs(vec![Algorithm::RS512])), 404 | common::JWT_RSA1_OK, 405 | ) 406 | .await; 407 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 408 | } 409 | 410 | // -------------------- 411 | // jwt_source 412 | // --------------------- 413 | #[tokio::test] 414 | async fn jwt_source_cookie() { 415 | // OK 416 | let response = proteced_request_with_header( 417 | JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem") 418 | .validation(Validation::new().aud(&["aud1"])) 419 | .jwt_source(JwtSource::Cookie("ccc".to_owned())), 420 | header::COOKIE.as_str(), 421 | &format!("ccc={}", common::JWT_RSA1_OK), 422 | ) 423 | .await; 424 | assert_eq!(response.status(), StatusCode::OK); 425 | 426 | // Cookie missing 427 | let response = proteced_request_with_header( 428 | JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").jwt_source(JwtSource::Cookie("ccc".to_owned())), 429 | header::COOKIE.as_str(), 430 | &format!("bad_cookie={}", common::JWT_EC2_OK), 431 | ) 432 | .await; 433 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 434 | assert_eq!(response.headers().get(header::WWW_AUTHENTICATE).unwrap(), &"Bearer"); 435 | 436 | // Invalid Token 437 | let response = proteced_request_with_header( 438 | JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").jwt_source(JwtSource::Cookie("ccc".to_owned())), 439 | header::COOKIE.as_str(), 440 | &format!("ccc={}", common::JWT_EC2_OK), 441 | ) 442 | .await; 443 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 444 | assert_eq!( 445 | response.headers().get(header::WWW_AUTHENTICATE).unwrap(), 446 | &"Bearer error=\"invalid_token\"" 447 | ); 448 | } 449 | 450 | // -------------------------- 451 | // Multiple Authorizers 452 | // -------------------------- 453 | #[tokio::test] 454 | async fn multiple_authorizers() { 455 | // 1) Vec 456 | let auths: Vec> = vec![ 457 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem") 458 | .validation(Validation::new().aud(&["aud1"])) 459 | .build() 460 | .await 461 | .unwrap(), 462 | JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem") 463 | .validation(Validation::new().aud(&["aud1"])) 464 | .jwt_source(JwtSource::Cookie("ccc".to_owned())) 465 | .build() 466 | .await 467 | .unwrap(), 468 | ]; 469 | 470 | // OK 471 | let response = proteced_request_with_header_and_layer( 472 | auths.into_layer(), 473 | header::COOKIE.as_str(), 474 | &format!("ccc={}", common::JWT_RSA1_OK), 475 | ) 476 | .await; 477 | assert_eq!(response.status(), StatusCode::OK); 478 | 479 | // 2) Slice 480 | let auths: [Authorizer; 2] = [ 481 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem") 482 | .build() 483 | .await 484 | .unwrap(), 485 | JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem") 486 | .jwt_source(JwtSource::Cookie("ccc".to_owned())) 487 | .build() 488 | .await 489 | .unwrap(), 490 | ]; 491 | 492 | // Cookie missing 493 | let response = proteced_request_with_header_and_layer( 494 | auths.into_layer(), 495 | header::COOKIE.as_str(), 496 | &format!("bad_cookie={}", common::JWT_EC2_OK), 497 | ) 498 | .await; 499 | assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 500 | assert_eq!(response.headers().get(header::WWW_AUTHENTICATE).unwrap(), &"Bearer"); 501 | 502 | // 3) Arc 503 | let auth1 = Arc::new( 504 | JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem") 505 | .validation(Validation::new().aud(&["aud1"])) 506 | .build() 507 | .await 508 | .unwrap(), 509 | ); 510 | let auth2 = Arc::new( 511 | JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem") 512 | .validation(Validation::new().aud(&["aud1"])) 513 | .jwt_source(JwtSource::Cookie("ccc".to_owned())) 514 | .build() 515 | .await 516 | .unwrap(), 517 | ); 518 | 519 | // Slice/OK 520 | let response = proteced_request_with_header_and_layer( 521 | [auth1.clone(), auth2.clone()].into_layer(), 522 | header::COOKIE.as_str(), 523 | &format!("ccc={}", common::JWT_RSA1_OK), 524 | ) 525 | .await; 526 | assert_eq!(response.status(), StatusCode::OK); 527 | 528 | // Vec/OK 529 | let response = proteced_request_with_header_and_layer( 530 | vec![auth1, auth2.clone()].into_layer(), 531 | header::COOKIE.as_str(), 532 | &format!("ccc={}", common::JWT_RSA1_OK), 533 | ) 534 | .await; 535 | assert_eq!(response.status(), StatusCode::OK); 536 | 537 | // Arc/OK 538 | let response = proteced_request_with_header_and_layer( 539 | auth2.into_layer(), 540 | header::COOKIE.as_str(), 541 | &format!("ccc={}", common::JWT_RSA1_OK), 542 | ) 543 | .await; 544 | assert_eq!(response.status(), StatusCode::OK); 545 | } 546 | } 547 | -------------------------------------------------------------------------------- /jwt-authorizer/tests/tonic.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::Once, task::Poll}; 2 | 3 | use axum::extract::Request; 4 | use futures_core::future::BoxFuture; 5 | use http::header::AUTHORIZATION; 6 | use jwt_authorizer::{layer::AuthorizationService, IntoLayer, JwtAuthorizer, Validation}; 7 | use serde::{Deserialize, Serialize}; 8 | use tonic::{server::NamedService, server::UnaryService, IntoRequest, Status}; 9 | use tower::{buffer::Buffer, Service}; 10 | 11 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 12 | 13 | use crate::common::{JWT_RSA1_OK, JWT_RSA2_OK}; 14 | 15 | mod common; 16 | 17 | /// Static variable to ensure that logging is only initialized once. 18 | pub static INITIALIZED: Once = Once::new(); 19 | 20 | #[derive(Debug, Deserialize, Serialize, Clone)] 21 | struct User { 22 | sub: String, 23 | } 24 | 25 | #[derive(prost::Message)] 26 | struct HelloMessage { 27 | #[prost(string, tag = "1")] 28 | message: String, 29 | } 30 | 31 | #[derive(Debug, Default, Clone)] 32 | struct SayHelloMethod {} 33 | impl UnaryService for SayHelloMethod { 34 | type Response = HelloMessage; 35 | type Future = BoxFuture<'static, Result, Status>>; 36 | 37 | fn call(&mut self, request: tonic::Request) -> Self::Future { 38 | Box::pin(async move { 39 | let hi = request.into_inner(); 40 | let reply = HelloMessage { 41 | message: format!("Hello, {}", hi.message), 42 | }; 43 | Ok(tonic::Response::new(reply)) 44 | }) 45 | } 46 | } 47 | 48 | #[derive(Debug, Default, Clone)] 49 | struct GreeterServer { 50 | expected_sub: String, 51 | } 52 | 53 | impl Service> for GreeterServer { 54 | type Response = http::Response; 55 | type Error = std::convert::Infallible; 56 | type Future = BoxFuture<'static, Result>; 57 | 58 | fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll> { 59 | Poll::Ready(Ok(())) 60 | } 61 | 62 | fn call(&mut self, req: http::Request) -> Self::Future { 63 | let token = req.extensions().get::>().unwrap(); 64 | assert_eq!(token.claims.sub, self.expected_sub); 65 | match req.uri().path() { 66 | "/hello/SayHello" => Box::pin(async move { 67 | let mut grpc = tonic::server::Grpc::new(tonic::codec::ProstCodec::default()); 68 | Ok(grpc.unary(SayHelloMethod::default(), req).await) 69 | }), 70 | p => { 71 | let p = p.to_string(); 72 | Box::pin(async move { Ok(Status::unimplemented(p).into_http()) }) 73 | } 74 | } 75 | } 76 | } 77 | 78 | impl NamedService for GreeterServer { 79 | const NAME: &'static str = "hello"; 80 | } 81 | 82 | async fn app( 83 | jwt_auth: JwtAuthorizer, 84 | expected_sub: String, 85 | ) -> AuthorizationService>, User> { 86 | let layer = jwt_auth.build().await.unwrap().into_layer(); 87 | tonic::transport::Server::builder() 88 | .layer(layer) 89 | .layer(tower::buffer::BufferLayer::new(1)) 90 | .add_service(GreeterServer { expected_sub }) 91 | .into_service() 92 | } 93 | 94 | fn init_test() { 95 | INITIALIZED.call_once(|| { 96 | tracing_subscriber::registry() 97 | .with(tracing_subscriber::EnvFilter::new( 98 | std::env::var("RUST_LOG").unwrap_or_else(|_| "info,jwt-authorizer=debug,tower_http=debug".into()), 99 | )) 100 | .with(tracing_subscriber::fmt::layer()) 101 | .init(); 102 | }); 103 | } 104 | 105 | async fn make_protected_request( 106 | app: AuthorizationService, 107 | bearer: Option<&str>, 108 | message: &str, 109 | ) -> Result, Status> 110 | where 111 | S: Service< 112 | http::Request, 113 | Response = http::Response, 114 | Error = tower::BoxError, 115 | > + Send 116 | + Clone 117 | + 'static, 118 | S::Future: Send, 119 | { 120 | let mut grpc = tonic::client::Grpc::new(app); 121 | 122 | let mut request = HelloMessage { 123 | message: message.to_string(), 124 | } 125 | .into_request(); 126 | 127 | if let Some(bearer) = bearer { 128 | let headers = request.metadata_mut(); 129 | headers.insert(AUTHORIZATION.as_str(), format!("Bearer {bearer}").parse().unwrap()); 130 | } 131 | 132 | grpc.ready().await.unwrap(); 133 | grpc.unary( 134 | request, 135 | http::uri::PathAndQuery::from_static("/hello/SayHello"), 136 | tonic::codec::ProstCodec::default(), 137 | ) 138 | .await 139 | } 140 | 141 | #[tokio::test] 142 | async fn successfull_auth() { 143 | init_test(); 144 | let auth: JwtAuthorizer = 145 | JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").validation(Validation::new().aud(&["aud1"])); 146 | let app = app(auth, "b@b.com".to_string()).await; 147 | let r = make_protected_request(app.clone(), Some(JWT_RSA1_OK), "world").await.unwrap(); 148 | assert_eq!(r.get_ref().message, "Hello, world"); 149 | } 150 | 151 | #[tokio::test] 152 | async fn wrong_token() { 153 | init_test(); 154 | let auth: JwtAuthorizer = JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem"); 155 | let app = app(auth, "b@b.com".to_string()).await; 156 | let status = make_protected_request(app.clone(), Some(JWT_RSA2_OK), "world") 157 | .await 158 | .unwrap_err(); 159 | assert_eq!(status.code(), tonic::Code::Unauthenticated); 160 | } 161 | 162 | #[tokio::test] 163 | async fn no_token() { 164 | init_test(); 165 | let auth: JwtAuthorizer = JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem"); 166 | let app = app(auth, "b@b.com".to_string()).await; 167 | let status = make_protected_request(app.clone(), None, "world").await.unwrap_err(); 168 | assert_eq!(status.code(), tonic::Code::Unauthenticated); 169 | } 170 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width=125 --------------------------------------------------------------------------------