mirror of
https://github.com/TECHNOFAB11/jwt-authorizer.git
synced 2025-12-10 23:20:05 +01:00
chore: fmt
This commit is contained in:
parent
f77a7ce54f
commit
b0667729a3
32 changed files with 3596 additions and 7 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -1,10 +1,5 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
.idea/
|
||||
.vscode
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
|
|
|||
1977
Cargo.lock
generated
Normal file
1977
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
5
Cargo.toml
Normal file
5
Cargo.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"demo-server",
|
||||
"jwt-authorizer",
|
||||
]
|
||||
40
README.md
Normal file
40
README.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# jwt-authorizer
|
||||
|
||||
JWT authorizer Layer for Axum.
|
||||
|
||||
## Features
|
||||
|
||||
- JWT token verification (Bearer)
|
||||
- Claims extraction
|
||||
- JWKS endpoint support (with refresh)
|
||||
- algoritms: ECDSA, RSA, EdDSA, HS
|
||||
|
||||
## Usage
|
||||
|
||||
See documentation of the [`jwt-authorizer`](./jwt-authorizer/docs/README.md) module or the [`demo-server`](./demo-server/) example.
|
||||
|
||||
## Development
|
||||
|
||||
### Key generation
|
||||
|
||||
EC (ECDSA) - (algorigthm ES256 - ECDSA using SHA-256)
|
||||
|
||||
curve name: prime256v1 (secp256r1, secp384r1)
|
||||
|
||||
> openssl ecparam -genkey -noout -name prime256v1 | openssl pkcs8 -topk8 -nocrypt -out ec-private.pem
|
||||
|
||||
> openssl ec -in ec-private.pem -pubout -out ec-public-key.pem
|
||||
|
||||
EdDSA (Edwards-curve Digital Signature Algorithm)
|
||||
|
||||
(Ed25519 - implémentation spécifique de EdDSA, utilisant la Courbe d'Edwards tordue)
|
||||
|
||||
> openssl genpkey -algorithm ed25519
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are wellcome!
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
5
config/ec256-private.pem
Normal file
5
config/ec256-private.pem
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgH614zrAA67VgIGav
|
||||
JQU718PqSvV95Z/QO0I4oxxpekmhRANCAAQxmLBzkRU/8Te+R3agp52viVZUw32+
|
||||
B3IEGkEhUUmPBtZ6S1O+QejNJm9Nc1AFaHmCUgzpUCZr4wXaiz4yb8t2
|
||||
-----END PRIVATE KEY-----
|
||||
4
config/ec256-public.pem
Normal file
4
config/ec256-public.pem
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMZiwc5EVP/E3vkd2oKedr4lWVMN9
|
||||
vgdyBBpBIVFJjwbWektTvkHozSZvTXNQBWh5glIM6VAma+MF2os+Mm/Ldg==
|
||||
-----END PUBLIC KEY-----
|
||||
3
config/ed25519-private.pem
Normal file
3
config/ed25519-private.pem
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIEoAdOduNX+lt7bOPEkqAOwYabpsGhyxiAaUbMw184Ca
|
||||
-----END PRIVATE KEY-----
|
||||
3
config/ed25519-public.pem
Normal file
3
config/ed25519-public.pem
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEAuWtSkE+I9aTMYTTvuTE1rtu0rNdxp3DU33cJ/ksL1Gk=
|
||||
-----END PUBLIC KEY-----
|
||||
8
config/ed256-jwk.json
Normal file
8
config/ed256-jwk.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"kty": "OKP",
|
||||
"use": "sig",
|
||||
"crv": "Ed25519",
|
||||
"x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk",
|
||||
"kid": "key-ed",
|
||||
"alg": "EdDSA"
|
||||
}
|
||||
55
config/jwks.json
Normal file
55
config/jwks.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"kty": "RSA",
|
||||
"n": "2pQeZdxa7q093K7bj5h6-leIpxfTnuAxzXdhjfGEJHxmt2ekHyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy_tw-5e-Px1xFj1PykGEkRlOpYSAeWsNaAWvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV-fcGGLhJnXl0-5_z7tKC7RvBoT3SGwlc_AmJqpFtTpEBn_fDnyqiZbpcjXYLExFpExm41xDitRKHWIwfc3dV8_vlNntlxCPGy_THkjdXJoHv2IJmlhvmr5_h03iGMLWDKSywxOol_4Wc1BT7Hb6byMxW40GKwSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO-XiVShZRVg7JeraGAfWwaIgIJ1D8C1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4NDX-A4KRMgaxcfAcui_x6gybksq6gF90-9nfQfmVMVJctZ6M-FvRr-itd1Nef5WAtwUp1qyZygAXU3cH3rarscajmurOsP6dE1OHl3grY_eZhQxk33VBK9lavqNKPg6Q_PLiq1ojbYBj3bcYifJrsNeQwxldQP83aWt5rGtgZTehKVJwa40Uy_Grae1iRnsDtdSy5sTJIJ6EiShnWAdMoGejdiI8vpkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPWc6JEotQqI0HwhzU0KHyoY4s",
|
||||
"e": "AQAB",
|
||||
"kid": "key-rsa",
|
||||
"alg": "RS256",
|
||||
"use": "sig"
|
||||
},
|
||||
{
|
||||
"kty": "RSA",
|
||||
"n": "yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ",
|
||||
"e": "AQAB",
|
||||
"kid": "rsa01",
|
||||
"alg": "RS256",
|
||||
"use": "sig"
|
||||
},
|
||||
{
|
||||
"kty": "EC",
|
||||
"crv": "P-256",
|
||||
"x": "MZiwc5EVP_E3vkd2oKedr4lWVMN9vgdyBBpBIVFJjwY",
|
||||
"y": "1npLU75B6M0mb01zUAVoeYJSDOlQJmvjBdqLPjJvy3Y",
|
||||
"kid": "key-ec",
|
||||
"alg": "ES256",
|
||||
"use": "sig"
|
||||
},
|
||||
{
|
||||
"kty": "EC",
|
||||
"crv": "P-256",
|
||||
"x": "w7JAoU_gJbZJvV-zCOvU9yFJq0FNC_edCMRM78P8eQQ",
|
||||
"y": "wQg1EytcsEmGrM70Gb53oluoDbVhCZ3Uq3hHMslHVb4",
|
||||
"kid": "ec01",
|
||||
"alg": "ES256",
|
||||
"use": "sig"
|
||||
},
|
||||
{
|
||||
"kty": "OKP",
|
||||
"use": "sig",
|
||||
"crv": "Ed25519",
|
||||
"x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk",
|
||||
"kid": "key-ed",
|
||||
"alg": "EdDSA"
|
||||
},
|
||||
{
|
||||
"kty": "OKP",
|
||||
"use": "sig",
|
||||
"crv": "Ed25519",
|
||||
"x": "2-Jj2UvNCvQiUPNYRgSi0cJSPiJI6Rs6D0UTeEpQVj8",
|
||||
"kid": "ed01",
|
||||
"alg": "EdDSA"
|
||||
}
|
||||
]
|
||||
}
|
||||
51
config/jwtRS256.key
Normal file
51
config/jwtRS256.key
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
||||
14
config/jwtRS256.key.pub
Normal file
14
config/jwtRS256.key.pub
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2pQeZdxa7q093K7bj5h6
|
||||
+leIpxfTnuAxzXdhjfGEJHxmt2ekHyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy/tw+5
|
||||
e+Px1xFj1PykGEkRlOpYSAeWsNaAWvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV
|
||||
+fcGGLhJnXl0+5/z7tKC7RvBoT3SGwlc/AmJqpFtTpEBn/fDnyqiZbpcjXYLExFp
|
||||
Exm41xDitRKHWIwfc3dV8/vlNntlxCPGy/THkjdXJoHv2IJmlhvmr5/h03iGMLWD
|
||||
KSywxOol/4Wc1BT7Hb6byMxW40GKwSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO+
|
||||
XiVShZRVg7JeraGAfWwaIgIJ1D8C1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4N
|
||||
DX+A4KRMgaxcfAcui/x6gybksq6gF90+9nfQfmVMVJctZ6M+FvRr+itd1Nef5WAt
|
||||
wUp1qyZygAXU3cH3rarscajmurOsP6dE1OHl3grY/eZhQxk33VBK9lavqNKPg6Q/
|
||||
PLiq1ojbYBj3bcYifJrsNeQwxldQP83aWt5rGtgZTehKVJwa40Uy/Grae1iRnsDt
|
||||
dSy5sTJIJ6EiShnWAdMoGejdiI8vpkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPW
|
||||
c6JEotQqI0HwhzU0KHyoY4sCAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
||||
5
config/private_ecdsa_key.pem
Normal file
5
config/private_ecdsa_key.pem
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWTFfCGljY6aw3Hrt
|
||||
kHmPRiazukxPLb6ilpRAewjW8nihRANCAATDskChT+Altkm9X7MI69T3IUmrQU0L
|
||||
950IxEzvw/x5BMEINRMrXLBJhqzO9Bm+d6JbqA21YQmd1Kt4RzLJR1W+
|
||||
-----END PRIVATE KEY-----
|
||||
3
config/private_ed25519_key.pem
Normal file
3
config/private_ed25519_key.pem
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIGrD/e7uKYqSY4twDEsRfMMuLSrODf14dpTiTK6K1YI0
|
||||
-----END PRIVATE KEY-----
|
||||
28
config/private_rsa_key_pkcs8.pem
Normal file
28
config/private_rsa_key_pkcs8.pem
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJETqse41HRBsc
|
||||
7cfcq3ak4oZWFCoZlcic525A3FfO4qW9BMtRO/iXiyCCHn8JhiL9y8j5JdVP2Q9Z
|
||||
IpfElcFd3/guS9w+5RqQGgCR+H56IVUyHZWtTJbKPcwWXQdNUX0rBFcsBzCRESJL
|
||||
eelOEdHIjG7LRkx5l/FUvlqsyHDVJEQsHwegZ8b8C0fz0EgT2MMEdn10t6Ur1rXz
|
||||
jMB/wvCg8vG8lvciXmedyo9xJ8oMOh0wUEgxziVDMMovmC+aJctcHUAYubwoGN8T
|
||||
yzcvnGqL7JSh36Pwy28iPzXZ2RLhAyJFU39vLaHdljwthUaupldlNyCfa6Ofy4qN
|
||||
ctlUPlN1AgMBAAECggEAdESTQjQ70O8QIp1ZSkCYXeZjuhj081CK7jhhp/4ChK7J
|
||||
GlFQZMwiBze7d6K84TwAtfQGZhQ7km25E1kOm+3hIDCoKdVSKch/oL54f/BK6sKl
|
||||
qlIzQEAenho4DuKCm3I4yAw9gEc0DV70DuMTR0LEpYyXcNJY3KNBOTjN5EYQAR9s
|
||||
2MeurpgK2MdJlIuZaIbzSGd+diiz2E6vkmcufJLtmYUT/k/ddWvEtz+1DnO6bRHh
|
||||
xuuDMeJA/lGB/EYloSLtdyCF6sII6C6slJJtgfb0bPy7l8VtL5iDyz46IKyzdyzW
|
||||
tKAn394dm7MYR1RlUBEfqFUyNK7C+pVMVoTwCC2V4QKBgQD64syfiQ2oeUlLYDm4
|
||||
CcKSP3RnES02bcTyEDFSuGyyS1jldI4A8GXHJ/lG5EYgiYa1RUivge4lJrlNfjyf
|
||||
dV230xgKms7+JiXqag1FI+3mqjAgg4mYiNjaao8N8O3/PD59wMPeWYImsWXNyeHS
|
||||
55rUKiHERtCcvdzKl4u35ZtTqQKBgQDNKnX2bVqOJ4WSqCgHRhOm386ugPHfy+8j
|
||||
m6cicmUR46ND6ggBB03bCnEG9OtGisxTo/TuYVRu3WP4KjoJs2LD5fwdwJqpgtHl
|
||||
yVsk45Y1Hfo+7M6lAuR8rzCi6kHHNb0HyBmZjysHWZsn79ZM+sQnLpgaYgQGRbKV
|
||||
DZWlbw7g7QKBgQCl1u+98UGXAP1jFutwbPsx40IVszP4y5ypCe0gqgon3UiY/G+1
|
||||
zTLp79GGe/SjI2VpQ7AlW7TI2A0bXXvDSDi3/5Dfya9ULnFXv9yfvH1QwWToySpW
|
||||
Kvd1gYSoiX84/WCtjZOr0e0HmLIb0vw0hqZA4szJSqoxQgvF22EfIWaIaQKBgQCf
|
||||
34+OmMYw8fEvSCPxDxVvOwW2i7pvV14hFEDYIeZKW2W1HWBhVMzBfFB5SE8yaCQy
|
||||
pRfOzj9aKOCm2FjjiErVNpkQoi6jGtLvScnhZAt/lr2TXTrl8OwVkPrIaN0bG/AS
|
||||
aUYxmBPCpXu3UjhfQiWqFq/mFyzlqlgvuCc9g95HPQKBgAscKP8mLxdKwOgX8yFW
|
||||
GcZ0izY/30012ajdHY+/QK5lsMoxTnn0skdS+spLxaS5ZEO4qvPVb8RAoCkWMMal
|
||||
2pOhmquJQVDPDLuZHdrIiKiDM20dy9sMfHygWcZjQ4WSxf/J7T9canLZIXFhHAZT
|
||||
3wc9h4G8BBCtWN2TN/LsGZdB
|
||||
-----END PRIVATE KEY-----
|
||||
4
config/public_ecdsa_key.pem
Normal file
4
config/public_ecdsa_key.pem
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEw7JAoU/gJbZJvV+zCOvU9yFJq0FN
|
||||
C/edCMRM78P8eQTBCDUTK1ywSYaszvQZvneiW6gNtWEJndSreEcyyUdVvg==
|
||||
-----END PUBLIC KEY-----
|
||||
3
config/public_ed25519_key.pem
Normal file
3
config/public_ed25519_key.pem
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEA2+Jj2UvNCvQiUPNYRgSi0cJSPiJI6Rs6D0UTeEpQVj8=
|
||||
-----END PUBLIC KEY-----
|
||||
23
demo-server/Cargo.toml
Normal file
23
demo-server/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "demo-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.64"
|
||||
axum = { version = "0.6.1", features = ["headers"] }
|
||||
headers = "0.3"
|
||||
josekit = "0.8.1"
|
||||
jsonwebtoken = "8.2.0"
|
||||
once_cell = "1.8"
|
||||
reqwest = { version = "0.11.11", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0.34"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tower-http = { version = "0.3.4", features = ["trace"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
jwt-authorizer = { path = "../jwt-authorizer" }
|
||||
35
demo-server/request.http
Normal file
35
demo-server/request.http
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
###
|
||||
POST http://localhost:3000/oidc/authorize
|
||||
Content-Type: application/json
|
||||
|
||||
{"client_id":"foo","client_secret":"bar"}
|
||||
|
||||
### Protected RSA
|
||||
GET http://localhost:3000/api/protected
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtleS1yc2EifQ.eyJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwfQ.K9QFjvVquRF2-Wt1QRfipOGwiYsmRs7SAwqKskHemFb9BRRZutpfV4oEoHaXMLomTUe8rH0TMjpKcweYK_H1I8D4r-mAN216oUfxCQiFWDB8T2VBI8um-efUg67i2myDZJr5VXdZH8ywj7bn9LyNS4I_xT-J3XvsngeCpuxVSRiYu4FkcUkLrPzbu2sDyBXFqYO9FOorZ8sl0Ninc93fWT2uUrEG8jRyWCa4xpoqbKbm7CN7T2tOKF7mx_xdSPTeSM-U9mUiHsMOrXi1S05IM0hvNJrBduLS6sMTFWrVhis6zqnuxDOirwZS-aN0_SgMDnZTFPsCh8dkqFde1Pv1IYjZfr5OOHjQ9QWj6UDjam6M1eWVPK6QLlxv5bU_gnlAiHm9wJX38-REwmVhIJIBzKxsgJAu1gnRBxe36OM3rkgYxpB86YvfDyOoFlqx8erdxYv38AtvJibe4HB6KLndp_QMm5XXQsbfyEXWGe8hzDwozdhGeQsJXz7PcI3KPlv19PrUM8njElFpOiyfAEXwbtp1EZTzMZ4ZNF6LLFy1fpLcosgyp05o_2YMvngltSnN3v0IPncJx50StdYsoxPN9Ac_nH8VbNlHfmPHMklD1plof0pYf5SiL8yCQP9Uiw9NrN2PeQzbveMKF1T1UNbn2tefxoxr3k6sgWiMH_g_kkk
|
||||
|
||||
### Protected EC
|
||||
GET http://localhost:3000/api/protected
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImtleS1lYyJ9.eyJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwfQ.r0qaeYJWhjEybhNPgrrFwLBTeLMYtL4IJqOxZyxH9m-JYWqzV0R0WyYQIkf_tQ1UmzqHc9_xzUZnzSjTeEwDHw
|
||||
|
||||
### Protected Ed
|
||||
GET http://localhost:3000/api/protected
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImtleS1lZCJ9.eyJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwfQ.XAx9msioheXEH1XUEIWMHGBg25JOpBHqcgL_ou_S3fwVht2TbKRiDZ4G6ZyEtn57hCbOy250zTD_g0EbaMGwAg
|
||||
|
||||
###
|
||||
GET http://localhost:3000/api/protected
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer blablabla.xxxx.xxxx
|
||||
|
||||
|
||||
###
|
||||
GET http://localhost:3000/oidc/jwks
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
###
|
||||
GET http://localhost:3000/oidc/tokens
|
||||
Content-Type: application/json
|
||||
70
demo-server/src/main.rs
Normal file
70
demo-server/src/main.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
use axum::{
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use jwt_authorizer::{AuthError, JwtAuthorizer, JwtClaims};
|
||||
use serde::Deserialize;
|
||||
use std::{fmt::Display, net::SocketAddr};
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
mod oidc_provider;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::new(
|
||||
std::env::var("RUST_LOG").unwrap_or_else(|_| "info,axum_poc=debug,tower_http=debug".into()),
|
||||
))
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
fn claim_checker(u: &User) -> bool {
|
||||
info!("checking claims: {} -> {}", u.sub, u.sub.contains('@'));
|
||||
|
||||
u.sub.contains('@') // must be an email
|
||||
}
|
||||
|
||||
// First let's create an authorizer builder from a JWKS Endpoint
|
||||
// User is a struct deserializable from JWT claims representing the authorized user
|
||||
let jwt_auth: JwtAuthorizer<User> = JwtAuthorizer::new()
|
||||
.from_jwks_url("http://localhost:3000/oidc/jwks")
|
||||
.with_check(claim_checker);
|
||||
|
||||
let oidc = Router::new()
|
||||
.route("/authorize", post(oidc_provider::authorize))
|
||||
.route("/jwks", get(oidc_provider::jwks))
|
||||
.route("/tokens", get(oidc_provider::tokens));
|
||||
|
||||
let api = Router::new()
|
||||
.route("/protected", get(protected))
|
||||
.layer(jwt_auth.layer());
|
||||
// .layer(jwt_auth.check_claims(|_: User| true));
|
||||
|
||||
let app = Router::new()
|
||||
.nest("/oidc/", oidc)
|
||||
.nest("/api", api)
|
||||
.layer(TraceLayer::new_for_http());
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
tracing::info!("listening on {}", addr);
|
||||
|
||||
axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();
|
||||
}
|
||||
|
||||
async fn protected(JwtClaims(user): JwtClaims<User>) -> Result<String, AuthError> {
|
||||
// Send the protected data to the user
|
||||
Ok(format!("Welcome: {}", user.sub))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct User {
|
||||
sub: String,
|
||||
}
|
||||
|
||||
impl Display for User {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "User: {:?}", self.sub)
|
||||
}
|
||||
}
|
||||
240
demo-server/src/oidc_provider/mod.rs
Normal file
240
demo-server/src/oidc_provider/mod.rs
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
use axum::{
|
||||
async_trait,
|
||||
extract::{FromRequestParts, TypedHeader},
|
||||
headers::{authorization::Bearer, Authorization},
|
||||
http::{request::Parts, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use josekit::jwk::{
|
||||
alg::{ec::EcCurve, ec::EcKeyPair, ed::EdKeyPair, rsa::RsaKeyPair},
|
||||
Jwk,
|
||||
};
|
||||
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::fmt::Display;
|
||||
|
||||
pub static KEYS: Lazy<Keys> = Lazy::new(|| {
|
||||
//let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
||||
// Keys::new("xxxxx".as_bytes())
|
||||
Keys::load_rsa()
|
||||
});
|
||||
|
||||
pub struct Keys {
|
||||
pub alg: Algorithm,
|
||||
pub encoding: EncodingKey,
|
||||
pub decoding: DecodingKey,
|
||||
}
|
||||
|
||||
impl Keys {
|
||||
fn new(secret: &[u8]) -> Self {
|
||||
Self {
|
||||
alg: Algorithm::HS256,
|
||||
encoding: EncodingKey::from_secret(secret),
|
||||
decoding: DecodingKey::from_secret(secret),
|
||||
}
|
||||
}
|
||||
fn load_rsa() -> Self {
|
||||
Self {
|
||||
alg: Algorithm::RS256,
|
||||
encoding: EncodingKey::from_rsa_pem(include_bytes!("../../../config/jwtRS256.key")).unwrap(),
|
||||
decoding: DecodingKey::from_rsa_pem(include_bytes!("../../../config/jwtRS256.key.pub")).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
|
||||
struct JwkSet {
|
||||
keys: Vec<Jwk>,
|
||||
}
|
||||
|
||||
pub async fn jwks() -> Json<Value> {
|
||||
// let mut ksmap = serde_json::Map::new();
|
||||
|
||||
let mut kset = JwkSet {
|
||||
keys: Vec::<Jwk>::new(),
|
||||
};
|
||||
|
||||
let keypair = RsaKeyPair::from_pem(include_bytes!("../../../config/jwtRS256.key")).unwrap();
|
||||
let mut pk = keypair.to_jwk_public_key();
|
||||
pk.set_key_id("key-rsa");
|
||||
pk.set_algorithm("RS256");
|
||||
pk.set_key_use("sig");
|
||||
kset.keys.push(pk);
|
||||
|
||||
let keypair = RsaKeyPair::from_pem(include_bytes!("../../../config/private_rsa_key_pkcs8.pem")).unwrap();
|
||||
let mut pk = keypair.to_jwk_public_key();
|
||||
pk.set_key_id("rsa01");
|
||||
pk.set_algorithm("RS256");
|
||||
pk.set_key_use("sig");
|
||||
kset.keys.push(pk);
|
||||
|
||||
let keypair =
|
||||
EcKeyPair::from_pem(include_bytes!("../../../config/ec256-private.pem"), Some(EcCurve::P256)).unwrap();
|
||||
let mut pk = keypair.to_jwk_public_key();
|
||||
pk.set_key_id("key-ec");
|
||||
pk.set_algorithm("ES256");
|
||||
pk.set_key_use("sig");
|
||||
kset.keys.push(pk);
|
||||
|
||||
let keypair = EcKeyPair::from_pem(
|
||||
include_bytes!("../../../config/private_ecdsa_key.pem"),
|
||||
Some(EcCurve::P256),
|
||||
)
|
||||
.unwrap();
|
||||
let mut pk = keypair.to_jwk_public_key();
|
||||
pk.set_key_id("ec01");
|
||||
pk.set_algorithm("ES256");
|
||||
pk.set_key_use("sig");
|
||||
kset.keys.push(pk);
|
||||
|
||||
let keypair = EdKeyPair::from_pem(include_bytes!("../../../config/ed25519-private.pem")).unwrap();
|
||||
let mut pk = keypair.to_jwk_public_key();
|
||||
pk.set_key_id("key-ed");
|
||||
pk.set_algorithm("EdDSA");
|
||||
pk.set_key_use("sig");
|
||||
kset.keys.push(pk);
|
||||
|
||||
let keypair = EdKeyPair::from_pem(include_bytes!("../../../config/private_ed25519_key.pem")).unwrap();
|
||||
let mut pk = keypair.to_jwk_public_key();
|
||||
pk.set_key_id("ed01");
|
||||
pk.set_algorithm("EdDSA");
|
||||
pk.set_key_use("sig");
|
||||
kset.keys.push(pk);
|
||||
|
||||
Json(json!(kset))
|
||||
}
|
||||
|
||||
fn build_header(alg: Algorithm, kid: &str) -> Header {
|
||||
Header {
|
||||
typ: Some("JWT".to_string()),
|
||||
alg,
|
||||
kid: Some(kid.to_owned()),
|
||||
cty: None,
|
||||
jku: None,
|
||||
jwk: None,
|
||||
x5u: None,
|
||||
x5c: None,
|
||||
x5t: None,
|
||||
x5t_s256: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn tokens() -> Json<Value> {
|
||||
let claims = Claims {
|
||||
sub: "b@b.com".to_owned(),
|
||||
exp: 2000000000, // May 2033
|
||||
};
|
||||
|
||||
let rsa_key = EncodingKey::from_rsa_pem(include_bytes!("../../../config/jwtRS256.key")).unwrap();
|
||||
let ec_key = EncodingKey::from_ec_pem(include_bytes!("../../../config/ec256-private.pem")).unwrap();
|
||||
let ed_key = EncodingKey::from_ed_pem(include_bytes!("../../../config/ed25519-private.pem")).unwrap();
|
||||
|
||||
let rsa_token = encode(&build_header(Algorithm::RS256, "key-rsa"), &claims, &rsa_key).unwrap();
|
||||
let ec_token = encode(&build_header(Algorithm::ES256, "key-ec"), &claims, &ec_key).unwrap();
|
||||
let ed_token = encode(&build_header(Algorithm::EdDSA, "key-ed"), &claims, &ed_key).unwrap();
|
||||
|
||||
Json(json!({
|
||||
"rsa": rsa_token,
|
||||
"ec": ec_token,
|
||||
"ed": ed_token
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn authorize(Json(payload): Json<AuthPayload>) -> Result<Json<AuthBody>, AuthError> {
|
||||
tracing::info!("authorizing ...");
|
||||
if payload.client_id.is_empty() || payload.client_secret.is_empty() {
|
||||
return Err(AuthError::MissingCredentials);
|
||||
}
|
||||
// Here you can check the user credentials from a database
|
||||
if payload.client_id != "foo" || payload.client_secret != "bar" {
|
||||
return Err(AuthError::WrongCredentials);
|
||||
}
|
||||
let claims = Claims {
|
||||
sub: "b@b.com".to_owned(),
|
||||
// Mandatory expiry time as UTC timestamp
|
||||
exp: 2000000000, // May 2033
|
||||
};
|
||||
// Create the authorization token
|
||||
let token = encode(&Header::new(KEYS.alg), &claims, &KEYS.encoding).map_err(|_| AuthError::TokenCreation)?;
|
||||
|
||||
// Send the authorized token
|
||||
Ok(Json(AuthBody::new(token)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Claims {
|
||||
sub: String,
|
||||
exp: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AuthBody {
|
||||
access_token: String,
|
||||
token_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AuthPayload {
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthError {
|
||||
WrongCredentials,
|
||||
MissingCredentials,
|
||||
TokenCreation,
|
||||
InvalidToken,
|
||||
}
|
||||
|
||||
impl Display for Claims {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "sub: {}", self.sub)
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthBody {
|
||||
fn new(access_token: String) -> Self {
|
||||
Self {
|
||||
access_token,
|
||||
token_type: "Bearer".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AuthError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, error_message) = match self {
|
||||
AuthError::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials"),
|
||||
AuthError::MissingCredentials => (StatusCode::BAD_REQUEST, "Missing credentials"),
|
||||
AuthError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error"),
|
||||
AuthError::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid token"),
|
||||
};
|
||||
let body = Json(json!({
|
||||
"error": error_message,
|
||||
}));
|
||||
(status, body).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for Claims
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = AuthError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
// Extract the token from the authorization header
|
||||
let TypedHeader(Authorization(bearer)) = TypedHeader::<Authorization<Bearer>>::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_err(|_| AuthError::InvalidToken)?;
|
||||
let token_data = decode::<Claims>(bearer.token(), &KEYS.decoding, &Validation::default())
|
||||
.map_err(|_| AuthError::InvalidToken)?;
|
||||
|
||||
Ok(token_data.claims)
|
||||
}
|
||||
}
|
||||
33
jwt-authorizer/Cargo.toml
Normal file
33
jwt-authorizer/Cargo.toml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
name = "jwt-authorizer"
|
||||
description = "jwt authorizer middleware for axum"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
authors = ["cduvray <c_duvray@proton.me>"]
|
||||
license = "MIT"
|
||||
readme = "docs/README.md"
|
||||
repository = "https://github.com/cduvray/jwt-authorizer"
|
||||
keywords = ["jwt","axum","authorisation"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.6.1", features = ["headers"] }
|
||||
futures-util = "0.3.25"
|
||||
futures-core = "0.3.25"
|
||||
headers = "0.3"
|
||||
jsonwebtoken = "8.2.0"
|
||||
http = "0.2.8"
|
||||
# pin-project-lite = "0.2.9"
|
||||
pin-project = "1.0.12"
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0.37"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tower-http = { version = "0.3.4", features = ["trace", "auth"] }
|
||||
tower-layer = "0.3"
|
||||
tower-service = "0.3"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wiremock = "0.5"
|
||||
0
jwt-authorizer/clippy.toml
Normal file
0
jwt-authorizer/clippy.toml
Normal file
36
jwt-authorizer/docs/README.md
Normal file
36
jwt-authorizer/docs/README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# jwt-authorizer
|
||||
|
||||
JWT authoriser Layer for Axum.
|
||||
|
||||
Example:
|
||||
|
||||
```rust
|
||||
use jwt_authorizer::{AuthError, JwtAuthorizer, JwtClaims};
|
||||
use axum::{routing::get, Router};
|
||||
use serde::Deserialize;
|
||||
|
||||
// Authorized entity, struct deserializable from JWT claims
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct User {
|
||||
sub: String,
|
||||
}
|
||||
|
||||
// let's create an authorizer builder from a JWKS Endpoint
|
||||
let jwt_auth: JwtAuthorizer<User> = JwtAuthorizer::new()
|
||||
.from_jwks_url("http://localhost:3000/oidc/jwks");
|
||||
|
||||
// adding the authorization layer
|
||||
let app = Router::new().route("/protected", get(protected))
|
||||
.layer(jwt_auth.layer());
|
||||
|
||||
// proteced handler with user injection (mapping some jwt claims)
|
||||
async fn protected(JwtClaims(user): JwtClaims<User>) -> Result<String, AuthError> {
|
||||
// Send the protected data to the user
|
||||
Ok(format!("Welcome: {}", user.sub))
|
||||
}
|
||||
|
||||
# async {
|
||||
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
|
||||
.serve(app.into_make_service()).await.expect("server failed");
|
||||
# };
|
||||
```
|
||||
170
jwt-authorizer/src/authorizer.rs
Normal file
170
jwt-authorizer/src/authorizer.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
use std::{io::Read, time::Duration};
|
||||
|
||||
use jsonwebtoken::{decode, decode_header, jwk::JwkSet, DecodingKey, TokenData, Validation};
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::{
|
||||
error::AuthError,
|
||||
jwks::{key_store_manager::KeyStoreManager, KeySource},
|
||||
};
|
||||
|
||||
pub trait ClaimsChecker<C> {
|
||||
fn check(&self, claims: &C) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FnClaimsChecker<C>
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
pub checker_fn: fn(&C) -> bool,
|
||||
}
|
||||
|
||||
impl<C> ClaimsChecker<C> for FnClaimsChecker<C>
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
fn check(&self, claims: &C) -> bool {
|
||||
(self.checker_fn)(claims)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Authorizer<C>
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
pub key_source: KeySource,
|
||||
pub claims_checker: Option<FnClaimsChecker<C>>,
|
||||
}
|
||||
|
||||
fn read_data(path: &str) -> Result<Vec<u8>, AuthError> {
|
||||
let mut data = Vec::<u8>::new();
|
||||
let mut f = std::fs::File::open(path)?;
|
||||
f.read_to_end(&mut data)?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
impl<C> Authorizer<C>
|
||||
where
|
||||
C: DeserializeOwned + Clone + Send + Sync,
|
||||
{
|
||||
pub fn from_jwks(jwks: &str, claims_checker: Option<FnClaimsChecker<C>>) -> Result<Authorizer<C>, AuthError> {
|
||||
let set: JwkSet = serde_json::from_str(jwks)?;
|
||||
let k = DecodingKey::from_jwk(&set.keys[0])?;
|
||||
|
||||
Ok(Authorizer {
|
||||
key_source: KeySource::DecodingKeySource(k),
|
||||
claims_checker,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_rsa_file(path: &str) -> Result<Authorizer<C>, AuthError> {
|
||||
Ok(Authorizer {
|
||||
key_source: KeySource::DecodingKeySource(DecodingKey::from_rsa_pem(&read_data(path)?)?),
|
||||
claims_checker: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_ec_file(path: &str) -> Result<Authorizer<C>, AuthError> {
|
||||
let k = DecodingKey::from_ec_der(&read_data(path)?);
|
||||
Ok(Authorizer {
|
||||
key_source: KeySource::DecodingKeySource(k),
|
||||
claims_checker: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_ed_file(path: &str) -> Result<Authorizer<C>, AuthError> {
|
||||
let k = DecodingKey::from_ed_der(&read_data(path)?);
|
||||
Ok(Authorizer {
|
||||
key_source: KeySource::DecodingKeySource(k),
|
||||
claims_checker: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_secret(secret: &str) -> Result<Authorizer<C>, AuthError> {
|
||||
let k = DecodingKey::from_secret(secret.as_bytes());
|
||||
Ok(Authorizer {
|
||||
key_source: KeySource::DecodingKeySource(k),
|
||||
claims_checker: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_jwks_url(url: &str, claims_checker: Option<FnClaimsChecker<C>>) -> Result<Authorizer<C>, AuthError> {
|
||||
let key_store_manager = KeyStoreManager::with_refresh_interval(url, Duration::from_secs(60));
|
||||
Ok(Authorizer {
|
||||
key_source: KeySource::KeyStoreSource(key_store_manager),
|
||||
claims_checker,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn check_auth(&self, token: &str) -> Result<TokenData<C>, AuthError> {
|
||||
let header = decode_header(token)?;
|
||||
let validation = Validation::new(header.alg);
|
||||
let decoding_key = self.key_source.get_key(header).await?;
|
||||
let token_data = decode::<C>(token, &decoding_key, &validation)?;
|
||||
|
||||
if let Some(ref checker) = self.claims_checker {
|
||||
if !checker.check(&token_data.claims) {
|
||||
return Err(AuthError::InvalidClaims());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(token_data)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use jsonwebtoken::{Algorithm, Header};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::Authorizer;
|
||||
|
||||
#[tokio::test]
|
||||
async fn from_secret() {
|
||||
let h = Header::new(Algorithm::HS256);
|
||||
let a = Authorizer::<Value>::from_secret("xxxxxx").unwrap();
|
||||
let k = a.key_source.get_key(h);
|
||||
assert!(k.await.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn from_jwks() {
|
||||
let jwks = r#"
|
||||
{"keys": [{
|
||||
"kid": "1",
|
||||
"kty": "RSA",
|
||||
"alg": "RS256",
|
||||
"use": "sig",
|
||||
"n": "2pQeZdxa7q093K7bj5h6-leIpxfTnuAxzXdhjfGEJHxmt2ekHyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy_tw-5e-Px1xFj1PykGEkRlOpYSAeWsNaAWvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV-fcGGLhJnXl0-5_z7tKC7RvBoT3SGwlc_AmJqpFtTpEBn_fDnyqiZbpcjXYLExFpExm41xDitRKHWIwfc3dV8_vlNntlxCPGy_THkjdXJoHv2IJmlhvmr5_h03iGMLWDKSywxOol_4Wc1BT7Hb6byMxW40GKwSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO-XiVShZRVg7JeraGAfWwaIgIJ1D8C1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4NDX-A4KRMgaxcfAcui_x6gybksq6gF90-9nfQfmVMVJctZ6M-FvRr-itd1Nef5WAtwUp1qyZygAXU3cH3rarscajmurOsP6dE1OHl3grY_eZhQxk33VBK9lavqNKPg6Q_PLiq1ojbYBj3bcYifJrsNeQwxldQP83aWt5rGtgZTehKVJwa40Uy_Grae1iRnsDtdSy5sTJIJ6EiShnWAdMoGejdiI8vpkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPWc6JEotQqI0HwhzU0KHyoY4s",
|
||||
"e": "AQAB"
|
||||
}]}
|
||||
"#;
|
||||
let a = Authorizer::<Value>::from_jwks(jwks, None).unwrap();
|
||||
let k = a.key_source.get_key(Header::new(Algorithm::RS256));
|
||||
assert!(k.await.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn from_file() {
|
||||
let a = Authorizer::<Value>::from_rsa_file("../config/jwtRS256.key.pub").unwrap();
|
||||
let k = a.key_source.get_key(Header::new(Algorithm::RS256));
|
||||
assert!(k.await.is_ok());
|
||||
|
||||
let a = Authorizer::<Value>::from_ec_file("../config/ec256-public.pem").unwrap();
|
||||
let k = a.key_source.get_key(Header::new(Algorithm::ES256));
|
||||
assert!(k.await.is_ok());
|
||||
|
||||
let a = Authorizer::<Value>::from_ed_file("../config/ed25519-public.pem").unwrap();
|
||||
let k = a.key_source.get_key(Header::new(Algorithm::EdDSA));
|
||||
assert!(k.await.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn from_file_errors() {
|
||||
let a = Authorizer::<Value>::from_rsa_file("./config/does-not-exist.pem");
|
||||
println!("{:?}", a.as_ref().err());
|
||||
assert!(a.is_err());
|
||||
}
|
||||
}
|
||||
60
jwt-authorizer/src/error.rs
Normal file
60
jwt-authorizer/src/error.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
use axum::{
|
||||
extract::rejection::TypedHeaderRejection,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use jsonwebtoken::Algorithm;
|
||||
use thiserror::Error;
|
||||
|
||||
use tracing::log::warn;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AuthError {
|
||||
#[error(transparent)]
|
||||
JwksSerialisationError(#[from] serde_json::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
JwksRefreshError(#[from] reqwest::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
KeyFileError(#[from] std::io::Error),
|
||||
|
||||
#[error("InvalidKey {0}")]
|
||||
InvalidKey(String),
|
||||
|
||||
#[error("Invalid Kid {0}")]
|
||||
InvalidKid(String),
|
||||
|
||||
#[error("Invalid Key Algorithm {0:?}")]
|
||||
InvalidKeyAlg(Algorithm),
|
||||
|
||||
#[error(transparent)]
|
||||
InvalidTokenHeader(#[from] TypedHeaderRejection),
|
||||
|
||||
#[error(transparent)]
|
||||
InvalidToken(#[from] jsonwebtoken::errors::Error),
|
||||
|
||||
#[error("Invalid Claim")]
|
||||
InvalidClaims(),
|
||||
}
|
||||
|
||||
impl IntoResponse for AuthError {
|
||||
fn into_response(self) -> Response {
|
||||
warn!("AuthError: {}", &self);
|
||||
let (status, error_message) = match self {
|
||||
AuthError::JwksRefreshError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
|
||||
AuthError::KeyFileError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
|
||||
AuthError::InvalidKid(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
|
||||
AuthError::InvalidTokenHeader(_) => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||
AuthError::InvalidToken(_) => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||
AuthError::InvalidKey(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
|
||||
AuthError::JwksSerialisationError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
|
||||
AuthError::InvalidKeyAlg(_) => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||
AuthError::InvalidClaims() => (StatusCode::FORBIDDEN, self.to_string()),
|
||||
};
|
||||
let body = axum::Json(serde_json::json!({
|
||||
"error": error_message,
|
||||
}));
|
||||
(status, body).into_response()
|
||||
}
|
||||
}
|
||||
373
jwt-authorizer/src/jwks/key_store_manager.rs
Normal file
373
jwt-authorizer/src/jwks/key_store_manager.rs
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
use jsonwebtoken::{
|
||||
jwk::{Jwk, JwkSet},
|
||||
Algorithm, DecodingKey,
|
||||
};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::error::AuthError;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum RefreshStrategy {
|
||||
/// refresh periodicaly
|
||||
Interval(Duration),
|
||||
/// when kid not found in the store
|
||||
KeyNotFound,
|
||||
// other strategies? KeyNotFoundOrInterval(Duration), Once,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KeyStoreManager {
|
||||
key_url: String,
|
||||
refresh: RefreshStrategy,
|
||||
/// in case of fail loading (error or key not found), minimal interval
|
||||
minimal_refresh_interval: Duration,
|
||||
keystore: Arc<Mutex<KeyStore>>,
|
||||
}
|
||||
|
||||
pub struct KeyStore {
|
||||
/// key set
|
||||
jwks: JwkSet,
|
||||
/// time of the last successfully loaded jwkset
|
||||
load_time: Option<Instant>,
|
||||
/// time of the last failed load
|
||||
fail_time: Option<Instant>,
|
||||
}
|
||||
|
||||
impl KeyStoreManager {
|
||||
pub(crate) fn new(url: &str, refresh: RefreshStrategy) -> KeyStoreManager {
|
||||
KeyStoreManager {
|
||||
key_url: url.to_owned(),
|
||||
refresh,
|
||||
minimal_refresh_interval: Duration::from_secs(5), // TODO: make configurable
|
||||
keystore: Arc::new(Mutex::new(KeyStore {
|
||||
jwks: JwkSet { keys: vec![] },
|
||||
load_time: None,
|
||||
fail_time: None,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_refresh(url: &str) -> KeyStoreManager {
|
||||
KeyStoreManager::new(url, RefreshStrategy::KeyNotFound)
|
||||
}
|
||||
|
||||
pub(crate) fn with_refresh_interval(url: &str, interval: Duration) -> KeyStoreManager {
|
||||
KeyStoreManager::new(url, RefreshStrategy::Interval(interval))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_key(&self, header: &jsonwebtoken::Header) -> Result<jsonwebtoken::DecodingKey, AuthError> {
|
||||
let kstore = self.keystore.clone();
|
||||
let mut ks_gard = kstore.lock().await;
|
||||
let key = match self.refresh {
|
||||
RefreshStrategy::Interval(refresh_interval) => {
|
||||
if ks_gard.should_refresh(refresh_interval) && ks_gard.can_refresh(self.minimal_refresh_interval) {
|
||||
ks_gard.refresh(&self.key_url, &[]).await?;
|
||||
}
|
||||
if let Some(ref kid) = header.kid {
|
||||
ks_gard
|
||||
.find_kid(kid)
|
||||
.ok_or_else(|| AuthError::InvalidKid(kid.to_owned()))?
|
||||
} else {
|
||||
ks_gard
|
||||
.find_alg(&header.alg)
|
||||
.ok_or(AuthError::InvalidKeyAlg(header.alg))?
|
||||
}
|
||||
}
|
||||
RefreshStrategy::KeyNotFound => {
|
||||
if let Some(ref kid) = header.kid {
|
||||
let jwk_opt = ks_gard.find_kid(kid);
|
||||
if let Some(jwk) = jwk_opt {
|
||||
jwk
|
||||
} else if ks_gard.can_refresh(self.minimal_refresh_interval) {
|
||||
ks_gard.refresh(&self.key_url, &[("kid", kid)]).await?;
|
||||
ks_gard
|
||||
.find_kid(kid)
|
||||
.ok_or_else(|| AuthError::InvalidKid(kid.to_owned()))?
|
||||
} else {
|
||||
return Err(AuthError::InvalidKid(kid.to_owned()));
|
||||
}
|
||||
} else {
|
||||
let jwk_opt = ks_gard.find_alg(&header.alg);
|
||||
// .ok_or(AuthError::InvalidKeyAlg(header.alg))?
|
||||
if let Some(jwk) = jwk_opt {
|
||||
jwk
|
||||
} else if ks_gard.can_refresh(self.minimal_refresh_interval) {
|
||||
ks_gard
|
||||
.refresh(
|
||||
&self.key_url,
|
||||
&[(
|
||||
"alg",
|
||||
&serde_json::to_string(&header.alg)
|
||||
.map_err(|_| AuthError::InvalidKeyAlg(header.alg))?,
|
||||
)],
|
||||
)
|
||||
.await?;
|
||||
ks_gard
|
||||
.find_alg(&header.alg)
|
||||
.ok_or_else(|| AuthError::InvalidKeyAlg(header.alg))?
|
||||
} else {
|
||||
return Err(AuthError::InvalidKeyAlg(header.alg));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DecodingKey::from_jwk(key).map_err(|err| AuthError::InvalidKey(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyStore {
|
||||
fn should_refresh(&self, refresh_interval: Duration) -> bool {
|
||||
if let Some(t) = self.load_time {
|
||||
t.elapsed() > refresh_interval
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn can_refresh(&self, minimal_refresh_interval: Duration) -> bool {
|
||||
if let Some(ft) = self.fail_time {
|
||||
if let Some(lt) = self.load_time {
|
||||
ft.elapsed() > minimal_refresh_interval && lt.elapsed() > minimal_refresh_interval
|
||||
} else {
|
||||
ft.elapsed() > minimal_refresh_interval
|
||||
}
|
||||
} else if let Some(lt) = self.load_time {
|
||||
lt.elapsed() > minimal_refresh_interval
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh(&mut self, key_url: &str, qparam: &[(&str, &str)]) -> Result<(), AuthError> {
|
||||
reqwest::Client::new()
|
||||
.get(key_url)
|
||||
.query(qparam)
|
||||
.send()
|
||||
.await
|
||||
.map_err(AuthError::JwksRefreshError)?
|
||||
.json::<JwkSet>()
|
||||
.await
|
||||
.map(|jwks| {
|
||||
self.load_time = Some(Instant::now());
|
||||
self.jwks = jwks;
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| {
|
||||
self.fail_time = Some(Instant::now());
|
||||
AuthError::JwksRefreshError(e)
|
||||
})?
|
||||
}
|
||||
|
||||
/// Find the key in the set that matches the given key id, if any.
|
||||
pub fn find_kid(&self, kid: &str) -> Option<&Jwk> {
|
||||
self.jwks.find(kid)
|
||||
}
|
||||
|
||||
/// Find the key in the set that matches the given key id, if any.
|
||||
pub fn find_alg(&self, alg: &Algorithm) -> Option<&Jwk> {
|
||||
self.jwks.keys.iter().find(|jwk| {
|
||||
if let Some(ref a) = jwk.common.algorithm {
|
||||
alg == a
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Find first key.
|
||||
pub fn find_first(&self) -> Option<&Jwk> {
|
||||
self.jwks.keys.get(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use jsonwebtoken::Algorithm;
|
||||
use jsonwebtoken::{jwk::Jwk, Header};
|
||||
use wiremock::{
|
||||
matchers::{method, path},
|
||||
Mock, MockServer, ResponseTemplate,
|
||||
};
|
||||
|
||||
use crate::jwks::key_store_manager::{KeyStore, KeyStoreManager};
|
||||
|
||||
#[test]
|
||||
fn keystore_should_refresh() {
|
||||
let ks = KeyStore {
|
||||
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![] },
|
||||
fail_time: None,
|
||||
load_time: Some(Instant::now()),
|
||||
};
|
||||
|
||||
assert!(!ks.should_refresh(Duration::from_secs(5)));
|
||||
|
||||
let ks = KeyStore {
|
||||
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![] },
|
||||
fail_time: None,
|
||||
load_time: Some(Instant::now() - Duration::from_secs(6)),
|
||||
};
|
||||
|
||||
assert!(ks.should_refresh(Duration::from_secs(5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keystore_can_refresh() {
|
||||
let ks = KeyStore {
|
||||
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![] },
|
||||
fail_time: Some(Instant::now() - Duration::from_secs(5)),
|
||||
load_time: None,
|
||||
};
|
||||
assert!(ks.can_refresh(Duration::from_secs(4)));
|
||||
assert!(!ks.can_refresh(Duration::from_secs(6)));
|
||||
|
||||
let ks = KeyStore {
|
||||
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![] },
|
||||
fail_time: None,
|
||||
load_time: Some(Instant::now() - Duration::from_secs(5)),
|
||||
};
|
||||
assert!(ks.can_refresh(Duration::from_secs(4)));
|
||||
assert!(!ks.can_refresh(Duration::from_secs(6)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_kid() {
|
||||
let jwk0: Jwk = serde_json::from_str(r#"{"kid":"1","kty":"RSA","alg":"RS256","n":"xxxx","e":"AQAB"}"#).unwrap();
|
||||
let jwk1: Jwk = serde_json::from_str(r#"{"kid":"2","kty":"RSA","alg":"RS256","n":"xxxx","e":"AQAB"}"#).unwrap();
|
||||
let ks = KeyStore {
|
||||
load_time: None,
|
||||
fail_time: None,
|
||||
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![jwk0, jwk1] },
|
||||
};
|
||||
assert!(ks.find_kid("1").is_some());
|
||||
assert!(ks.find_kid("2").is_some());
|
||||
assert!(ks.find_kid("3").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_alg() {
|
||||
let jwk0: Jwk = serde_json::from_str(r#"{"kty": "RSA", "alg": "RS256", "n": "xxx","e": "yyy"}"#).unwrap();
|
||||
let ks = KeyStore {
|
||||
load_time: None,
|
||||
fail_time: None,
|
||||
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![jwk0] },
|
||||
};
|
||||
assert!(ks.find_alg(&Algorithm::RS256).is_some());
|
||||
assert!(ks.find_alg(&Algorithm::EdDSA).is_none());
|
||||
}
|
||||
|
||||
async fn mock_jwks_response_once(mock_server: &MockServer, jwk: &str) {
|
||||
let jwk0: Jwk = serde_json::from_str(jwk).unwrap();
|
||||
let jwks = jsonwebtoken::jwk::JwkSet { keys: vec![jwk0] };
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(&jwks))
|
||||
.expect(1)
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn build_header(kid: &str, alg: Algorithm) -> Header {
|
||||
let mut header = Header::new(alg);
|
||||
header.kid = Some(kid.to_owned());
|
||||
header
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn keystore_manager_find_key_with_refresh_interval() {
|
||||
let mock_server = MockServer::start().await;
|
||||
mock_jwks_response_once(
|
||||
&mock_server,
|
||||
r#"{
|
||||
"kty": "OKP",
|
||||
"use": "sig",
|
||||
"crv": "Ed25519",
|
||||
"x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk",
|
||||
"kid": "key-ed",
|
||||
"alg": "EdDSA"
|
||||
}"#,
|
||||
)
|
||||
.await;
|
||||
|
||||
let ksm = KeyStoreManager::with_refresh_interval(&mock_server.uri(), Duration::from_secs(3000));
|
||||
let r = ksm.get_key(&Header::new(Algorithm::EdDSA)).await;
|
||||
assert!(r.is_ok());
|
||||
mock_server.verify().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn keystore_manager_find_key_with_refresh() {
|
||||
let mock_server = MockServer::start().await;
|
||||
mock_jwks_response_once(
|
||||
&mock_server,
|
||||
r#"{
|
||||
"kty": "OKP",
|
||||
"use": "sig",
|
||||
"crv": "Ed25519",
|
||||
"x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk",
|
||||
"kid": "key-ed",
|
||||
"alg": "EdDSA"
|
||||
}"#,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut ksm = KeyStoreManager::with_refresh(&mock_server.uri());
|
||||
|
||||
// STEP 1: initial (lazy) reloading
|
||||
let r = ksm.get_key(&build_header("key-ed", Algorithm::EdDSA)).await;
|
||||
assert!(r.is_ok());
|
||||
mock_server.verify().await;
|
||||
|
||||
// STEP2: new kid -> reloading ksm
|
||||
mock_server.reset().await;
|
||||
mock_jwks_response_once(
|
||||
&mock_server,
|
||||
r#"{
|
||||
"kty": "OKP",
|
||||
"use": "sig",
|
||||
"crv": "Ed25519",
|
||||
"x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk",
|
||||
"kid": "key-ed02",
|
||||
"alg": "EdDSA"
|
||||
}"#,
|
||||
)
|
||||
.await;
|
||||
let h = build_header("key-ed02", Algorithm::EdDSA);
|
||||
assert!(ksm.get_key(&h).await.is_err());
|
||||
|
||||
ksm.minimal_refresh_interval = Duration::from_millis(100);
|
||||
tokio::time::sleep(Duration::from_millis(101)).await;
|
||||
assert!(ksm.get_key(&h).await.is_ok());
|
||||
|
||||
mock_server.verify().await;
|
||||
|
||||
// STEP3: new algorithm -> try to reload
|
||||
mock_server.reset().await;
|
||||
mock_jwks_response_once(
|
||||
&mock_server,
|
||||
r#"{
|
||||
"kty": "EC",
|
||||
"crv": "P-256",
|
||||
"x": "w7JAoU_gJbZJvV-zCOvU9yFJq0FNC_edCMRM78P8eQQ",
|
||||
"y": "wQg1EytcsEmGrM70Gb53oluoDbVhCZ3Uq3hHMslHVb4",
|
||||
"kid": "ec01",
|
||||
"alg": "ES256",
|
||||
"use": "sig"
|
||||
}"#,
|
||||
)
|
||||
.await;
|
||||
let h = Header::new(Algorithm::ES256);
|
||||
assert!(ksm.get_key(&h).await.is_err());
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(101)).await;
|
||||
assert!(ksm.get_key(&h).await.is_ok());
|
||||
|
||||
mock_server.verify().await;
|
||||
}
|
||||
}
|
||||
24
jwt-authorizer/src/jwks/mod.rs
Normal file
24
jwt-authorizer/src/jwks/mod.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use jsonwebtoken::{DecodingKey, Header};
|
||||
|
||||
use crate::error::AuthError;
|
||||
|
||||
use self::key_store_manager::KeyStoreManager;
|
||||
|
||||
pub mod key_store_manager;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum KeySource {
|
||||
KeyStoreSource(KeyStoreManager),
|
||||
DecodingKeySource(DecodingKey),
|
||||
}
|
||||
|
||||
impl KeySource {
|
||||
pub async fn get_key(&self, header: Header) -> Result<DecodingKey, AuthError> {
|
||||
match self {
|
||||
KeySource::KeyStoreSource(kstore) => kstore.get_key(&header).await,
|
||||
KeySource::DecodingKeySource(key) => {
|
||||
Ok(key.clone()) // TODO: clone -> &
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
286
jwt-authorizer/src/layer.rs
Normal file
286
jwt-authorizer/src/layer.rs
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
use axum::http::Request;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{body::Body, response::Response};
|
||||
use futures_core::ready;
|
||||
use futures_util::future::BoxFuture;
|
||||
use headers::authorization::Bearer;
|
||||
use headers::{Authorization, HeaderMapExt};
|
||||
use http::StatusCode;
|
||||
use pin_project::pin_project;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use tower_layer::Layer;
|
||||
use tower_service::Service;
|
||||
|
||||
use crate::authorizer::{Authorizer, FnClaimsChecker};
|
||||
|
||||
/// Authorizer Layer builder
|
||||
///
|
||||
/// - initialisation of the Authorizer from jwks, rsa, ed, ec or secret
|
||||
/// - can define a checker (jwt claims check)
|
||||
pub struct JwtAuthorizer<C>
|
||||
where
|
||||
C: Clone + DeserializeOwned,
|
||||
{
|
||||
url: Option<&'static str>,
|
||||
claims_checker: Option<FnClaimsChecker<C>>,
|
||||
}
|
||||
|
||||
/// layer builder
|
||||
impl<C> JwtAuthorizer<C>
|
||||
where
|
||||
C: Clone + DeserializeOwned + Send + Sync,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
JwtAuthorizer {
|
||||
url: None,
|
||||
claims_checker: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_jwks_url(mut self, url: &'static str) -> JwtAuthorizer<C> {
|
||||
self.url = Some(url);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn from_rsa_pem(mut self, path: &'static str) -> JwtAuthorizer<C> {
|
||||
// TODO
|
||||
self
|
||||
}
|
||||
|
||||
pub fn from_ec_der(mut self, path: &'static str) -> JwtAuthorizer<C> {
|
||||
// TODO
|
||||
self
|
||||
}
|
||||
|
||||
pub fn from_ed_der(mut self, path: &'static str) -> JwtAuthorizer<C> {
|
||||
// TODO
|
||||
self
|
||||
}
|
||||
|
||||
pub fn from_secret(mut self, path: &'static str) -> JwtAuthorizer<C> {
|
||||
// TODO
|
||||
self
|
||||
}
|
||||
|
||||
/// layer that checks token validity and claim constraints (custom function)
|
||||
pub fn with_check(mut self, checker_fn: fn(&C) -> bool) -> JwtAuthorizer<C> {
|
||||
self.claims_checker = Some(FnClaimsChecker { checker_fn });
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// build axum layer
|
||||
pub fn layer(&self) -> AsyncAuthorizationLayer<C> {
|
||||
// TODO: replace unwrap
|
||||
let auth = Arc::new(Authorizer::from_jwks_url(self.url.unwrap(), self.claims_checker.clone()).unwrap());
|
||||
|
||||
AsyncAuthorizationLayer::new(auth)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for authorizing requests.
|
||||
pub trait AsyncAuthorizer<B> {
|
||||
type RequestBody;
|
||||
type ResponseBody;
|
||||
type Future: Future<Output = Result<Request<Self::RequestBody>, Response<Self::ResponseBody>>>;
|
||||
|
||||
/// Authorize the request.
|
||||
///
|
||||
/// If the future resolves to `Ok(request)` then the request is allowed through, otherwise not.
|
||||
fn authorize(&self, request: Request<B>) -> Self::Future;
|
||||
}
|
||||
|
||||
impl<B, S, C> AsyncAuthorizer<B> for AsyncAuthorizationService<S, C>
|
||||
where
|
||||
B: Send + Sync + 'static,
|
||||
C: Clone + DeserializeOwned + Send + Sync + 'static,
|
||||
{
|
||||
type RequestBody = B;
|
||||
type ResponseBody = Body;
|
||||
type Future = BoxFuture<'static, Result<Request<B>, Response<Self::ResponseBody>>>;
|
||||
|
||||
fn authorize(&self, mut request: Request<B>) -> Self::Future {
|
||||
let authorizer = self.auth.clone();
|
||||
let h = request.headers();
|
||||
let bearer: Authorization<Bearer> = h.typed_get().unwrap();
|
||||
Box::pin(async move {
|
||||
if let Ok(token_data) = authorizer.check_auth(bearer.token()).await {
|
||||
// Set `token_data` as a request extension so it can be accessed by other
|
||||
// services down the stack.
|
||||
request.extensions_mut().insert(token_data);
|
||||
|
||||
Ok(request)
|
||||
} else {
|
||||
let unauthorized_response = Response::builder()
|
||||
.status(StatusCode::UNAUTHORIZED)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
Err(unauthorized_response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// -------------- Layer -----------------
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncAuthorizationLayer<C>
|
||||
where
|
||||
C: Clone + DeserializeOwned + Send,
|
||||
{
|
||||
auth: Arc<Authorizer<C>>,
|
||||
}
|
||||
|
||||
impl<C> AsyncAuthorizationLayer<C>
|
||||
where
|
||||
C: Clone + DeserializeOwned + Send,
|
||||
{
|
||||
pub fn new(auth: Arc<Authorizer<C>>) -> AsyncAuthorizationLayer<C> {
|
||||
Self { auth }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, C> Layer<S> for AsyncAuthorizationLayer<C>
|
||||
where
|
||||
C: Clone + DeserializeOwned + Send + Sync,
|
||||
{
|
||||
type Service = AsyncAuthorizationService<S, C>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
AsyncAuthorizationService::new(inner, self.auth.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- AsyncAuthorizationService --------
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncAuthorizationService<S, C>
|
||||
where
|
||||
C: Clone + DeserializeOwned + Send + Sync,
|
||||
{
|
||||
pub inner: S,
|
||||
pub auth: Arc<Authorizer<C>>,
|
||||
}
|
||||
|
||||
impl<S, C> AsyncAuthorizationService<S, C>
|
||||
where
|
||||
C: Clone + DeserializeOwned + Send + Sync,
|
||||
{
|
||||
pub fn get_ref(&self) -> &S {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Gets a mutable reference to the underlying service.
|
||||
pub fn get_mut(&mut self) -> &mut S {
|
||||
&mut self.inner
|
||||
}
|
||||
|
||||
/// Consumes `self`, returning the underlying service.
|
||||
pub fn into_inner(self) -> S {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, C> AsyncAuthorizationService<S, C>
|
||||
where
|
||||
C: Clone + DeserializeOwned + Send + Sync,
|
||||
{
|
||||
/// Authorize requests using a custom scheme.
|
||||
///
|
||||
/// The `Authorization` header is required to have the value provided.
|
||||
pub fn new(inner: S, auth: Arc<Authorizer<C>>) -> AsyncAuthorizationService<S, C> {
|
||||
Self { inner, auth }
|
||||
}
|
||||
}
|
||||
|
||||
impl<ReqBody, S, C> Service<Request<ReqBody>> for AsyncAuthorizationService<S, C>
|
||||
where
|
||||
ReqBody: Send + Sync + 'static,
|
||||
S: Service<Request<ReqBody>, Response = Response> + Clone,
|
||||
C: Clone + DeserializeOwned + Send + Sync + 'static,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type Future = ResponseFuture<S, ReqBody, C>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
|
||||
let inner = self.inner.clone();
|
||||
let auth_fut = self.authorize(req);
|
||||
|
||||
ResponseFuture {
|
||||
state: State::Authorize { auth_fut },
|
||||
service: inner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
/// Response future for [`AsyncAuthorizationService`].
|
||||
pub struct ResponseFuture<S, ReqBody, C>
|
||||
where
|
||||
S: Service<Request<ReqBody>, Response = Response>,
|
||||
ReqBody: Send + Sync + 'static,
|
||||
C: Clone + DeserializeOwned + Send + Sync + 'static,
|
||||
{
|
||||
#[pin]
|
||||
state: State<<AsyncAuthorizationService<S, C> as AsyncAuthorizer<ReqBody>>::Future, S::Future>,
|
||||
service: S,
|
||||
}
|
||||
|
||||
#[pin_project(project = StateProj)]
|
||||
enum State<A, SFut> {
|
||||
Authorize {
|
||||
#[pin]
|
||||
auth_fut: A,
|
||||
},
|
||||
Authorized {
|
||||
#[pin]
|
||||
svc_fut: SFut,
|
||||
},
|
||||
}
|
||||
|
||||
impl<S, ReqBody, C> Future for ResponseFuture<S, ReqBody, C>
|
||||
where
|
||||
S: Service<Request<ReqBody>, Response = Response>,
|
||||
ReqBody: Send + Sync + 'static,
|
||||
C: Clone + DeserializeOwned + Send + Sync,
|
||||
{
|
||||
type Output = Result<S::Response, S::Error>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let mut this = self.project();
|
||||
|
||||
loop {
|
||||
match this.state.as_mut().project() {
|
||||
StateProj::Authorize { auth_fut } => {
|
||||
let auth = ready!(auth_fut.poll(cx));
|
||||
match auth {
|
||||
Ok(req) => {
|
||||
let svc_fut = this.service.call(req);
|
||||
this.state.set(State::Authorized { svc_fut })
|
||||
}
|
||||
Err(res) => {
|
||||
tracing::info!("err: {:?}", res);
|
||||
let r = (StatusCode::FORBIDDEN, format!("Unauthorized : {:?}", res)).into_response();
|
||||
// TODO: replace r by res (type problems: res should be already a 403 error response)
|
||||
return Poll::Ready(Ok(r));
|
||||
}
|
||||
};
|
||||
}
|
||||
StateProj::Authorized { svc_fut } => {
|
||||
return svc_fut.poll(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
jwt-authorizer/src/lib.rs
Normal file
34
jwt-authorizer/src/lib.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#![doc = include_str!("../docs/README.md")]
|
||||
|
||||
use axum::{async_trait, extract::FromRequestParts, http::request::Parts};
|
||||
use jsonwebtoken::TokenData;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
pub use self::error::AuthError;
|
||||
pub use layer::JwtAuthorizer;
|
||||
|
||||
pub mod authorizer;
|
||||
pub mod error;
|
||||
pub mod jwks;
|
||||
pub mod layer;
|
||||
|
||||
/// Claims serialized using T
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct JwtClaims<T>(pub T);
|
||||
|
||||
#[async_trait]
|
||||
impl<T, S> FromRequestParts<S> for JwtClaims<T>
|
||||
where
|
||||
T: DeserializeOwned + Send + Sync + Clone + 'static,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = error::AuthError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
|
||||
let claims = parts.extensions.get::<TokenData<T>>().unwrap(); // TODO: unwrap -> err
|
||||
Ok(JwtClaims(claims.claims.clone())) // TODO: unwrap -> err
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
1
jwt-authorizer/src/tests.rs
Normal file
1
jwt-authorizer/src/tests.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
// TODO: tests
|
||||
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
|
|
@ -0,0 +1 @@
|
|||
max_width=120
|
||||
Loading…
Add table
Add a link
Reference in a new issue