mirror of
https://codeberg.org/pfzetto/axum-oidc
synced 2025-12-18 19:15:17 +01:00
Compare commits
No commits in common. "master" and "0.3.0" have entirely different histories.
14 changed files with 371 additions and 1482 deletions
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
|
|
@ -10,8 +10,8 @@ env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
build_and_test:
|
||||||
name: axum-oidc
|
name: axum-oidc - latest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
|
@ -21,21 +21,14 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
|
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
|
||||||
- run: cargo build --verbose --release
|
- run: cargo build --verbose
|
||||||
- run: cargo test --verbose --release
|
- run: cargo test --verbose
|
||||||
|
|
||||||
test_basic_example:
|
build_examples:
|
||||||
name: axum-oidc - basic
|
name: axum-oidc - examples
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
toolchain:
|
|
||||||
- stable
|
|
||||||
- nightly
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
|
- run: rustup update stable && rustup default stable
|
||||||
- run: cargo build --verbose --release
|
- run: cargo build --verbose
|
||||||
working-directory: ./examples/basic
|
|
||||||
- run: cargo test --verbose --release
|
|
||||||
working-directory: ./examples/basic
|
working-directory: ./examples/basic
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,2 @@
|
||||||
target
|
target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
.env
|
|
||||||
|
|
|
||||||
34
Cargo.toml
34
Cargo.toml
|
|
@ -1,32 +1,26 @@
|
||||||
[package]
|
[package]
|
||||||
name = "axum-oidc"
|
name = "axum-oidc"
|
||||||
description = "A wrapper for the openidconnect crate for axum"
|
description = "A wrapper for the openidconnect crate for axum"
|
||||||
version = "1.0.0-dev-0"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Paul Z <info@pfzetto.de>"]
|
authors = [ "Paul Z <info@pfz4.de>" ]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://codeberg.org/pfzetto/axum-oidc"
|
repository = "https://github.com/pfz4/axum-oidc"
|
||||||
license = "MPL-2.0"
|
license = "LGPL-3.0-or-later"
|
||||||
keywords = ["axum", "oidc", "openidconnect", "authentication"]
|
keywords = [ "axum", "oidc", "openidconnect", "authentication" ]
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "2.0"
|
thiserror = "1.0"
|
||||||
axum-core = "0.5"
|
axum-core = "0.4"
|
||||||
axum = { version = "0.8", default-features = false, features = [
|
axum = { version = "0.7", default-features = false, features = [ "query" ] }
|
||||||
"query",
|
tower-service = "0.3.2"
|
||||||
"original-uri",
|
|
||||||
] }
|
|
||||||
tower-service = "0.3"
|
|
||||||
tower-layer = "0.3"
|
tower-layer = "0.3"
|
||||||
tower-sessions = { version = "0.14", default-features = false, features = [
|
tower-sessions = { version = "0.11", default-features = false, features = [ "axum-core" ] }
|
||||||
"axum-core",
|
http = "1.1"
|
||||||
] }
|
async-trait = "0.1"
|
||||||
http = "1.3.1"
|
openidconnect = "3.5"
|
||||||
openidconnect = "4.0"
|
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
reqwest = { version = "0.12", default-features = false }
|
reqwest = { version = "0.11", default-features = false }
|
||||||
urlencoding = "2.1"
|
|
||||||
tracing = "0.1.41"
|
|
||||||
|
|
|
||||||
373
LICENSE.txt
373
LICENSE.txt
|
|
@ -1,373 +0,0 @@
|
||||||
Mozilla Public License Version 2.0
|
|
||||||
==================================
|
|
||||||
|
|
||||||
1. Definitions
|
|
||||||
--------------
|
|
||||||
|
|
||||||
1.1. "Contributor"
|
|
||||||
means each individual or legal entity that creates, contributes to
|
|
||||||
the creation of, or owns Covered Software.
|
|
||||||
|
|
||||||
1.2. "Contributor Version"
|
|
||||||
means the combination of the Contributions of others (if any) used
|
|
||||||
by a Contributor and that particular Contributor's Contribution.
|
|
||||||
|
|
||||||
1.3. "Contribution"
|
|
||||||
means Covered Software of a particular Contributor.
|
|
||||||
|
|
||||||
1.4. "Covered Software"
|
|
||||||
means Source Code Form to which the initial Contributor has attached
|
|
||||||
the notice in Exhibit A, the Executable Form of such Source Code
|
|
||||||
Form, and Modifications of such Source Code Form, in each case
|
|
||||||
including portions thereof.
|
|
||||||
|
|
||||||
1.5. "Incompatible With Secondary Licenses"
|
|
||||||
means
|
|
||||||
|
|
||||||
(a) that the initial Contributor has attached the notice described
|
|
||||||
in Exhibit B to the Covered Software; or
|
|
||||||
|
|
||||||
(b) that the Covered Software was made available under the terms of
|
|
||||||
version 1.1 or earlier of the License, but not also under the
|
|
||||||
terms of a Secondary License.
|
|
||||||
|
|
||||||
1.6. "Executable Form"
|
|
||||||
means any form of the work other than Source Code Form.
|
|
||||||
|
|
||||||
1.7. "Larger Work"
|
|
||||||
means a work that combines Covered Software with other material, in
|
|
||||||
a separate file or files, that is not Covered Software.
|
|
||||||
|
|
||||||
1.8. "License"
|
|
||||||
means this document.
|
|
||||||
|
|
||||||
1.9. "Licensable"
|
|
||||||
means having the right to grant, to the maximum extent possible,
|
|
||||||
whether at the time of the initial grant or subsequently, any and
|
|
||||||
all of the rights conveyed by this License.
|
|
||||||
|
|
||||||
1.10. "Modifications"
|
|
||||||
means any of the following:
|
|
||||||
|
|
||||||
(a) any file in Source Code Form that results from an addition to,
|
|
||||||
deletion from, or modification of the contents of Covered
|
|
||||||
Software; or
|
|
||||||
|
|
||||||
(b) any new file in Source Code Form that contains any Covered
|
|
||||||
Software.
|
|
||||||
|
|
||||||
1.11. "Patent Claims" of a Contributor
|
|
||||||
means any patent claim(s), including without limitation, method,
|
|
||||||
process, and apparatus claims, in any patent Licensable by such
|
|
||||||
Contributor that would be infringed, but for the grant of the
|
|
||||||
License, by the making, using, selling, offering for sale, having
|
|
||||||
made, import, or transfer of either its Contributions or its
|
|
||||||
Contributor Version.
|
|
||||||
|
|
||||||
1.12. "Secondary License"
|
|
||||||
means either the GNU General Public License, Version 2.0, the GNU
|
|
||||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
|
||||||
Public License, Version 3.0, or any later versions of those
|
|
||||||
licenses.
|
|
||||||
|
|
||||||
1.13. "Source Code Form"
|
|
||||||
means the form of the work preferred for making modifications.
|
|
||||||
|
|
||||||
1.14. "You" (or "Your")
|
|
||||||
means an individual or a legal entity exercising rights under this
|
|
||||||
License. For legal entities, "You" includes any entity that
|
|
||||||
controls, is controlled by, or is under common control with You. For
|
|
||||||
purposes of this definition, "control" means (a) the power, direct
|
|
||||||
or indirect, to cause the direction or management of such entity,
|
|
||||||
whether by contract or otherwise, or (b) ownership of more than
|
|
||||||
fifty percent (50%) of the outstanding shares or beneficial
|
|
||||||
ownership of such entity.
|
|
||||||
|
|
||||||
2. License Grants and Conditions
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
2.1. Grants
|
|
||||||
|
|
||||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
|
||||||
non-exclusive license:
|
|
||||||
|
|
||||||
(a) under intellectual property rights (other than patent or trademark)
|
|
||||||
Licensable by such Contributor to use, reproduce, make available,
|
|
||||||
modify, display, perform, distribute, and otherwise exploit its
|
|
||||||
Contributions, either on an unmodified basis, with Modifications, or
|
|
||||||
as part of a Larger Work; and
|
|
||||||
|
|
||||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
|
||||||
for sale, have made, import, and otherwise transfer either its
|
|
||||||
Contributions or its Contributor Version.
|
|
||||||
|
|
||||||
2.2. Effective Date
|
|
||||||
|
|
||||||
The licenses granted in Section 2.1 with respect to any Contribution
|
|
||||||
become effective for each Contribution on the date the Contributor first
|
|
||||||
distributes such Contribution.
|
|
||||||
|
|
||||||
2.3. Limitations on Grant Scope
|
|
||||||
|
|
||||||
The licenses granted in this Section 2 are the only rights granted under
|
|
||||||
this License. No additional rights or licenses will be implied from the
|
|
||||||
distribution or licensing of Covered Software under this License.
|
|
||||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
|
||||||
Contributor:
|
|
||||||
|
|
||||||
(a) for any code that a Contributor has removed from Covered Software;
|
|
||||||
or
|
|
||||||
|
|
||||||
(b) for infringements caused by: (i) Your and any other third party's
|
|
||||||
modifications of Covered Software, or (ii) the combination of its
|
|
||||||
Contributions with other software (except as part of its Contributor
|
|
||||||
Version); or
|
|
||||||
|
|
||||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
|
||||||
its Contributions.
|
|
||||||
|
|
||||||
This License does not grant any rights in the trademarks, service marks,
|
|
||||||
or logos of any Contributor (except as may be necessary to comply with
|
|
||||||
the notice requirements in Section 3.4).
|
|
||||||
|
|
||||||
2.4. Subsequent Licenses
|
|
||||||
|
|
||||||
No Contributor makes additional grants as a result of Your choice to
|
|
||||||
distribute the Covered Software under a subsequent version of this
|
|
||||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
|
||||||
permitted under the terms of Section 3.3).
|
|
||||||
|
|
||||||
2.5. Representation
|
|
||||||
|
|
||||||
Each Contributor represents that the Contributor believes its
|
|
||||||
Contributions are its original creation(s) or it has sufficient rights
|
|
||||||
to grant the rights to its Contributions conveyed by this License.
|
|
||||||
|
|
||||||
2.6. Fair Use
|
|
||||||
|
|
||||||
This License is not intended to limit any rights You have under
|
|
||||||
applicable copyright doctrines of fair use, fair dealing, or other
|
|
||||||
equivalents.
|
|
||||||
|
|
||||||
2.7. Conditions
|
|
||||||
|
|
||||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
|
||||||
in Section 2.1.
|
|
||||||
|
|
||||||
3. Responsibilities
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
3.1. Distribution of Source Form
|
|
||||||
|
|
||||||
All distribution of Covered Software in Source Code Form, including any
|
|
||||||
Modifications that You create or to which You contribute, must be under
|
|
||||||
the terms of this License. You must inform recipients that the Source
|
|
||||||
Code Form of the Covered Software is governed by the terms of this
|
|
||||||
License, and how they can obtain a copy of this License. You may not
|
|
||||||
attempt to alter or restrict the recipients' rights in the Source Code
|
|
||||||
Form.
|
|
||||||
|
|
||||||
3.2. Distribution of Executable Form
|
|
||||||
|
|
||||||
If You distribute Covered Software in Executable Form then:
|
|
||||||
|
|
||||||
(a) such Covered Software must also be made available in Source Code
|
|
||||||
Form, as described in Section 3.1, and You must inform recipients of
|
|
||||||
the Executable Form how they can obtain a copy of such Source Code
|
|
||||||
Form by reasonable means in a timely manner, at a charge no more
|
|
||||||
than the cost of distribution to the recipient; and
|
|
||||||
|
|
||||||
(b) You may distribute such Executable Form under the terms of this
|
|
||||||
License, or sublicense it under different terms, provided that the
|
|
||||||
license for the Executable Form does not attempt to limit or alter
|
|
||||||
the recipients' rights in the Source Code Form under this License.
|
|
||||||
|
|
||||||
3.3. Distribution of a Larger Work
|
|
||||||
|
|
||||||
You may create and distribute a Larger Work under terms of Your choice,
|
|
||||||
provided that You also comply with the requirements of this License for
|
|
||||||
the Covered Software. If the Larger Work is a combination of Covered
|
|
||||||
Software with a work governed by one or more Secondary Licenses, and the
|
|
||||||
Covered Software is not Incompatible With Secondary Licenses, this
|
|
||||||
License permits You to additionally distribute such Covered Software
|
|
||||||
under the terms of such Secondary License(s), so that the recipient of
|
|
||||||
the Larger Work may, at their option, further distribute the Covered
|
|
||||||
Software under the terms of either this License or such Secondary
|
|
||||||
License(s).
|
|
||||||
|
|
||||||
3.4. Notices
|
|
||||||
|
|
||||||
You may not remove or alter the substance of any license notices
|
|
||||||
(including copyright notices, patent notices, disclaimers of warranty,
|
|
||||||
or limitations of liability) contained within the Source Code Form of
|
|
||||||
the Covered Software, except that You may alter any license notices to
|
|
||||||
the extent required to remedy known factual inaccuracies.
|
|
||||||
|
|
||||||
3.5. Application of Additional Terms
|
|
||||||
|
|
||||||
You may choose to offer, and to charge a fee for, warranty, support,
|
|
||||||
indemnity or liability obligations to one or more recipients of Covered
|
|
||||||
Software. However, You may do so only on Your own behalf, and not on
|
|
||||||
behalf of any Contributor. You must make it absolutely clear that any
|
|
||||||
such warranty, support, indemnity, or liability obligation is offered by
|
|
||||||
You alone, and You hereby agree to indemnify every Contributor for any
|
|
||||||
liability incurred by such Contributor as a result of warranty, support,
|
|
||||||
indemnity or liability terms You offer. You may include additional
|
|
||||||
disclaimers of warranty and limitations of liability specific to any
|
|
||||||
jurisdiction.
|
|
||||||
|
|
||||||
4. Inability to Comply Due to Statute or Regulation
|
|
||||||
---------------------------------------------------
|
|
||||||
|
|
||||||
If it is impossible for You to comply with any of the terms of this
|
|
||||||
License with respect to some or all of the Covered Software due to
|
|
||||||
statute, judicial order, or regulation then You must: (a) comply with
|
|
||||||
the terms of this License to the maximum extent possible; and (b)
|
|
||||||
describe the limitations and the code they affect. Such description must
|
|
||||||
be placed in a text file included with all distributions of the Covered
|
|
||||||
Software under this License. Except to the extent prohibited by statute
|
|
||||||
or regulation, such description must be sufficiently detailed for a
|
|
||||||
recipient of ordinary skill to be able to understand it.
|
|
||||||
|
|
||||||
5. Termination
|
|
||||||
--------------
|
|
||||||
|
|
||||||
5.1. The rights granted under this License will terminate automatically
|
|
||||||
if You fail to comply with any of its terms. However, if You become
|
|
||||||
compliant, then the rights granted under this License from a particular
|
|
||||||
Contributor are reinstated (a) provisionally, unless and until such
|
|
||||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
|
||||||
ongoing basis, if such Contributor fails to notify You of the
|
|
||||||
non-compliance by some reasonable means prior to 60 days after You have
|
|
||||||
come back into compliance. Moreover, Your grants from a particular
|
|
||||||
Contributor are reinstated on an ongoing basis if such Contributor
|
|
||||||
notifies You of the non-compliance by some reasonable means, this is the
|
|
||||||
first time You have received notice of non-compliance with this License
|
|
||||||
from such Contributor, and You become compliant prior to 30 days after
|
|
||||||
Your receipt of the notice.
|
|
||||||
|
|
||||||
5.2. If You initiate litigation against any entity by asserting a patent
|
|
||||||
infringement claim (excluding declaratory judgment actions,
|
|
||||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
|
||||||
directly or indirectly infringes any patent, then the rights granted to
|
|
||||||
You by any and all Contributors for the Covered Software under Section
|
|
||||||
2.1 of this License shall terminate.
|
|
||||||
|
|
||||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
|
||||||
end user license agreements (excluding distributors and resellers) which
|
|
||||||
have been validly granted by You or Your distributors under this License
|
|
||||||
prior to termination shall survive termination.
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 6. Disclaimer of Warranty *
|
|
||||||
* ------------------------- *
|
|
||||||
* *
|
|
||||||
* Covered Software is provided under this License on an "as is" *
|
|
||||||
* basis, without warranty of any kind, either expressed, implied, or *
|
|
||||||
* statutory, including, without limitation, warranties that the *
|
|
||||||
* Covered Software is free of defects, merchantable, fit for a *
|
|
||||||
* particular purpose or non-infringing. The entire risk as to the *
|
|
||||||
* quality and performance of the Covered Software is with You. *
|
|
||||||
* Should any Covered Software prove defective in any respect, You *
|
|
||||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
|
||||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
|
||||||
* essential part of this License. No use of any Covered Software is *
|
|
||||||
* authorized under this License except under this disclaimer. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 7. Limitation of Liability *
|
|
||||||
* -------------------------- *
|
|
||||||
* *
|
|
||||||
* Under no circumstances and under no legal theory, whether tort *
|
|
||||||
* (including negligence), contract, or otherwise, shall any *
|
|
||||||
* Contributor, or anyone who distributes Covered Software as *
|
|
||||||
* permitted above, be liable to You for any direct, indirect, *
|
|
||||||
* special, incidental, or consequential damages of any character *
|
|
||||||
* including, without limitation, damages for lost profits, loss of *
|
|
||||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
|
||||||
* and all other commercial damages or losses, even if such party *
|
|
||||||
* shall have been informed of the possibility of such damages. This *
|
|
||||||
* limitation of liability shall not apply to liability for death or *
|
|
||||||
* personal injury resulting from such party's negligence to the *
|
|
||||||
* extent applicable law prohibits such limitation. Some *
|
|
||||||
* jurisdictions do not allow the exclusion or limitation of *
|
|
||||||
* incidental or consequential damages, so this exclusion and *
|
|
||||||
* limitation may not apply to You. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
8. Litigation
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Any litigation relating to this License may be brought only in the
|
|
||||||
courts of a jurisdiction where the defendant maintains its principal
|
|
||||||
place of business and such litigation shall be governed by laws of that
|
|
||||||
jurisdiction, without reference to its conflict-of-law provisions.
|
|
||||||
Nothing in this Section shall prevent a party's ability to bring
|
|
||||||
cross-claims or counter-claims.
|
|
||||||
|
|
||||||
9. Miscellaneous
|
|
||||||
----------------
|
|
||||||
|
|
||||||
This License represents the complete agreement concerning the subject
|
|
||||||
matter hereof. If any provision of this License is held to be
|
|
||||||
unenforceable, such provision shall be reformed only to the extent
|
|
||||||
necessary to make it enforceable. Any law or regulation which provides
|
|
||||||
that the language of a contract shall be construed against the drafter
|
|
||||||
shall not be used to construe this License against a Contributor.
|
|
||||||
|
|
||||||
10. Versions of the License
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
10.1. New Versions
|
|
||||||
|
|
||||||
Mozilla Foundation is the license steward. Except as provided in Section
|
|
||||||
10.3, no one other than the license steward has the right to modify or
|
|
||||||
publish new versions of this License. Each version will be given a
|
|
||||||
distinguishing version number.
|
|
||||||
|
|
||||||
10.2. Effect of New Versions
|
|
||||||
|
|
||||||
You may distribute the Covered Software under the terms of the version
|
|
||||||
of the License under which You originally received the Covered Software,
|
|
||||||
or under the terms of any subsequent version published by the license
|
|
||||||
steward.
|
|
||||||
|
|
||||||
10.3. Modified Versions
|
|
||||||
|
|
||||||
If you create software not governed by this License, and you want to
|
|
||||||
create a new license for such software, you may create and use a
|
|
||||||
modified version of this License if you rename the license and remove
|
|
||||||
any references to the name of the license steward (except to note that
|
|
||||||
such modified license differs from this License).
|
|
||||||
|
|
||||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
|
||||||
Licenses
|
|
||||||
|
|
||||||
If You choose to distribute Source Code Form that is Incompatible With
|
|
||||||
Secondary Licenses under the terms of this version of the License, the
|
|
||||||
notice described in Exhibit B of this License must be attached.
|
|
||||||
|
|
||||||
Exhibit A - Source Code Form License Notice
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
If it is not possible or desirable to put the notice in a particular
|
|
||||||
file, then You may include the notice in a location (such as a LICENSE
|
|
||||||
file in a relevant directory) where a recipient would be likely to look
|
|
||||||
for such a notice.
|
|
||||||
|
|
||||||
You may add additional accurate notices of copyright ownership.
|
|
||||||
|
|
||||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
|
||||||
---------------------------------------------------------
|
|
||||||
|
|
||||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
||||||
defined by the Mozilla Public License, v. 2.0.
|
|
||||||
11
README.md
11
README.md
|
|
@ -1,5 +1,5 @@
|
||||||
This Library allows using [OpenID Connect](https://openid.net/developers/how-connect-works/) with [axum](https://github.com/tokio-rs/axum).
|
This Library allows using [OpenID Connect](https://openid.net/developers/how-connect-works/) with [axum](https://github.com/tokio-rs/axum).
|
||||||
It authenticates the user with the OpenID Connect Issuer and provides Extractors.
|
It authenticates the user with the OpenID Conenct Issuer and provides Extractors.
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
The `OidcAuthLayer` must be loaded on any handler that might use the extractors.
|
The `OidcAuthLayer` must be loaded on any handler that might use the extractors.
|
||||||
|
|
@ -13,23 +13,16 @@ The extractors will always return a value.
|
||||||
The `OidcClaims`-extractor can be used to get the OpenId Conenct Claims.
|
The `OidcClaims`-extractor can be used to get the OpenId Conenct Claims.
|
||||||
The `OidcAccessToken`-extractor can be used to get the OpenId Connect Access Token.
|
The `OidcAccessToken`-extractor can be used to get the OpenId Connect Access Token.
|
||||||
|
|
||||||
The `OidcRpInitializedLogout`-extractor can be used to get the rp initialized logout uri.
|
|
||||||
|
|
||||||
Your OIDC-Client must be allowed to redirect to **every** subpath of your application base url.
|
Your OIDC-Client must be allowed to redirect to **every** subpath of your application base url.
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
Take a look at the `examples` folder for examples.
|
Take a look at the `examples` folder for examples.
|
||||||
|
|
||||||
# Older Versions
|
|
||||||
All versions on [crates.io](https://crates.io) are available as git tags.
|
|
||||||
Additional all minor versions have their own branch (format `vX.Y` where `X` is the major and `Y` is the minor version) where bug fixes are implemented.
|
|
||||||
Examples for each version can be found there in the previously mentioned `examples` folder.
|
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
I'm happy about any contribution in any form.
|
I'm happy about any contribution in any form.
|
||||||
Feel free to submit feature requests and bug reports using a GitHub Issue.
|
Feel free to submit feature requests and bug reports using a GitHub Issue.
|
||||||
PR's are also appreciated.
|
PR's are also appreciated.
|
||||||
|
|
||||||
# License
|
# License
|
||||||
This Library is licensed under [MPLv2](https://www.mozilla.org/en-US/MPL/2.0/).
|
This Library is licensed under [LGPLv3](https://www.gnu.org/licenses/lgpl-3.0.en.html).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
[package]
|
[package]
|
||||||
edition = "2024"
|
|
||||||
name = "basic"
|
name = "basic"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.8", features = ["macros"] }
|
tokio = { version = "1.36.0", features = ["net", "macros"] }
|
||||||
|
axum = "0.7.4"
|
||||||
axum-oidc = { path = "./../.." }
|
axum-oidc = { path = "./../.." }
|
||||||
dotenvy = "0.15"
|
tower = "0.4.13"
|
||||||
tokio = { version = "1.48.0", features = ["macros", "net", "rt-multi-thread"] }
|
tower-sessions = "0.11.0"
|
||||||
tower = "0.5"
|
|
||||||
tower-sessions = "0.14"
|
|
||||||
tracing-subscriber = "0.3.20"
|
|
||||||
tracing = "0.1.41"
|
|
||||||
serde = "1.0.228"
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
This example is a basic web application to demonstrate the features of the `axum-oidc`-crate.
|
|
||||||
It has three endpoints:
|
|
||||||
- `/logout` - Logout of the current session using `OIDC RP-Initiated Logout`.
|
|
||||||
- `/foo` - A handler that only can be accessed when logged in.
|
|
||||||
- `/bar` - A handler that can be accessed logged out and logged in. It will greet the user with their name if they are logged in.
|
|
||||||
|
|
||||||
# Running the Example
|
|
||||||
## Dependencies
|
|
||||||
You will need a running OpenID Connect capable issuer like [Keycloak](https://www.keycloak.org/getting-started/getting-started-docker) and a valid client for the issuer.
|
|
||||||
|
|
||||||
## Setup Environment
|
|
||||||
Create a `.env`-file that contains the following keys:
|
|
||||||
```
|
|
||||||
APP_URL=http://127.0.0.1:8080
|
|
||||||
ISSUER=<your-issuer>
|
|
||||||
CLIENT_ID=<your-client-id>
|
|
||||||
CLIENT_SECRET=<your-client-secret>
|
|
||||||
```
|
|
||||||
## Run the application
|
|
||||||
`RUST_LOG=debug cargo run`
|
|
||||||
|
|
@ -1,37 +1,18 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
error_handling::HandleErrorLayer, http::Uri, response::IntoResponse, routing::get, Router,
|
||||||
error_handling::HandleErrorLayer,
|
|
||||||
http::Uri,
|
|
||||||
response::IntoResponse,
|
|
||||||
routing::{any, get},
|
|
||||||
};
|
};
|
||||||
use axum_oidc::{
|
use axum_oidc::{
|
||||||
EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcClient, OidcLoginLayer,
|
error::MiddlewareError, EmptyAdditionalClaims, OidcAuthLayer, OidcClaims, OidcLoginLayer,
|
||||||
OidcRpInitiatedLogout,
|
|
||||||
error::MiddlewareError,
|
|
||||||
handle_oidc_redirect,
|
|
||||||
openidconnect::{Audience, ClientId, ClientSecret, IssuerUrl, Scope},
|
|
||||||
};
|
};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tower::ServiceBuilder;
|
use tower::ServiceBuilder;
|
||||||
use tower_sessions::{
|
use tower_sessions::{
|
||||||
|
cookie::{time::Duration, SameSite},
|
||||||
Expiry, MemoryStore, SessionManagerLayer,
|
Expiry, MemoryStore, SessionManagerLayer,
|
||||||
cookie::{SameSite, time::Duration},
|
|
||||||
};
|
};
|
||||||
use tracing::Level;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_file(true)
|
|
||||||
.with_line_number(true)
|
|
||||||
.with_max_level(Level::INFO)
|
|
||||||
.init();
|
|
||||||
dotenvy::dotenv().ok();
|
|
||||||
let issuer = std::env::var("ISSUER").expect("ISSUER env variable");
|
|
||||||
let client_id = std::env::var("CLIENT_ID").expect("CLIENT_ID env variable");
|
|
||||||
let client_secret = std::env::var("CLIENT_SECRET").ok();
|
|
||||||
|
|
||||||
let session_store = MemoryStore::default();
|
let session_store = MemoryStore::default();
|
||||||
let session_layer = SessionManagerLayer::new(session_store)
|
let session_layer = SessionManagerLayer::new(session_store)
|
||||||
.with_secure(false)
|
.with_secure(false)
|
||||||
|
|
@ -40,48 +21,33 @@ async fn main() {
|
||||||
|
|
||||||
let oidc_login_service = ServiceBuilder::new()
|
let oidc_login_service = ServiceBuilder::new()
|
||||||
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
|
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
|
||||||
dbg!(&e);
|
|
||||||
e.into_response()
|
e.into_response()
|
||||||
}))
|
}))
|
||||||
.layer(OidcLoginLayer::<EmptyAdditionalClaims>::new());
|
.layer(OidcLoginLayer::<EmptyAdditionalClaims>::new());
|
||||||
|
|
||||||
let mut oidc_client = OidcClient::<EmptyAdditionalClaims>::builder()
|
|
||||||
.with_default_http_client()
|
|
||||||
.with_redirect_url(Uri::from_static("http://localhost:8080/oidc"))
|
|
||||||
.with_client_id(ClientId::new(client_id))
|
|
||||||
.add_scope(Scope::new("profile".into()))
|
|
||||||
.add_scope(Scope::new("email".into()))
|
|
||||||
// Optional: add untrusted audiences. If the `aud` claim contains any of these audiences, the token is rejected.
|
|
||||||
.add_untrusted_audience(Audience::new("123456789".to_string()));
|
|
||||||
|
|
||||||
if let Some(client_secret) = client_secret {
|
|
||||||
oidc_client = oidc_client.with_client_secret(ClientSecret::new(client_secret));
|
|
||||||
}
|
|
||||||
let oidc_client = oidc_client
|
|
||||||
.discover(IssuerUrl::new(issuer.into()).expect("Invalid IssuerUrl"))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let oidc_auth_service = ServiceBuilder::new()
|
let oidc_auth_service = ServiceBuilder::new()
|
||||||
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
|
.layer(HandleErrorLayer::new(|e: MiddlewareError| async {
|
||||||
dbg!(&e);
|
|
||||||
e.into_response()
|
e.into_response()
|
||||||
}))
|
}))
|
||||||
.layer(OidcAuthLayer::new(oidc_client));
|
.layer(
|
||||||
|
OidcAuthLayer::<EmptyAdditionalClaims>::discover_client(
|
||||||
|
Uri::from_static("http://localhost:8080"),
|
||||||
|
"https://auth.zettoit.eu/realms/zettoit".to_string(),
|
||||||
|
"oxicloud".to_string(),
|
||||||
|
Some("IvBcDOfp9WBfGNmwIbiv67bxCwuQUGbl".to_owned()),
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/foo", get(authenticated))
|
.route("/foo", get(authenticated))
|
||||||
.route("/logout", get(logout))
|
|
||||||
.layer(oidc_login_service)
|
.layer(oidc_login_service)
|
||||||
.route("/bar", get(maybe_authenticated))
|
.route("/bar", get(maybe_authenticated))
|
||||||
.route("/oidc", any(handle_oidc_redirect::<EmptyAdditionalClaims>))
|
|
||||||
.layer(oidc_auth_service)
|
.layer(oidc_auth_service)
|
||||||
.layer(session_layer);
|
.layer(session_layer);
|
||||||
|
|
||||||
tracing::info!("Running on http://localhost:8080");
|
|
||||||
tracing::info!("Visit http://localhost:8080/bar or http://localhost:8080/foo");
|
|
||||||
|
|
||||||
let listener = TcpListener::bind("[::]:8080").await.unwrap();
|
let listener = TcpListener::bind("[::]:8080").await.unwrap();
|
||||||
axum::serve(listener, app.into_make_service())
|
axum::serve(listener, app.into_make_service())
|
||||||
.await
|
.await
|
||||||
|
|
@ -92,11 +58,10 @@ async fn authenticated(claims: OidcClaims<EmptyAdditionalClaims>) -> impl IntoRe
|
||||||
format!("Hello {}", claims.subject().as_str())
|
format!("Hello {}", claims.subject().as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[axum::debug_handler]
|
|
||||||
async fn maybe_authenticated(
|
async fn maybe_authenticated(
|
||||||
claims: Result<OidcClaims<EmptyAdditionalClaims>, axum_oidc::error::ExtractorError>,
|
claims: Option<OidcClaims<EmptyAdditionalClaims>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if let Ok(claims) = claims {
|
if let Some(claims) = claims {
|
||||||
format!(
|
format!(
|
||||||
"Hello {}! You are already logged in from another Handler.",
|
"Hello {}! You are already logged in from another Handler.",
|
||||||
claims.subject().as_str()
|
claims.subject().as_str()
|
||||||
|
|
@ -105,7 +70,3 @@ async fn maybe_authenticated(
|
||||||
"Hello anon!".to_string()
|
"Hello anon!".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn logout(logout: OidcRpInitiatedLogout) -> impl IntoResponse {
|
|
||||||
logout.with_post_logout_redirect(Uri::from_static("https://example.com"))
|
|
||||||
}
|
|
||||||
|
|
|
||||||
244
src/builder.rs
244
src/builder.rs
|
|
@ -1,244 +0,0 @@
|
||||||
use std::marker::PhantomData;
|
|
||||||
|
|
||||||
use http::Uri;
|
|
||||||
use openidconnect::{
|
|
||||||
Audience, AuthenticationContextClass, ClientId, ClientSecret, IssuerUrl, Scope,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{error::Error, AdditionalClaims, Client, OidcClient, ProviderMetadata};
|
|
||||||
|
|
||||||
pub struct Unconfigured;
|
|
||||||
pub struct OpenidconnectClient<AC: AdditionalClaims>(crate::Client<AC>);
|
|
||||||
pub struct HttpClient(reqwest::Client);
|
|
||||||
pub struct RedirectUrl(Uri);
|
|
||||||
|
|
||||||
pub struct ClientCredentials {
|
|
||||||
id: ClientId,
|
|
||||||
secret: Option<ClientSecret>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Builder<AC: AdditionalClaims, Credentials, Client, HttpClient, RedirectUrl> {
|
|
||||||
credentials: Credentials,
|
|
||||||
client: Client,
|
|
||||||
http_client: HttpClient,
|
|
||||||
redirect_url: RedirectUrl,
|
|
||||||
end_session_endpoint: Option<Uri>,
|
|
||||||
scopes: Vec<Scope>,
|
|
||||||
auth_context_class: Option<AuthenticationContextClass>,
|
|
||||||
untrusted_audiences: Vec<Audience>,
|
|
||||||
_ac: PhantomData<AC>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<AC: AdditionalClaims> Default for Builder<AC, (), (), (), ()> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<AC: AdditionalClaims> Builder<AC, (), (), (), ()> {
|
|
||||||
/// create a new builder with default values
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
credentials: (),
|
|
||||||
client: (),
|
|
||||||
http_client: (),
|
|
||||||
redirect_url: (),
|
|
||||||
end_session_endpoint: None,
|
|
||||||
scopes: vec![Scope::new("openid".to_string())],
|
|
||||||
auth_context_class: None,
|
|
||||||
untrusted_audiences: Vec::new(),
|
|
||||||
_ac: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<AC: AdditionalClaims> OidcClient<AC> {
|
|
||||||
/// create a new builder with default values
|
|
||||||
pub fn builder() -> Builder<AC, (), (), (), ()> {
|
|
||||||
Builder::<AC, (), (), (), ()>::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<AC: AdditionalClaims, CREDS, CLIENT, HTTP, RURL> Builder<AC, CREDS, CLIENT, HTTP, RURL> {
|
|
||||||
/// add a scope to existing (default) scopes
|
|
||||||
pub fn add_scope(mut self, scope: Scope) -> Self {
|
|
||||||
self.scopes.push(scope);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// replace scopes (including default)
|
|
||||||
pub fn with_scopes(mut self, scopes: Vec<Scope>) -> Self {
|
|
||||||
self.scopes = scopes;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// authenticate with Authentication Context Class Reference
|
|
||||||
pub fn with_auth_context_class(mut self, acr: AuthenticationContextClass) -> Self {
|
|
||||||
self.auth_context_class = Some(acr);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// add a an untrusted audience to existing untrusted audiences
|
|
||||||
pub fn add_untrusted_audience(mut self, audience: Audience) -> Self {
|
|
||||||
self.untrusted_audiences.push(audience);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// replace untrusted audiences
|
|
||||||
pub fn with_untrusted_audiences(mut self, untrusted_audiences: Vec<Audience>) -> Self {
|
|
||||||
self.untrusted_audiences = untrusted_audiences;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<AC: AdditionalClaims, CLIENT, HTTP, RURL> Builder<AC, (), CLIENT, HTTP, RURL> {
|
|
||||||
/// set client id for authentication with issuer
|
|
||||||
pub fn with_client_id(
|
|
||||||
self,
|
|
||||||
id: impl Into<ClientId>,
|
|
||||||
) -> Builder<AC, ClientCredentials, CLIENT, HTTP, RURL> {
|
|
||||||
Builder::<_, _, _, _, _> {
|
|
||||||
credentials: ClientCredentials {
|
|
||||||
id: id.into(),
|
|
||||||
secret: None,
|
|
||||||
},
|
|
||||||
client: self.client,
|
|
||||||
http_client: self.http_client,
|
|
||||||
redirect_url: self.redirect_url,
|
|
||||||
end_session_endpoint: self.end_session_endpoint,
|
|
||||||
scopes: self.scopes,
|
|
||||||
auth_context_class: self.auth_context_class,
|
|
||||||
untrusted_audiences: self.untrusted_audiences,
|
|
||||||
_ac: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<AC: AdditionalClaims, CLIENT, HTTP, RURL> Builder<AC, ClientCredentials, CLIENT, HTTP, RURL> {
|
|
||||||
/// set client secret for authentication with issuer
|
|
||||||
pub fn with_client_secret(mut self, secret: impl Into<ClientSecret>) -> Self {
|
|
||||||
self.credentials.secret = Some(secret.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<AC: AdditionalClaims, CREDS, CLIENT, RURL> Builder<AC, CREDS, CLIENT, (), RURL> {
|
|
||||||
/// use custom http client
|
|
||||||
pub fn with_http_client(
|
|
||||||
self,
|
|
||||||
client: reqwest::Client,
|
|
||||||
) -> Builder<AC, CREDS, CLIENT, HttpClient, RURL> {
|
|
||||||
Builder {
|
|
||||||
credentials: self.credentials,
|
|
||||||
client: self.client,
|
|
||||||
http_client: HttpClient(client),
|
|
||||||
redirect_url: self.redirect_url,
|
|
||||||
end_session_endpoint: self.end_session_endpoint,
|
|
||||||
scopes: self.scopes,
|
|
||||||
auth_context_class: self.auth_context_class,
|
|
||||||
untrusted_audiences: self.untrusted_audiences,
|
|
||||||
_ac: self._ac,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// use default reqwest http client
|
|
||||||
pub fn with_default_http_client(self) -> Builder<AC, CREDS, CLIENT, HttpClient, RURL> {
|
|
||||||
Builder {
|
|
||||||
credentials: self.credentials,
|
|
||||||
client: self.client,
|
|
||||||
http_client: HttpClient(reqwest::Client::default()),
|
|
||||||
redirect_url: self.redirect_url,
|
|
||||||
end_session_endpoint: self.end_session_endpoint,
|
|
||||||
scopes: self.scopes,
|
|
||||||
auth_context_class: self.auth_context_class,
|
|
||||||
untrusted_audiences: self.untrusted_audiences,
|
|
||||||
_ac: self._ac,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<AC: AdditionalClaims, CREDS, CLIENT, HCLIENT> Builder<AC, CREDS, CLIENT, HCLIENT, ()> {
|
|
||||||
pub fn with_redirect_url(
|
|
||||||
self,
|
|
||||||
redirect_url: Uri,
|
|
||||||
) -> Builder<AC, CREDS, CLIENT, HCLIENT, RedirectUrl> {
|
|
||||||
Builder {
|
|
||||||
credentials: self.credentials,
|
|
||||||
client: self.client,
|
|
||||||
http_client: self.http_client,
|
|
||||||
redirect_url: RedirectUrl(redirect_url),
|
|
||||||
end_session_endpoint: self.end_session_endpoint,
|
|
||||||
scopes: self.scopes,
|
|
||||||
auth_context_class: self.auth_context_class,
|
|
||||||
untrusted_audiences: self.untrusted_audiences,
|
|
||||||
_ac: self._ac,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<AC: AdditionalClaims> Builder<AC, ClientCredentials, (), HttpClient, RedirectUrl> {
|
|
||||||
/// provide issuer details manually
|
|
||||||
pub fn manual(
|
|
||||||
self,
|
|
||||||
provider_metadata: ProviderMetadata,
|
|
||||||
) -> Result<
|
|
||||||
Builder<AC, ClientCredentials, OpenidconnectClient<AC>, HttpClient, RedirectUrl>,
|
|
||||||
Error,
|
|
||||||
> {
|
|
||||||
let end_session_endpoint = provider_metadata
|
|
||||||
.additional_metadata()
|
|
||||||
.end_session_endpoint
|
|
||||||
.clone()
|
|
||||||
.map(Uri::from_maybe_shared)
|
|
||||||
.transpose()
|
|
||||||
.map_err(Error::InvalidEndSessionEndpoint)?;
|
|
||||||
let client = Client::from_provider_metadata(
|
|
||||||
provider_metadata,
|
|
||||||
ClientId::new(self.credentials.id.to_string()),
|
|
||||||
self.credentials.secret.clone(),
|
|
||||||
)
|
|
||||||
.set_redirect_uri(openidconnect::RedirectUrl::new(
|
|
||||||
self.redirect_url.0.to_string(),
|
|
||||||
)?);
|
|
||||||
|
|
||||||
Ok(Builder {
|
|
||||||
credentials: self.credentials,
|
|
||||||
client: OpenidconnectClient(client),
|
|
||||||
http_client: self.http_client,
|
|
||||||
redirect_url: self.redirect_url,
|
|
||||||
end_session_endpoint,
|
|
||||||
scopes: self.scopes,
|
|
||||||
auth_context_class: self.auth_context_class,
|
|
||||||
untrusted_audiences: self.untrusted_audiences,
|
|
||||||
_ac: self._ac,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/// discover issuer details
|
|
||||||
pub async fn discover(
|
|
||||||
self,
|
|
||||||
issuer: IssuerUrl,
|
|
||||||
) -> Result<
|
|
||||||
Builder<AC, ClientCredentials, OpenidconnectClient<AC>, HttpClient, RedirectUrl>,
|
|
||||||
Error,
|
|
||||||
> {
|
|
||||||
let http_client = self.http_client.0.clone();
|
|
||||||
let provider_metadata = ProviderMetadata::discover_async(issuer, &http_client);
|
|
||||||
|
|
||||||
Self::manual(self, provider_metadata.await?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<AC: AdditionalClaims>
|
|
||||||
Builder<AC, ClientCredentials, OpenidconnectClient<AC>, HttpClient, RedirectUrl>
|
|
||||||
{
|
|
||||||
/// create oidc client
|
|
||||||
pub fn build(self) -> OidcClient<AC> {
|
|
||||||
OidcClient {
|
|
||||||
scopes: self.scopes,
|
|
||||||
client_id: self.credentials.id,
|
|
||||||
client: self.client.0,
|
|
||||||
http_client: self.http_client.0,
|
|
||||||
end_session_endpoint: self.end_session_endpoint,
|
|
||||||
auth_context_class: self.auth_context_class,
|
|
||||||
untrusted_audiences: self.untrusted_audiences,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
106
src/error.rs
106
src/error.rs
|
|
@ -10,19 +10,10 @@ use thiserror::Error;
|
||||||
pub enum ExtractorError {
|
pub enum ExtractorError {
|
||||||
#[error("unauthorized")]
|
#[error("unauthorized")]
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
|
|
||||||
#[error("rp initiated logout not supported by issuer")]
|
|
||||||
RpInitiatedLogoutNotSupported,
|
|
||||||
|
|
||||||
#[error("could not build rp initiated logout uri")]
|
|
||||||
FailedToCreateRpInitiatedLogoutUri,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum MiddlewareError {
|
pub enum MiddlewareError {
|
||||||
#[error("configuration: {0:?}")]
|
|
||||||
Configuration(#[from] openidconnect::ConfigurationError),
|
|
||||||
|
|
||||||
#[error("access token hash invalid")]
|
#[error("access token hash invalid")]
|
||||||
AccessTokenHashInvalid,
|
AccessTokenHashInvalid,
|
||||||
|
|
||||||
|
|
@ -35,20 +26,9 @@ pub enum MiddlewareError {
|
||||||
#[error("signing: {0:?}")]
|
#[error("signing: {0:?}")]
|
||||||
Signing(#[from] openidconnect::SigningError),
|
Signing(#[from] openidconnect::SigningError),
|
||||||
|
|
||||||
#[error("signature verification: {0:?}")]
|
|
||||||
Signature(#[from] openidconnect::SignatureVerificationError),
|
|
||||||
|
|
||||||
#[error("claims verification: {0:?}")]
|
#[error("claims verification: {0:?}")]
|
||||||
ClaimsVerification(#[from] openidconnect::ClaimsVerificationError),
|
ClaimsVerification(#[from] openidconnect::ClaimsVerificationError),
|
||||||
|
|
||||||
#[error("user info retrieval: {0:?}")]
|
|
||||||
UserInfoRetrieval(
|
|
||||||
#[from]
|
|
||||||
openidconnect::UserInfoError<
|
|
||||||
openidconnect::HttpClientError<openidconnect::reqwest::Error>,
|
|
||||||
>,
|
|
||||||
),
|
|
||||||
|
|
||||||
#[error("url parsing: {0:?}")]
|
#[error("url parsing: {0:?}")]
|
||||||
UrlParsing(#[from] openidconnect::url::ParseError),
|
UrlParsing(#[from] openidconnect::url::ParseError),
|
||||||
|
|
||||||
|
|
@ -62,7 +42,7 @@ pub enum MiddlewareError {
|
||||||
RequestToken(
|
RequestToken(
|
||||||
#[from]
|
#[from]
|
||||||
openidconnect::RequestTokenError<
|
openidconnect::RequestTokenError<
|
||||||
openidconnect::HttpClientError<openidconnect::reqwest::Error>,
|
openidconnect::reqwest::Error<reqwest::Error>,
|
||||||
StandardErrorResponse<CoreErrorResponseType>,
|
StandardErrorResponse<CoreErrorResponseType>,
|
||||||
>,
|
>,
|
||||||
),
|
),
|
||||||
|
|
@ -78,48 +58,6 @@ pub enum MiddlewareError {
|
||||||
|
|
||||||
#[error("auth middleware not found")]
|
#[error("auth middleware not found")]
|
||||||
AuthMiddlewareNotFound,
|
AuthMiddlewareNotFound,
|
||||||
|
|
||||||
#[error("original url not found")]
|
|
||||||
OriginalUrlNotFound,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum HandlerError {
|
|
||||||
#[error("redirect handler accessed without valid session, session cookie missing?")]
|
|
||||||
RedirectedWithoutSession,
|
|
||||||
|
|
||||||
#[error("csrf token invalid")]
|
|
||||||
CsrfTokenInvalid,
|
|
||||||
|
|
||||||
#[error("id token missing")]
|
|
||||||
IdTokenMissing,
|
|
||||||
|
|
||||||
#[error("access token hash invalid")]
|
|
||||||
AccessTokenHashInvalid,
|
|
||||||
|
|
||||||
#[error("signing: {0:?}")]
|
|
||||||
Signing(#[from] openidconnect::SigningError),
|
|
||||||
|
|
||||||
#[error("signature verification: {0:?}")]
|
|
||||||
Signature(#[from] openidconnect::SignatureVerificationError),
|
|
||||||
|
|
||||||
#[error("session error: {0:?}")]
|
|
||||||
Session(#[from] tower_sessions::session::Error),
|
|
||||||
|
|
||||||
#[error("configuration: {0:?}")]
|
|
||||||
Configuration(#[from] openidconnect::ConfigurationError),
|
|
||||||
|
|
||||||
#[error("request token: {0:?}")]
|
|
||||||
RequestToken(
|
|
||||||
#[from]
|
|
||||||
openidconnect::RequestTokenError<
|
|
||||||
openidconnect::HttpClientError<openidconnect::reqwest::Error>,
|
|
||||||
StandardErrorResponse<CoreErrorResponseType>,
|
|
||||||
>,
|
|
||||||
),
|
|
||||||
|
|
||||||
#[error("claims verification: {0:?}")]
|
|
||||||
ClaimsVerification(#[from] openidconnect::ClaimsVerificationError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
@ -127,58 +65,36 @@ pub enum Error {
|
||||||
#[error("url parsing: {0:?}")]
|
#[error("url parsing: {0:?}")]
|
||||||
UrlParsing(#[from] openidconnect::url::ParseError),
|
UrlParsing(#[from] openidconnect::url::ParseError),
|
||||||
|
|
||||||
#[error("invalid end_session_endpoint uri: {0:?}")]
|
|
||||||
InvalidEndSessionEndpoint(http::uri::InvalidUri),
|
|
||||||
|
|
||||||
#[error("discovery: {0:?}")]
|
#[error("discovery: {0:?}")]
|
||||||
Discovery(
|
Discovery(#[from] openidconnect::DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>),
|
||||||
#[from]
|
|
||||||
openidconnect::DiscoveryError<
|
|
||||||
openidconnect::HttpClientError<openidconnect::reqwest::Error>,
|
|
||||||
>,
|
|
||||||
),
|
|
||||||
|
|
||||||
#[error("extractor: {0:?}")]
|
#[error("extractor: {0:?}")]
|
||||||
Extractor(#[from] ExtractorError),
|
Extractor(#[from] ExtractorError),
|
||||||
|
|
||||||
#[error("extractor: {0:?}")]
|
#[error("extractor: {0:?}")]
|
||||||
Middleware(#[from] MiddlewareError),
|
Middleware(#[from] MiddlewareError),
|
||||||
|
|
||||||
#[error("handler: {0:?}")]
|
|
||||||
Handler(#[from] HandlerError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for ExtractorError {
|
impl IntoResponse for ExtractorError {
|
||||||
fn into_response(self) -> axum_core::response::Response {
|
fn into_response(self) -> axum_core::response::Response {
|
||||||
match self {
|
(StatusCode::UNAUTHORIZED, "unauthorized").into_response()
|
||||||
Self::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized").into_response(),
|
|
||||||
Self::RpInitiatedLogoutNotSupported => {
|
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, "intenal server error").into_response()
|
|
||||||
}
|
|
||||||
Self::FailedToCreateRpInitiatedLogoutUri => {
|
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, "intenal server error").into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for Error {
|
impl IntoResponse for Error {
|
||||||
fn into_response(self) -> axum_core::response::Response {
|
fn into_response(self) -> axum_core::response::Response {
|
||||||
tracing::error!(error = self.to_string());
|
dbg!(&self);
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response()
|
match self {
|
||||||
|
_ => (StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for MiddlewareError {
|
impl IntoResponse for MiddlewareError {
|
||||||
fn into_response(self) -> axum_core::response::Response {
|
fn into_response(self) -> axum_core::response::Response {
|
||||||
tracing::error!(error = self.to_string());
|
dbg!(&self);
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response()
|
match self {
|
||||||
}
|
_ => (StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response(),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for HandlerError {
|
|
||||||
fn into_response(self) -> axum_core::response::Response {
|
|
||||||
tracing::error!(error = self.to_string());
|
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
207
src/extractor.rs
207
src/extractor.rs
|
|
@ -1,20 +1,18 @@
|
||||||
use std::{borrow::Cow, convert::Infallible, ops::Deref};
|
use std::ops::Deref;
|
||||||
|
|
||||||
use crate::{error::ExtractorError, AdditionalClaims, ClearSessionFlag};
|
use crate::{error::ExtractorError, AdditionalClaims};
|
||||||
use axum::response::Redirect;
|
use async_trait::async_trait;
|
||||||
use axum_core::{
|
use axum_core::extract::FromRequestParts;
|
||||||
extract::{FromRequestParts, OptionalFromRequestParts},
|
use http::request::Parts;
|
||||||
response::IntoResponse,
|
use openidconnect::{core::CoreGenderClaim, IdTokenClaims};
|
||||||
};
|
|
||||||
use http::{request::Parts, uri::PathAndQuery, Uri};
|
|
||||||
use openidconnect::{core::CoreGenderClaim, ClientId, IdTokenClaims, UserInfoClaims};
|
|
||||||
|
|
||||||
/// Extractor for the OpenID Connect Claims.
|
/// Extractor for the OpenID Connect Claims.
|
||||||
///
|
///
|
||||||
/// This Extractor will only return the Claims when the cached session is valid and [`crate::middleware::OidcAuthMiddleware`] is loaded.
|
/// This Extractor will only return the Claims when the cached session is valid and [crate::middleware::OidcAuthMiddleware] is loaded.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone)]
|
||||||
pub struct OidcClaims<AC: AdditionalClaims>(pub IdTokenClaims<AC, CoreGenderClaim>);
|
pub struct OidcClaims<AC: AdditionalClaims>(pub IdTokenClaims<AC, CoreGenderClaim>);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl<S, AC> FromRequestParts<S> for OidcClaims<AC>
|
impl<S, AC> FromRequestParts<S> for OidcClaims<AC>
|
||||||
where
|
where
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
|
|
@ -31,18 +29,6 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, AC> OptionalFromRequestParts<S> for OidcClaims<AC>
|
|
||||||
where
|
|
||||||
S: Send + Sync,
|
|
||||||
AC: AdditionalClaims,
|
|
||||||
{
|
|
||||||
type Rejection = Infallible;
|
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Option<Self>, Self::Rejection> {
|
|
||||||
Ok(parts.extensions.get::<Self>().cloned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<AC: AdditionalClaims> Deref for OidcClaims<AC> {
|
impl<AC: AdditionalClaims> Deref for OidcClaims<AC> {
|
||||||
type Target = IdTokenClaims<AC, CoreGenderClaim>;
|
type Target = IdTokenClaims<AC, CoreGenderClaim>;
|
||||||
|
|
||||||
|
|
@ -62,10 +48,11 @@ where
|
||||||
|
|
||||||
/// Extractor for the OpenID Connect Access Token.
|
/// Extractor for the OpenID Connect Access Token.
|
||||||
///
|
///
|
||||||
/// This Extractor will only return the Access Token when the cached session is valid and [`crate::middleware::OidcAuthMiddleware`] is loaded.
|
/// This Extractor will only return the Access Token when the cached session is valid and [crate::middleware::OidcAuthMiddleware] is loaded.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct OidcAccessToken(pub String);
|
pub struct OidcAccessToken(pub String);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl<S> FromRequestParts<S> for OidcAccessToken
|
impl<S> FromRequestParts<S> for OidcAccessToken
|
||||||
where
|
where
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
|
|
@ -81,17 +68,6 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> OptionalFromRequestParts<S> for OidcAccessToken
|
|
||||||
where
|
|
||||||
S: Send + Sync,
|
|
||||||
{
|
|
||||||
type Rejection = Infallible;
|
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Option<Self>, Self::Rejection> {
|
|
||||||
Ok(parts.extensions.get::<Self>().cloned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for OidcAccessToken {
|
impl Deref for OidcAccessToken {
|
||||||
type Target = str;
|
type Target = str;
|
||||||
|
|
||||||
|
|
@ -102,165 +78,6 @@ impl Deref for OidcAccessToken {
|
||||||
|
|
||||||
impl AsRef<str> for OidcAccessToken {
|
impl AsRef<str> for OidcAccessToken {
|
||||||
fn as_ref(&self) -> &str {
|
fn as_ref(&self) -> &str {
|
||||||
&self.0
|
self.0.as_str()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extractor for the [OpenID Connect RP-Initialized Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) URL
|
|
||||||
///
|
|
||||||
/// This Extractor will only succed when the cached session is valid, [`crate::middleware::OidcAuthMiddleware`] is loaded and the issuer supports RP-Initialized Logout.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct OidcRpInitiatedLogout {
|
|
||||||
pub(crate) end_session_endpoint: Uri,
|
|
||||||
pub(crate) id_token_hint: Box<str>,
|
|
||||||
pub(crate) client_id: ClientId,
|
|
||||||
pub(crate) post_logout_redirect_uri: Option<Uri>,
|
|
||||||
pub(crate) state: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OidcRpInitiatedLogout {
|
|
||||||
/// set uri that the user is redirected to after logout.
|
|
||||||
/// This uri must be in the allowed by issuer.
|
|
||||||
pub fn with_post_logout_redirect(mut self, uri: Uri) -> Self {
|
|
||||||
self.post_logout_redirect_uri = Some(uri);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// set the state parameter that is appended as a query to the post logout redirect uri.
|
|
||||||
pub fn with_state(mut self, state: String) -> Self {
|
|
||||||
self.state = Some(state);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// get the uri that the client needs to access for logout. This does **NOT** delete the
|
|
||||||
/// session in axum-oidc. You should use the [`ClearSessionFlag`] responder or include
|
|
||||||
/// [`OidcRpInitiatedLogout`] in the response extensions
|
|
||||||
pub fn uri(&self) -> Result<Uri, http::Error> {
|
|
||||||
let mut parts = self.end_session_endpoint.clone().into_parts();
|
|
||||||
|
|
||||||
let query = {
|
|
||||||
let mut query: Vec<(&str, Cow<'_, str>)> = Vec::with_capacity(4);
|
|
||||||
query.push(("id_token_hint", Cow::Borrowed(&self.id_token_hint)));
|
|
||||||
query.push(("client_id", Cow::Borrowed(&self.client_id)));
|
|
||||||
|
|
||||||
if let Some(post_logout_redirect_uri) = &self.post_logout_redirect_uri {
|
|
||||||
query.push((
|
|
||||||
"post_logout_redirect_uri",
|
|
||||||
Cow::Owned(post_logout_redirect_uri.to_string()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if let Some(state) = &self.state {
|
|
||||||
query.push(("state", Cow::Borrowed(state)));
|
|
||||||
}
|
|
||||||
|
|
||||||
query
|
|
||||||
.into_iter()
|
|
||||||
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(&v)))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("&")
|
|
||||||
};
|
|
||||||
|
|
||||||
let path_and_query = match parts.path_and_query {
|
|
||||||
Some(path_and_query) => {
|
|
||||||
PathAndQuery::from_maybe_shared(format!("{}?{}", path_and_query.path(), query))
|
|
||||||
}
|
|
||||||
None => PathAndQuery::from_maybe_shared(format!("?{}", query)),
|
|
||||||
};
|
|
||||||
parts.path_and_query = Some(path_and_query?);
|
|
||||||
|
|
||||||
Ok(Uri::from_parts(parts)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> FromRequestParts<S> for OidcRpInitiatedLogout
|
|
||||||
where
|
|
||||||
S: Send + Sync,
|
|
||||||
{
|
|
||||||
type Rejection = ExtractorError;
|
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
|
|
||||||
match parts
|
|
||||||
.extensions
|
|
||||||
.get::<Option<Self>>()
|
|
||||||
.cloned()
|
|
||||||
.ok_or(ExtractorError::Unauthorized)?
|
|
||||||
{
|
|
||||||
Some(this) => Ok(this),
|
|
||||||
None => Err(ExtractorError::RpInitiatedLogoutNotSupported),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> OptionalFromRequestParts<S> for OidcRpInitiatedLogout
|
|
||||||
where
|
|
||||||
S: Send + Sync,
|
|
||||||
{
|
|
||||||
type Rejection = Infallible;
|
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Option<Self>, Self::Rejection> {
|
|
||||||
Ok(parts.extensions.get::<Option<Self>>().cloned().flatten())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoResponse for OidcRpInitiatedLogout {
|
|
||||||
/// redirect to the logout uri and signal the [`crate::middleware::OidcAuthMiddleware`] that
|
|
||||||
/// the session should be cleared
|
|
||||||
fn into_response(self) -> axum_core::response::Response {
|
|
||||||
if let Ok(uri) = self.uri() {
|
|
||||||
let mut response = Redirect::temporary(&uri.to_string()).into_response();
|
|
||||||
response.extensions_mut().insert(ClearSessionFlag);
|
|
||||||
response
|
|
||||||
} else {
|
|
||||||
ExtractorError::FailedToCreateRpInitiatedLogoutUri.into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extractor for the OpenID Connect User Info Claims.
|
|
||||||
///
|
|
||||||
/// This Extractor will only return the User Info Claims when the cached session is valid and [`crate::middleware::OidcAuthMiddleware`] is loaded.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct OidcUserInfo<AC: AdditionalClaims>(pub UserInfoClaims<AC, CoreGenderClaim>);
|
|
||||||
|
|
||||||
impl<S, AC> FromRequestParts<S> for OidcUserInfo<AC>
|
|
||||||
where
|
|
||||||
S: Send + Sync,
|
|
||||||
AC: AdditionalClaims,
|
|
||||||
{
|
|
||||||
type Rejection = ExtractorError;
|
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
|
|
||||||
parts
|
|
||||||
.extensions
|
|
||||||
.get::<Self>()
|
|
||||||
.cloned()
|
|
||||||
.ok_or(ExtractorError::Unauthorized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, AC> OptionalFromRequestParts<S> for OidcUserInfo<AC>
|
|
||||||
where
|
|
||||||
S: Send + Sync,
|
|
||||||
AC: AdditionalClaims,
|
|
||||||
{
|
|
||||||
type Rejection = Infallible;
|
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Option<Self>, Self::Rejection> {
|
|
||||||
Ok(parts.extensions.get::<Self>().cloned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<AC: AdditionalClaims> Deref for OidcUserInfo<AC> {
|
|
||||||
type Target = UserInfoClaims<AC, CoreGenderClaim>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<AC> AsRef<UserInfoClaims<AC, CoreGenderClaim>> for OidcUserInfo<AC>
|
|
||||||
where
|
|
||||||
AC: AdditionalClaims,
|
|
||||||
{
|
|
||||||
fn as_ref(&self) -> &UserInfoClaims<AC, CoreGenderClaim> {
|
|
||||||
&self.0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
122
src/handler.rs
122
src/handler.rs
|
|
@ -1,122 +0,0 @@
|
||||||
use axum::{extract::Query, response::Redirect, Extension};
|
|
||||||
use openidconnect::{
|
|
||||||
core::{CoreGenderClaim, CoreJsonWebKey},
|
|
||||||
AccessToken, AccessTokenHash, AuthorizationCode, IdTokenClaims, IdTokenVerifier,
|
|
||||||
OAuth2TokenResponse, PkceCodeVerifier, TokenResponse,
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use tower_sessions::Session;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
error::HandlerError, AdditionalClaims, AuthenticatedSession, IdToken, OidcClient, OidcSession,
|
|
||||||
SESSION_KEY,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// response data of the openid issuer after login
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct OidcQuery {
|
|
||||||
code: String,
|
|
||||||
state: String,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
session_state: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(oidcclient), err)]
|
|
||||||
pub async fn handle_oidc_redirect<AC: AdditionalClaims>(
|
|
||||||
session: Session,
|
|
||||||
Extension(oidcclient): Extension<OidcClient<AC>>,
|
|
||||||
Query(query): Query<OidcQuery>,
|
|
||||||
) -> Result<impl axum::response::IntoResponse, HandlerError> {
|
|
||||||
tracing::debug!("start handling oidc redirect");
|
|
||||||
|
|
||||||
let mut login_session: OidcSession<AC> = session
|
|
||||||
.get(SESSION_KEY)
|
|
||||||
.await?
|
|
||||||
.ok_or(HandlerError::RedirectedWithoutSession)?;
|
|
||||||
// the request has the request headers of the oidc redirect
|
|
||||||
// parse the headers and exchange the code for a valid token
|
|
||||||
|
|
||||||
tracing::debug!("validating scrf token");
|
|
||||||
if login_session.csrf_token.secret() != &query.state {
|
|
||||||
return Err(HandlerError::CsrfTokenInvalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::debug!("obtain token response");
|
|
||||||
let token_response = oidcclient
|
|
||||||
.client
|
|
||||||
.exchange_code(AuthorizationCode::new(query.code.to_string()))?
|
|
||||||
// Set the PKCE code verifier.
|
|
||||||
.set_pkce_verifier(PkceCodeVerifier::new(
|
|
||||||
login_session.pkce_verifier.secret().to_string(),
|
|
||||||
))
|
|
||||||
.request_async(&oidcclient.http_client)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
tracing::debug!("extract claims and verify it");
|
|
||||||
// Extract the ID token claims after verifying its authenticity and nonce.
|
|
||||||
let id_token = token_response
|
|
||||||
.id_token()
|
|
||||||
.ok_or(HandlerError::IdTokenMissing)?;
|
|
||||||
let id_token_verifier = oidcclient
|
|
||||||
.client
|
|
||||||
.id_token_verifier()
|
|
||||||
.set_other_audience_verifier_fn(|audience|
|
|
||||||
// Return false (reject) if audience is in list of untrusted audiences
|
|
||||||
!oidcclient.untrusted_audiences.contains(audience));
|
|
||||||
let claims = id_token.claims(&id_token_verifier, &login_session.nonce)?;
|
|
||||||
|
|
||||||
tracing::debug!("validate access token hash");
|
|
||||||
validate_access_token_hash(
|
|
||||||
id_token,
|
|
||||||
id_token_verifier,
|
|
||||||
token_response.access_token(),
|
|
||||||
claims,
|
|
||||||
)
|
|
||||||
.inspect_err(|e| tracing::error!(?e, "Access token hash invalid"))?;
|
|
||||||
|
|
||||||
tracing::debug!("Access token hash validated");
|
|
||||||
|
|
||||||
login_session.authenticated = Some(AuthenticatedSession {
|
|
||||||
id_token: id_token.clone(),
|
|
||||||
access_token: token_response.access_token().clone(),
|
|
||||||
});
|
|
||||||
let refresh_token = token_response.refresh_token().cloned();
|
|
||||||
if let Some(refresh_token) = refresh_token {
|
|
||||||
login_session.refresh_token = Some(refresh_token);
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::debug!(
|
|
||||||
"Inserting session and redirecting to {}",
|
|
||||||
&login_session.redirect_url
|
|
||||||
);
|
|
||||||
let redirect_url = login_session.redirect_url.clone();
|
|
||||||
session.insert(SESSION_KEY, login_session).await?;
|
|
||||||
|
|
||||||
Ok(Redirect::to(&redirect_url))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify the access token hash to ensure that the access token hasn't been substituted for
|
|
||||||
/// another user's.
|
|
||||||
/// Returns `Ok` when access token is valid
|
|
||||||
#[tracing::instrument(skip_all, err)]
|
|
||||||
fn validate_access_token_hash<AC: AdditionalClaims>(
|
|
||||||
id_token: &IdToken<AC>,
|
|
||||||
id_token_verifier: IdTokenVerifier<CoreJsonWebKey>,
|
|
||||||
access_token: &AccessToken,
|
|
||||||
claims: &IdTokenClaims<AC, CoreGenderClaim>,
|
|
||||||
) -> Result<(), HandlerError> {
|
|
||||||
if let Some(expected_access_token_hash) = claims.access_token_hash() {
|
|
||||||
let actual_access_token_hash = AccessTokenHash::from_token(
|
|
||||||
access_token,
|
|
||||||
id_token.signing_alg()?,
|
|
||||||
id_token.signing_key(&id_token_verifier)?,
|
|
||||||
)?;
|
|
||||||
if actual_access_token_hash == *expected_access_token_hash {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(HandlerError::AccessTokenHashInvalid)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
149
src/lib.rs
149
src/lib.rs
|
|
@ -1,40 +1,32 @@
|
||||||
#![deny(unsafe_code)]
|
|
||||||
#![deny(clippy::unwrap_used)]
|
|
||||||
#![deny(warnings)]
|
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::error::Error;
|
||||||
use http::Uri;
|
use http::Uri;
|
||||||
use openidconnect::{
|
use openidconnect::{
|
||||||
core::{
|
core::{
|
||||||
CoreAuthDisplay, CoreAuthPrompt, CoreClaimName, CoreClaimType, CoreClientAuthMethod,
|
CoreAuthDisplay, CoreAuthPrompt, CoreErrorResponseType, CoreGenderClaim, CoreJsonWebKey,
|
||||||
CoreErrorResponseType, CoreGenderClaim, CoreGrantType, CoreJsonWebKey,
|
CoreJsonWebKeyType, CoreJsonWebKeyUse, CoreJweContentEncryptionAlgorithm,
|
||||||
CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreJwsSigningAlgorithm,
|
CoreJwsSigningAlgorithm, CoreProviderMetadata, CoreRevocableToken,
|
||||||
CoreResponseMode, CoreResponseType, CoreRevocableToken, CoreRevocationErrorResponse,
|
CoreRevocationErrorResponse, CoreTokenIntrospectionResponse, CoreTokenType,
|
||||||
CoreSubjectIdentifierType, CoreTokenIntrospectionResponse, CoreTokenType,
|
|
||||||
},
|
},
|
||||||
AccessToken, Audience, AuthenticationContextClass, ClientId, CsrfToken, EmptyExtraTokenFields,
|
reqwest::async_http_client,
|
||||||
EndpointMaybeSet, EndpointNotSet, EndpointSet, IdTokenFields, Nonce, PkceCodeVerifier,
|
ClientId, ClientSecret, CsrfToken, EmptyExtraTokenFields, IdTokenFields, IssuerUrl, Nonce,
|
||||||
RefreshToken, Scope, StandardErrorResponse, StandardTokenResponse,
|
PkceCodeVerifier, RefreshToken, StandardErrorResponse, StandardTokenResponse,
|
||||||
};
|
};
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub mod builder;
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
mod extractor;
|
mod extractor;
|
||||||
mod handler;
|
|
||||||
mod middleware;
|
mod middleware;
|
||||||
|
|
||||||
pub use extractor::{OidcAccessToken, OidcClaims, OidcRpInitiatedLogout, OidcUserInfo};
|
pub use extractor::{OidcAccessToken, OidcClaims};
|
||||||
pub use handler::handle_oidc_redirect;
|
|
||||||
pub use middleware::{OidcAuthLayer, OidcAuthMiddleware, OidcLoginLayer, OidcLoginMiddleware};
|
pub use middleware::{OidcAuthLayer, OidcAuthMiddleware, OidcLoginLayer, OidcLoginMiddleware};
|
||||||
pub use openidconnect;
|
|
||||||
|
|
||||||
const SESSION_KEY: &str = "axum-oidc";
|
const SESSION_KEY: &str = "axum-oidc";
|
||||||
|
|
||||||
pub trait AdditionalClaims:
|
pub trait AdditionalClaims: openidconnect::AdditionalClaims + Clone + Sync + Send {}
|
||||||
openidconnect::AdditionalClaims + Clone + Sync + Send + Serialize + DeserializeOwned
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
type OidcTokenResponse<AC> = StandardTokenResponse<
|
type OidcTokenResponse<AC> = StandardTokenResponse<
|
||||||
IdTokenFields<
|
IdTokenFields<
|
||||||
|
|
@ -43,6 +35,7 @@ type OidcTokenResponse<AC> = StandardTokenResponse<
|
||||||
CoreGenderClaim,
|
CoreGenderClaim,
|
||||||
CoreJweContentEncryptionAlgorithm,
|
CoreJweContentEncryptionAlgorithm,
|
||||||
CoreJwsSigningAlgorithm,
|
CoreJwsSigningAlgorithm,
|
||||||
|
CoreJsonWebKeyType,
|
||||||
>,
|
>,
|
||||||
CoreTokenType,
|
CoreTokenType,
|
||||||
>;
|
>;
|
||||||
|
|
@ -52,49 +45,25 @@ pub type IdToken<AZ> = openidconnect::IdToken<
|
||||||
CoreGenderClaim,
|
CoreGenderClaim,
|
||||||
CoreJweContentEncryptionAlgorithm,
|
CoreJweContentEncryptionAlgorithm,
|
||||||
CoreJwsSigningAlgorithm,
|
CoreJwsSigningAlgorithm,
|
||||||
|
CoreJsonWebKeyType,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type Client<
|
type Client<AC> = openidconnect::Client<
|
||||||
AC,
|
|
||||||
HasAuthUrl = EndpointSet,
|
|
||||||
HasDeviceAuthUrl = EndpointNotSet,
|
|
||||||
HasIntrospectionUrl = EndpointNotSet,
|
|
||||||
HasRevocationUrl = EndpointNotSet,
|
|
||||||
HasTokenUrl = EndpointMaybeSet,
|
|
||||||
HasUserInfoUrl = EndpointMaybeSet,
|
|
||||||
> = openidconnect::Client<
|
|
||||||
AC,
|
AC,
|
||||||
CoreAuthDisplay,
|
CoreAuthDisplay,
|
||||||
CoreGenderClaim,
|
CoreGenderClaim,
|
||||||
CoreJweContentEncryptionAlgorithm,
|
CoreJweContentEncryptionAlgorithm,
|
||||||
|
CoreJwsSigningAlgorithm,
|
||||||
|
CoreJsonWebKeyType,
|
||||||
|
CoreJsonWebKeyUse,
|
||||||
CoreJsonWebKey,
|
CoreJsonWebKey,
|
||||||
CoreAuthPrompt,
|
CoreAuthPrompt,
|
||||||
StandardErrorResponse<CoreErrorResponseType>,
|
StandardErrorResponse<CoreErrorResponseType>,
|
||||||
OidcTokenResponse<AC>,
|
OidcTokenResponse<AC>,
|
||||||
|
CoreTokenType,
|
||||||
CoreTokenIntrospectionResponse,
|
CoreTokenIntrospectionResponse,
|
||||||
CoreRevocableToken,
|
CoreRevocableToken,
|
||||||
CoreRevocationErrorResponse,
|
CoreRevocationErrorResponse,
|
||||||
HasAuthUrl,
|
|
||||||
HasDeviceAuthUrl,
|
|
||||||
HasIntrospectionUrl,
|
|
||||||
HasRevocationUrl,
|
|
||||||
HasTokenUrl,
|
|
||||||
HasUserInfoUrl,
|
|
||||||
>;
|
|
||||||
|
|
||||||
pub type ProviderMetadata = openidconnect::ProviderMetadata<
|
|
||||||
AdditionalProviderMetadata,
|
|
||||||
CoreAuthDisplay,
|
|
||||||
CoreClientAuthMethod,
|
|
||||||
CoreClaimName,
|
|
||||||
CoreClaimType,
|
|
||||||
CoreGrantType,
|
|
||||||
CoreJweContentEncryptionAlgorithm,
|
|
||||||
CoreJweKeyManagementAlgorithm,
|
|
||||||
CoreJsonWebKey,
|
|
||||||
CoreResponseMode,
|
|
||||||
CoreResponseType,
|
|
||||||
CoreSubjectIdentifierType,
|
|
||||||
>;
|
>;
|
||||||
|
|
||||||
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
|
@ -102,13 +71,33 @@ pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||||
/// OpenID Connect Client
|
/// OpenID Connect Client
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct OidcClient<AC: AdditionalClaims> {
|
pub struct OidcClient<AC: AdditionalClaims> {
|
||||||
scopes: Vec<Scope>,
|
scopes: Vec<String>,
|
||||||
client_id: ClientId,
|
|
||||||
client: Client<AC>,
|
client: Client<AC>,
|
||||||
http_client: reqwest::Client,
|
application_base_url: Uri,
|
||||||
end_session_endpoint: Option<Uri>,
|
}
|
||||||
auth_context_class: Option<AuthenticationContextClass>,
|
|
||||||
untrusted_audiences: Vec<Audience>,
|
impl<AC: AdditionalClaims> OidcClient<AC> {
|
||||||
|
pub async fn discover_new(
|
||||||
|
application_base_url: Uri,
|
||||||
|
issuer: String,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: Option<String>,
|
||||||
|
scopes: Vec<String>,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let provider_metadata =
|
||||||
|
CoreProviderMetadata::discover_async(IssuerUrl::new(issuer)?, async_http_client)
|
||||||
|
.await?;
|
||||||
|
let client = Client::from_provider_metadata(
|
||||||
|
provider_metadata,
|
||||||
|
ClientId::new(client_id),
|
||||||
|
client_secret.map(ClientSecret::new),
|
||||||
|
);
|
||||||
|
Ok(Self {
|
||||||
|
scopes,
|
||||||
|
client,
|
||||||
|
application_base_url,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// an empty struct to be used as the default type for the additional claims generic
|
/// an empty struct to be used as the default type for the additional claims generic
|
||||||
|
|
@ -117,33 +106,35 @@ pub struct EmptyAdditionalClaims {}
|
||||||
impl AdditionalClaims for EmptyAdditionalClaims {}
|
impl AdditionalClaims for EmptyAdditionalClaims {}
|
||||||
impl openidconnect::AdditionalClaims for EmptyAdditionalClaims {}
|
impl openidconnect::AdditionalClaims for EmptyAdditionalClaims {}
|
||||||
|
|
||||||
|
/// response data of the openid issuer after login
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct OidcQuery {
|
||||||
|
code: String,
|
||||||
|
state: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
session_state: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// oidc session
|
/// oidc session
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(bound = "AC: Serialize + DeserializeOwned")]
|
struct OidcSession {
|
||||||
struct OidcSession<AC: AdditionalClaims> {
|
|
||||||
nonce: Nonce,
|
nonce: Nonce,
|
||||||
csrf_token: CsrfToken,
|
csrf_token: CsrfToken,
|
||||||
pkce_verifier: PkceCodeVerifier,
|
pkce_verifier: PkceCodeVerifier,
|
||||||
authenticated: Option<AuthenticatedSession<AC>>,
|
id_token: Option<String>,
|
||||||
refresh_token: Option<RefreshToken>,
|
access_token: Option<String>,
|
||||||
redirect_url: Box<str>,
|
refresh_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
impl OidcSession {
|
||||||
#[serde(bound = "AC: Serialize + DeserializeOwned")]
|
pub(crate) fn id_token<AC: AdditionalClaims>(&self) -> Option<IdToken<AC>> {
|
||||||
struct AuthenticatedSession<AC: AdditionalClaims> {
|
self.id_token
|
||||||
id_token: IdToken<AC>,
|
.as_ref()
|
||||||
access_token: AccessToken,
|
.map(|x| IdToken::<AC>::from_str(x).unwrap())
|
||||||
|
}
|
||||||
|
pub(crate) fn refresh_token(&self) -> Option<RefreshToken> {
|
||||||
|
self.refresh_token
|
||||||
|
.as_ref()
|
||||||
|
.map(|x| RefreshToken::new(x.to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// additional metadata that is discovered on client creation via the
|
|
||||||
/// `.well-knwon/openid-configuration` endpoint.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct AdditionalProviderMetadata {
|
|
||||||
end_session_endpoint: Option<String>,
|
|
||||||
}
|
|
||||||
impl openidconnect::AdditionalProviderMetadata for AdditionalProviderMetadata {}
|
|
||||||
|
|
||||||
/// response extension flag to signal the [`OidcAuthLayer`] that the session should be cleared.
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub struct ClearSessionFlag;
|
|
||||||
|
|
|
||||||
|
|
@ -4,32 +4,32 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::OriginalUri,
|
extract::Query,
|
||||||
response::{IntoResponse, Redirect},
|
response::{IntoResponse, Redirect},
|
||||||
};
|
};
|
||||||
use axum_core::response::Response;
|
use axum_core::{extract::FromRequestParts, response::Response};
|
||||||
use futures_util::future::BoxFuture;
|
use futures_util::future::BoxFuture;
|
||||||
use http::{request::Parts, Request};
|
use http::{uri::PathAndQuery, Request, Uri};
|
||||||
use tower_layer::Layer;
|
use tower_layer::Layer;
|
||||||
use tower_service::Service;
|
use tower_service::Service;
|
||||||
use tower_sessions::Session;
|
use tower_sessions::Session;
|
||||||
|
|
||||||
use openidconnect::{
|
use openidconnect::{
|
||||||
core::{CoreAuthenticationFlow, CoreErrorResponseType, CoreGenderClaim, CoreJsonWebKey},
|
core::{CoreAuthenticationFlow, CoreErrorResponseType},
|
||||||
AccessToken, AccessTokenHash, CsrfToken, IdTokenClaims, IdTokenVerifier, Nonce,
|
reqwest::async_http_client,
|
||||||
NonceVerifier as _, OAuth2TokenResponse, PkceCodeChallenge, RefreshToken,
|
AccessTokenHash, AuthorizationCode, CsrfToken, Nonce, OAuth2TokenResponse, PkceCodeChallenge,
|
||||||
|
PkceCodeVerifier, RedirectUrl,
|
||||||
RequestTokenError::ServerResponse,
|
RequestTokenError::ServerResponse,
|
||||||
Scope, TokenResponse, UserInfoClaims,
|
Scope, TokenResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::MiddlewareError,
|
error::{Error, MiddlewareError},
|
||||||
extractor::{OidcAccessToken, OidcClaims, OidcRpInitiatedLogout, OidcUserInfo},
|
extractor::{OidcAccessToken, OidcClaims},
|
||||||
AdditionalClaims, AuthenticatedSession, BoxError, ClearSessionFlag, IdToken, OidcClient,
|
AdditionalClaims, BoxError, OidcClient, OidcQuery, OidcSession, SESSION_KEY,
|
||||||
OidcSession, SESSION_KEY,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Layer for the [`OidcLoginMiddleware`].
|
/// Layer for the [OidcLoginMiddleware].
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct OidcLoginLayer<AC>
|
pub struct OidcLoginLayer<AC>
|
||||||
where
|
where
|
||||||
|
|
@ -61,7 +61,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This middleware forces the user to be authenticated and redirects the user to the OpenID Connect
|
/// This middleware forces the user to be authenticated and redirects the user to the OpenID Connect
|
||||||
/// Issuer to authenticate. This Middleware needs to be loaded afer [`OidcAuthMiddleware`].
|
/// Issuer to authenticate. This Middleware needs to be loaded afer [OidcAuthMiddleware].
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct OidcLoginMiddleware<I, AC>
|
pub struct OidcLoginMiddleware<I, AC>
|
||||||
where
|
where
|
||||||
|
|
@ -105,68 +105,118 @@ where
|
||||||
} else {
|
} else {
|
||||||
// no valid id token or refresh token was found and the user has to login
|
// no valid id token or refresh token was found and the user has to login
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let (parts, _) = request.into_parts();
|
let (mut parts, _) = request.into_parts();
|
||||||
|
|
||||||
let oidcclient: OidcClient<AC> = parts
|
let mut oidcclient: OidcClient<AC> = parts
|
||||||
.extensions
|
.extensions
|
||||||
.get()
|
.get()
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or(MiddlewareError::AuthMiddlewareNotFound)?;
|
.ok_or(MiddlewareError::AuthMiddlewareNotFound)?;
|
||||||
|
|
||||||
|
let query = Query::<OidcQuery>::from_request_parts(&mut parts, &())
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
let session = parts
|
let session = parts
|
||||||
.extensions
|
.extensions
|
||||||
.get::<Session>()
|
.get::<Session>()
|
||||||
.ok_or(MiddlewareError::SessionNotFound)?;
|
.ok_or(MiddlewareError::SessionNotFound)?;
|
||||||
|
let login_session: Option<OidcSession> = session
|
||||||
|
.get(SESSION_KEY)
|
||||||
|
.await
|
||||||
|
.map_err(MiddlewareError::from)?;
|
||||||
|
|
||||||
let redirect_url = parts
|
let handler_uri =
|
||||||
.extensions
|
strip_oidc_from_path(oidcclient.application_base_url.clone(), &parts.uri)?;
|
||||||
.get::<OriginalUri>()
|
|
||||||
.ok_or(MiddlewareError::OriginalUrlNotFound)?;
|
|
||||||
|
|
||||||
let redirect_url = if let Some(query) = redirect_url.query() {
|
oidcclient.client = oidcclient
|
||||||
redirect_url.path().to_string() + "?" + query
|
.client
|
||||||
|
.set_redirect_uri(RedirectUrl::new(handler_uri.to_string())?);
|
||||||
|
|
||||||
|
if let (Some(mut login_session), Some(query)) = (login_session, query) {
|
||||||
|
// the request has the request headers of the oidc redirect
|
||||||
|
// parse the headers and exchange the code for a valid token
|
||||||
|
|
||||||
|
if login_session.csrf_token.secret() != &query.state {
|
||||||
|
return Err(MiddlewareError::CsrfTokenInvalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_response = oidcclient
|
||||||
|
.client
|
||||||
|
.exchange_code(AuthorizationCode::new(query.code.to_string()))
|
||||||
|
// Set the PKCE code verifier.
|
||||||
|
.set_pkce_verifier(PkceCodeVerifier::new(
|
||||||
|
login_session.pkce_verifier.secret().to_string(),
|
||||||
|
))
|
||||||
|
.request_async(async_http_client)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Extract the ID token claims after verifying its authenticity and nonce.
|
||||||
|
let id_token = token_response
|
||||||
|
.id_token()
|
||||||
|
.ok_or(MiddlewareError::IdTokenMissing)?;
|
||||||
|
let claims = id_token
|
||||||
|
.claims(&oidcclient.client.id_token_verifier(), &login_session.nonce)?;
|
||||||
|
|
||||||
|
// Verify the access token hash to ensure that the access token hasn't been substituted for
|
||||||
|
// another user's.
|
||||||
|
if let Some(expected_access_token_hash) = claims.access_token_hash() {
|
||||||
|
let actual_access_token_hash = AccessTokenHash::from_token(
|
||||||
|
token_response.access_token(),
|
||||||
|
&id_token.signing_alg()?,
|
||||||
|
)?;
|
||||||
|
if actual_access_token_hash != *expected_access_token_hash {
|
||||||
|
return Err(MiddlewareError::AccessTokenHashInvalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
login_session.id_token = Some(id_token.to_string());
|
||||||
|
login_session.access_token =
|
||||||
|
Some(token_response.access_token().secret().to_string());
|
||||||
|
login_session.refresh_token = token_response
|
||||||
|
.refresh_token()
|
||||||
|
.map(|x| x.secret().to_string());
|
||||||
|
|
||||||
|
session.insert(SESSION_KEY, login_session).await.unwrap();
|
||||||
|
|
||||||
|
Ok(Redirect::temporary(&handler_uri.to_string()).into_response())
|
||||||
} else {
|
} else {
|
||||||
redirect_url.path().to_string()
|
// generate a login url and redirect the user to it
|
||||||
};
|
|
||||||
// generate a login url and redirect the user to it
|
|
||||||
|
|
||||||
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||||
let (auth_url, csrf_token, nonce) = {
|
let (auth_url, csrf_token, nonce) = {
|
||||||
let mut auth = oidcclient.client.authorize_url(
|
let mut auth = oidcclient.client.authorize_url(
|
||||||
CoreAuthenticationFlow::AuthorizationCode,
|
CoreAuthenticationFlow::AuthorizationCode,
|
||||||
CsrfToken::new_random,
|
CsrfToken::new_random,
|
||||||
Nonce::new_random,
|
Nonce::new_random,
|
||||||
);
|
);
|
||||||
|
|
||||||
for scope in oidcclient.scopes.iter() {
|
for scope in oidcclient.scopes.iter() {
|
||||||
auth = auth.add_scope(Scope::new(scope.to_string()));
|
auth = auth.add_scope(Scope::new(scope.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(acr) = oidcclient.auth_context_class {
|
auth.set_pkce_challenge(pkce_challenge).url()
|
||||||
auth = auth.add_auth_context_value(acr);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
auth.set_pkce_challenge(pkce_challenge).url()
|
let oidc_session = OidcSession {
|
||||||
};
|
nonce,
|
||||||
|
csrf_token,
|
||||||
|
pkce_verifier,
|
||||||
|
id_token: None,
|
||||||
|
access_token: None,
|
||||||
|
refresh_token: None,
|
||||||
|
};
|
||||||
|
|
||||||
let oidc_session = OidcSession::<AC> {
|
session.insert(SESSION_KEY, oidc_session).await.unwrap();
|
||||||
nonce,
|
|
||||||
csrf_token,
|
|
||||||
pkce_verifier,
|
|
||||||
authenticated: None,
|
|
||||||
refresh_token: None,
|
|
||||||
redirect_url: redirect_url.into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
session.insert(SESSION_KEY, oidc_session).await?;
|
Ok(Redirect::temporary(auth_url.as_str()).into_response())
|
||||||
|
}
|
||||||
Ok(Redirect::to(auth_url.as_str()).into_response())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layer for the [`OidcAuthMiddleware`].
|
/// Layer for the [OidcAuthMiddleware].
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct OidcAuthLayer<AC>
|
pub struct OidcAuthLayer<AC>
|
||||||
where
|
where
|
||||||
|
|
@ -179,10 +229,24 @@ impl<AC: AdditionalClaims> OidcAuthLayer<AC> {
|
||||||
pub fn new(client: OidcClient<AC>) -> Self {
|
pub fn new(client: OidcClient<AC>) -> Self {
|
||||||
Self { client }
|
Self { client }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
impl<AC: AdditionalClaims> From<OidcClient<AC>> for OidcAuthLayer<AC> {
|
pub async fn discover_client(
|
||||||
fn from(value: OidcClient<AC>) -> Self {
|
application_base_url: Uri,
|
||||||
Self::new(value)
|
issuer: String,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: Option<String>,
|
||||||
|
scopes: Vec<String>,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
client: OidcClient::<AC>::discover_new(
|
||||||
|
application_base_url,
|
||||||
|
issuer,
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
scopes,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,77 +299,109 @@ where
|
||||||
fn call(&mut self, request: Request<B>) -> Self::Future {
|
fn call(&mut self, request: Request<B>) -> Self::Future {
|
||||||
let inner = self.inner.clone();
|
let inner = self.inner.clone();
|
||||||
let mut inner = std::mem::replace(&mut self.inner, inner);
|
let mut inner = std::mem::replace(&mut self.inner, inner);
|
||||||
let oidcclient = self.client.clone();
|
let mut oidcclient = self.client.clone();
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let (mut parts, body) = request.into_parts();
|
let (mut parts, body) = request.into_parts();
|
||||||
|
|
||||||
let session = parts
|
let session = parts
|
||||||
.extensions
|
.extensions
|
||||||
.get::<Session>()
|
.get::<Session>()
|
||||||
.ok_or(MiddlewareError::SessionNotFound)?
|
.ok_or(MiddlewareError::SessionNotFound)?;
|
||||||
.clone();
|
let mut login_session: Option<OidcSession> = session
|
||||||
let mut login_session: Option<OidcSession<AC>> = session
|
|
||||||
.get(SESSION_KEY)
|
.get(SESSION_KEY)
|
||||||
.await
|
.await
|
||||||
.map_err(MiddlewareError::from)?;
|
.map_err(MiddlewareError::from)?;
|
||||||
|
|
||||||
|
let handler_uri =
|
||||||
|
strip_oidc_from_path(oidcclient.application_base_url.clone(), &parts.uri)?;
|
||||||
|
|
||||||
|
oidcclient.client = oidcclient
|
||||||
|
.client
|
||||||
|
.set_redirect_uri(RedirectUrl::new(handler_uri.to_string())?);
|
||||||
|
|
||||||
if let Some(login_session) = &mut login_session {
|
if let Some(login_session) = &mut login_session {
|
||||||
let id_token_claims = login_session.authenticated.as_ref().and_then(|session| {
|
let id_token_claims = login_session.id_token::<AC>().and_then(|id_token| {
|
||||||
session
|
id_token
|
||||||
.id_token
|
.claims(&oidcclient.client.id_token_verifier(), &login_session.nonce)
|
||||||
.claims(
|
|
||||||
&oidcclient
|
|
||||||
.client
|
|
||||||
.id_token_verifier()
|
|
||||||
.set_other_audience_verifier_fn(|audience| {
|
|
||||||
// Return false (reject) if audience is in list of untrusted audiences
|
|
||||||
!oidcclient.untrusted_audiences.contains(audience)
|
|
||||||
}),
|
|
||||||
&login_session.nonce,
|
|
||||||
)
|
|
||||||
.ok()
|
.ok()
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|claims| (session, claims))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some((session, claims)) = id_token_claims {
|
match (id_token_claims, login_session.refresh_token()) {
|
||||||
let user_claims =
|
|
||||||
get_user_claims(&oidcclient, session.access_token.clone()).await?;
|
|
||||||
// stored id token is valid and can be used
|
// stored id token is valid and can be used
|
||||||
insert_extensions(
|
(Some(claims), _) => {
|
||||||
&mut parts,
|
parts.extensions.insert(OidcClaims(claims));
|
||||||
claims.clone(),
|
parts.extensions.insert(OidcAccessToken(
|
||||||
user_claims,
|
login_session.access_token.clone().unwrap_or_default(),
|
||||||
&oidcclient,
|
));
|
||||||
session,
|
}
|
||||||
);
|
// stored id token is invalid and can't be uses, but we have a refresh token
|
||||||
} else if let Some(refresh_token) = login_session.refresh_token.as_ref() {
|
// and can use it and try to get another id token.
|
||||||
// session is expired but can be refreshed using the refresh_token
|
(_, Some(refresh_token)) => {
|
||||||
if let Some((claims, user_claims, authenticated_session, refresh_token)) =
|
let mut refresh_request =
|
||||||
try_refresh_token(&oidcclient, refresh_token, &login_session.nonce).await?
|
oidcclient.client.exchange_refresh_token(&refresh_token);
|
||||||
{
|
|
||||||
insert_extensions(
|
|
||||||
&mut parts,
|
|
||||||
claims,
|
|
||||||
user_claims.clone(),
|
|
||||||
&oidcclient,
|
|
||||||
&authenticated_session,
|
|
||||||
);
|
|
||||||
login_session.authenticated = Some(authenticated_session);
|
|
||||||
|
|
||||||
if let Some(refresh_token) = refresh_token {
|
for scope in oidcclient.scopes.iter() {
|
||||||
login_session.refresh_token = Some(refresh_token);
|
refresh_request =
|
||||||
|
refresh_request.add_scope(Scope::new(scope.to_string()));
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// save refreshed session or delete it when the token couldn't be refreshed
|
match refresh_request.request_async(async_http_client).await {
|
||||||
let session = parts
|
Ok(token_response) => {
|
||||||
.extensions
|
// Extract the ID token claims after verifying its authenticity and nonce.
|
||||||
.get::<Session>()
|
let id_token = token_response
|
||||||
.ok_or(MiddlewareError::SessionNotFound)?;
|
.id_token()
|
||||||
|
.ok_or(MiddlewareError::IdTokenMissing)?;
|
||||||
|
let claims = id_token.claims(
|
||||||
|
&oidcclient.client.id_token_verifier(),
|
||||||
|
&login_session.nonce,
|
||||||
|
)?;
|
||||||
|
|
||||||
session.insert(SESSION_KEY, login_session).await?;
|
// Verify the access token hash to ensure that the access token hasn't been substituted for
|
||||||
|
// another user's.
|
||||||
|
if let Some(expected_access_token_hash) = claims.access_token_hash()
|
||||||
|
{
|
||||||
|
let actual_access_token_hash = AccessTokenHash::from_token(
|
||||||
|
token_response.access_token(),
|
||||||
|
&id_token.signing_alg()?,
|
||||||
|
)?;
|
||||||
|
if actual_access_token_hash != *expected_access_token_hash {
|
||||||
|
return Err(MiddlewareError::AccessTokenHashInvalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
login_session.id_token = Some(id_token.to_string());
|
||||||
|
login_session.access_token =
|
||||||
|
Some(token_response.access_token().secret().to_string());
|
||||||
|
login_session.refresh_token = token_response
|
||||||
|
.refresh_token()
|
||||||
|
.map(|x| x.secret().to_string());
|
||||||
|
|
||||||
|
parts.extensions.insert(OidcClaims(claims.clone()));
|
||||||
|
parts.extensions.insert(OidcAccessToken(
|
||||||
|
login_session.access_token.clone().unwrap_or_default(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(ServerResponse(e))
|
||||||
|
if *e.error() == CoreErrorResponseType::InvalidGrant =>
|
||||||
|
{
|
||||||
|
// Refresh failed, refresh_token most likely expired or
|
||||||
|
// invalid, the session can be considered lost
|
||||||
|
login_session.refresh_token = None;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let session = parts
|
||||||
|
.extensions
|
||||||
|
.get::<Session>()
|
||||||
|
.ok_or(MiddlewareError::SessionNotFound)?;
|
||||||
|
|
||||||
|
session.insert(SESSION_KEY, login_session).await.unwrap();
|
||||||
|
}
|
||||||
|
(None, None) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,148 +413,38 @@ where
|
||||||
.await
|
.await
|
||||||
.map_err(|e| MiddlewareError::NextMiddleware(e.into()))?
|
.map_err(|e| MiddlewareError::NextMiddleware(e.into()))?
|
||||||
.into_response();
|
.into_response();
|
||||||
|
|
||||||
let has_logout_ext = response.extensions().get::<ClearSessionFlag>().is_some();
|
|
||||||
if let (true, Some(mut login_session)) = (has_logout_ext, login_session) {
|
|
||||||
login_session.authenticated = None;
|
|
||||||
login_session.refresh_token = None;
|
|
||||||
session.insert(SESSION_KEY, login_session).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// insert all extensions that are used by the extractors
|
/// Helper function to remove the OpenID Connect authentication response query attributes from a
|
||||||
fn insert_extensions<AC: AdditionalClaims>(
|
/// [`Uri`].
|
||||||
parts: &mut Parts,
|
pub fn strip_oidc_from_path(base_url: Uri, uri: &Uri) -> Result<Uri, MiddlewareError> {
|
||||||
claims: IdTokenClaims<AC, CoreGenderClaim>,
|
let mut base_url = base_url.into_parts();
|
||||||
user_claims: UserInfoClaims<AC, CoreGenderClaim>,
|
|
||||||
client: &OidcClient<AC>,
|
base_url.path_and_query = uri
|
||||||
authenticated_session: &AuthenticatedSession<AC>,
|
.path_and_query()
|
||||||
) {
|
.map(|path_and_query| {
|
||||||
parts.extensions.insert(OidcClaims(claims));
|
let query = path_and_query
|
||||||
parts.extensions.insert(OidcUserInfo(user_claims));
|
.query()
|
||||||
parts.extensions.insert(OidcAccessToken(
|
.and_then(|uri| {
|
||||||
authenticated_session.access_token.secret().to_string(),
|
uri.split('&')
|
||||||
));
|
.filter(|x| {
|
||||||
let rp_initiated_logout = client
|
!x.starts_with("code")
|
||||||
.end_session_endpoint
|
&& !x.starts_with("state")
|
||||||
.as_ref()
|
&& !x.starts_with("session_state")
|
||||||
.map(|end_session_endpoint| OidcRpInitiatedLogout {
|
&& !x.starts_with("iss")
|
||||||
end_session_endpoint: end_session_endpoint.clone(),
|
})
|
||||||
id_token_hint: authenticated_session.id_token.to_string().into(),
|
.map(|x| x.to_string())
|
||||||
client_id: client.client_id.clone(),
|
.reduce(|acc, x| acc + "&" + &x)
|
||||||
post_logout_redirect_uri: None,
|
})
|
||||||
state: None,
|
.map(|x| format!("?{x}"))
|
||||||
});
|
.unwrap_or_default();
|
||||||
parts.extensions.insert(rp_initiated_logout);
|
|
||||||
}
|
PathAndQuery::from_maybe_shared(format!("{}{}", path_and_query.path(), query))
|
||||||
|
})
|
||||||
/// Verify the access token hash to ensure that the access token hasn't been substituted for
|
.transpose()?;
|
||||||
/// another user's.
|
|
||||||
/// Returns `Ok` when access token is valid
|
Ok(Uri::from_parts(base_url)?)
|
||||||
fn validate_access_token_hash<AC: AdditionalClaims>(
|
|
||||||
id_token: &IdToken<AC>,
|
|
||||||
id_token_verifier: IdTokenVerifier<CoreJsonWebKey>,
|
|
||||||
access_token: &AccessToken,
|
|
||||||
claims: &IdTokenClaims<AC, CoreGenderClaim>,
|
|
||||||
) -> Result<(), MiddlewareError> {
|
|
||||||
if let Some(expected_access_token_hash) = claims.access_token_hash() {
|
|
||||||
let actual_access_token_hash = AccessTokenHash::from_token(
|
|
||||||
access_token,
|
|
||||||
id_token.signing_alg()?,
|
|
||||||
id_token.signing_key(&id_token_verifier)?,
|
|
||||||
)?;
|
|
||||||
if actual_access_token_hash == *expected_access_token_hash {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(MiddlewareError::AccessTokenHashInvalid)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_user_claims<AC: AdditionalClaims>(
|
|
||||||
client: &OidcClient<AC>,
|
|
||||||
access_token: AccessToken,
|
|
||||||
) -> Result<UserInfoClaims<AC, CoreGenderClaim>, MiddlewareError> {
|
|
||||||
client
|
|
||||||
.client
|
|
||||||
.user_info(access_token, None)
|
|
||||||
.map_err(MiddlewareError::Configuration)?
|
|
||||||
.request_async(&client.http_client)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_refresh_token<AC: AdditionalClaims>(
|
|
||||||
client: &OidcClient<AC>,
|
|
||||||
refresh_token: &RefreshToken,
|
|
||||||
nonce: &Nonce,
|
|
||||||
) -> Result<
|
|
||||||
Option<(
|
|
||||||
IdTokenClaims<AC, CoreGenderClaim>,
|
|
||||||
UserInfoClaims<AC, CoreGenderClaim>,
|
|
||||||
AuthenticatedSession<AC>,
|
|
||||||
Option<RefreshToken>,
|
|
||||||
)>,
|
|
||||||
MiddlewareError,
|
|
||||||
> {
|
|
||||||
let mut refresh_request = client.client.exchange_refresh_token(refresh_token)?;
|
|
||||||
|
|
||||||
for scope in client.scopes.iter() {
|
|
||||||
refresh_request = refresh_request.add_scope(Scope::new(scope.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
match refresh_request.request_async(&client.http_client).await {
|
|
||||||
Ok(token_response) => {
|
|
||||||
// Extract the ID token claims after verifying its authenticity and nonce.
|
|
||||||
let id_token = token_response
|
|
||||||
.id_token()
|
|
||||||
.ok_or(MiddlewareError::IdTokenMissing)?;
|
|
||||||
let id_token_verifier = client
|
|
||||||
.client
|
|
||||||
.id_token_verifier()
|
|
||||||
.set_other_audience_verifier_fn(|audience|
|
|
||||||
// Return false (reject) if audience is in list of untrusted audiences
|
|
||||||
!client.untrusted_audiences.contains(audience));
|
|
||||||
let claims = id_token.claims(&id_token_verifier, |claims_nonce: Option<&Nonce>| {
|
|
||||||
match claims_nonce {
|
|
||||||
Some(_) => nonce.verify(claims_nonce),
|
|
||||||
None => Ok(()),
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
validate_access_token_hash(
|
|
||||||
id_token,
|
|
||||||
id_token_verifier,
|
|
||||||
token_response.access_token(),
|
|
||||||
claims,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let authenticated_session = AuthenticatedSession {
|
|
||||||
id_token: id_token.clone(),
|
|
||||||
access_token: token_response.access_token().clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let user_claims =
|
|
||||||
get_user_claims(client, authenticated_session.access_token.clone()).await?;
|
|
||||||
|
|
||||||
Ok(Some((
|
|
||||||
claims.clone(),
|
|
||||||
user_claims,
|
|
||||||
authenticated_session,
|
|
||||||
token_response.refresh_token().cloned(),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
Err(ServerResponse(e)) if *e.error() == CoreErrorResponseType::InvalidGrant => {
|
|
||||||
// Refresh failed, refresh_token most likely expired or
|
|
||||||
// invalid, the session can be considered lost
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
Err(err) => Err(err.into()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue