Storing error_chain version before trashing it
This commit is contained in:
commit
d0954586c9
|
@ -0,0 +1,4 @@
|
||||||
|
/target/
|
||||||
|
**/*.rs.bk
|
||||||
|
Cargo.lock
|
||||||
|
.vscode
|
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "oidc"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Matthew Scheirer <matt.scheirer@gmail.com>"]
|
||||||
|
categories = ["web-programming", "authentication"]
|
||||||
|
description = "OpenID Connect client library using Reqwest"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
keywords = ["sync", "authentication", "client", "reqwest",
|
||||||
|
"oauth", "openid", "openid_connect", "web"]
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
base64 = "0.6"
|
||||||
|
biscuit = { git = "https://github.com/Korvox/biscuit" }
|
||||||
|
error-chain = "0.11"
|
||||||
|
chrono = "0.4"
|
||||||
|
inth-oauth2 = "0.13"
|
||||||
|
reqwest = "0.7"
|
||||||
|
serde = "1"
|
||||||
|
serde_derive = "1"
|
||||||
|
serde_json = "1"
|
||||||
|
url = "1.5"
|
||||||
|
url_serde = "0.2"
|
||||||
|
validator = "0.6"
|
||||||
|
validator_derive = "0.6"
|
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,202 @@
|
||||||
|
use biscuit::Empty;
|
||||||
|
use biscuit::jwk::JWKSet;
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
use inth_oauth2;
|
||||||
|
use url::Url;
|
||||||
|
use url_serde;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use discovery::{self, Config, Discovered};
|
||||||
|
use error::{ErrorKind, Result};
|
||||||
|
use token::{Expiring, Token};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Params {
|
||||||
|
pub client_id: String,
|
||||||
|
pub client_secret: String,
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
pub redirect_url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional parameters that [OpenID specifies](https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters) for the auth URI.
|
||||||
|
/// Derives Default, so remember to ..Default::default() after you specify what you want.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Options {
|
||||||
|
pub nonce: Option<String>,
|
||||||
|
pub display: Option<Display>,
|
||||||
|
pub prompt: Option<HashSet<Prompt>>,
|
||||||
|
pub max_age: Option<Duration>,
|
||||||
|
pub ui_locales: Option<String>,
|
||||||
|
pub claims_locales: Option<String>,
|
||||||
|
pub id_token_hint: Option<String>,
|
||||||
|
pub login_hint: Option<String>,
|
||||||
|
pub acr_values: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Display {
|
||||||
|
Page,
|
||||||
|
Popup,
|
||||||
|
Touch,
|
||||||
|
Wap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display {
|
||||||
|
fn as_str(&self) -> &'static str {
|
||||||
|
match *self {
|
||||||
|
Display::Page => "page",
|
||||||
|
Display::Popup => "popup",
|
||||||
|
Display::Touch => "touch",
|
||||||
|
Display::Wap => "wap",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Hash)]
|
||||||
|
pub enum Prompt {
|
||||||
|
None,
|
||||||
|
Login,
|
||||||
|
Consent,
|
||||||
|
SelectAccount,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Prompt {
|
||||||
|
fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
&Prompt::None => "none",
|
||||||
|
&Prompt::Login => "login",
|
||||||
|
&Prompt::Consent => "consent",
|
||||||
|
&Prompt::SelectAccount => "select_account",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The userinfo struct contains all possible userinfo fields regardless of scope. [See spec.](https://openid.net/specs/openid-connect-basic-1_0.html#StandardClaims)
|
||||||
|
// TODO is there a way to use claims_supported in config to simplify this struct?
|
||||||
|
#[derive(Deserialize, Validate)]
|
||||||
|
pub struct Userinfo {
|
||||||
|
pub sub: String,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub given_name: Option<String>,
|
||||||
|
pub family_name: Option<String>,
|
||||||
|
pub middle_name: Option<String>,
|
||||||
|
pub nickname: Option<String>,
|
||||||
|
pub preferred_username: Option<String>,
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
pub profile: Option<Url>,
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
pub picture: Option<Url>,
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
pub website: Option<Url>,
|
||||||
|
#[validate(email)]
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub email_verified: Option<bool>,
|
||||||
|
// Isn't required to be just male or female
|
||||||
|
pub gender: Option<String>,
|
||||||
|
// ISO 9601:2004 YYYY-MM-DD or YYYY. Would be nice to serialize to chrono::Date.
|
||||||
|
pub birthdate: Option<String>,
|
||||||
|
// Region/City codes. Should also have a more concrete serializer form.
|
||||||
|
pub zoneinfo: Option<String>,
|
||||||
|
// Usually RFC5646 langcode-countrycode, maybe with a _ sep, could be arbitrary
|
||||||
|
pub locale: Option<String>,
|
||||||
|
// Usually E.164 format number
|
||||||
|
pub phone_number: Option<String>,
|
||||||
|
pub phone_number_verified: Option<bool>,
|
||||||
|
pub address: Option<Address>,
|
||||||
|
pub updated_at: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Address Claim struct. Can be only formatted, only the rest, or both.
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Address {
|
||||||
|
pub formatted: Option<String>,
|
||||||
|
pub street_address: Option<String>,
|
||||||
|
pub locality: Option<String>,
|
||||||
|
pub region: Option<String>,
|
||||||
|
// Countries like the UK use alphanumeric postal codes, so you can't just use a number here
|
||||||
|
pub postal_code: Option<String>,
|
||||||
|
pub country: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Client {
|
||||||
|
oauth: inth_oauth2::Client<Discovered>,
|
||||||
|
jwks: JWKSet<Empty>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
/// Constructs a client from an issuer url and client parameters via discovery
|
||||||
|
pub fn discover(issuer: &Url, params: Params) -> Result<Self> {
|
||||||
|
let config = discovery::discover(issuer)?;
|
||||||
|
let jwks = discovery::jwks(&config.jwks_uri)?;
|
||||||
|
let provider = Discovered { config };
|
||||||
|
Ok(Self::new(provider, params, jwks))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a client from a given provider, key set, and parameters. Unlike ::discover(..)
|
||||||
|
/// this function does not perform any network operations.
|
||||||
|
fn new(provider: Discovered, params: Params, jwks: JWKSet<Empty>) -> Self {
|
||||||
|
Client {
|
||||||
|
oauth: inth_oauth2::Client::new(
|
||||||
|
provider,
|
||||||
|
params.client_id,
|
||||||
|
params.client_secret,
|
||||||
|
Some(params.redirect_url.into_string())),
|
||||||
|
jwks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reference to the config document of the provider obtained via discovery
|
||||||
|
pub fn config(&self) -> &Config {
|
||||||
|
&self.oauth.provider.config
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs the auth_url to redirect a client to the provider. Options are... optional. Use
|
||||||
|
/// them as needed. Keep the Options struct around for authentication, or at least the nonce
|
||||||
|
/// and max_age parameter - we need to verify they stay the same and validate if you used them.
|
||||||
|
pub fn auth_url(&self, scope: &str, state: &str, options: &Options) -> Result<Url>{
|
||||||
|
if !scope.contains("openid") {
|
||||||
|
return Err(ErrorKind::MissingOpenidScope.into())
|
||||||
|
}
|
||||||
|
let mut url = self.oauth.auth_uri(Some(&scope), Some(state))?;
|
||||||
|
{
|
||||||
|
let mut query = url.query_pairs_mut();
|
||||||
|
if let Some(ref nonce) = options.nonce {
|
||||||
|
query.append_pair("nonce", nonce.as_str());
|
||||||
|
}
|
||||||
|
if let Some(ref display) = options.display {
|
||||||
|
query.append_pair("display", display.as_str());
|
||||||
|
}
|
||||||
|
if let Some(ref prompt) = options.prompt {
|
||||||
|
let s = prompt.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" ");
|
||||||
|
query.append_pair("prompt", s.as_str());
|
||||||
|
}
|
||||||
|
if let Some(max_age) = options.max_age {
|
||||||
|
query.append_pair("max_age", max_age.num_seconds().to_string().as_str());
|
||||||
|
}
|
||||||
|
if let Some(ref ui_locales) = options.ui_locales {
|
||||||
|
query.append_pair("ui_locales", ui_locales.as_str());
|
||||||
|
}
|
||||||
|
if let Some(ref claims_locales) = options.claims_locales {
|
||||||
|
query.append_pair("claims_locales", claims_locales.as_str());
|
||||||
|
}
|
||||||
|
if let Some(ref id_token_hint) = options.id_token_hint {
|
||||||
|
query.append_pair("id_token_hint", id_token_hint.as_str());
|
||||||
|
}
|
||||||
|
if let Some(ref login_hint) = options.login_hint {
|
||||||
|
query.append_pair("login_hint", login_hint.as_str());
|
||||||
|
}
|
||||||
|
if let Some(ref acr_values) = options.acr_values {
|
||||||
|
query.append_pair("acr_values", acr_values.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given an auth_code, request the token, validate it, and if userinfo_endpoint exists
|
||||||
|
/// request that and give the response
|
||||||
|
pub fn authenticate(&self, auth_code: &str, options: &Options
|
||||||
|
) -> Result<(Token<Expiring>, Option<Userinfo>)> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
use biscuit::Empty;
|
||||||
|
use biscuit::jwk::JWKSet;
|
||||||
|
use inth_oauth2::provider::Provider;
|
||||||
|
use url::Url;
|
||||||
|
use url_serde;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
use error::{Error, ErrorKind, Result};
|
||||||
|
use token::{Expiring, Token};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct Config {
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
pub issuer: Url,
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
pub authorization_endpoint: Url,
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
// Only optional in the implicit flow
|
||||||
|
// TODO For now, we only support code flows.
|
||||||
|
pub token_endpoint: Url,
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
pub userinfo_endpoint: Option<Url>,
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
pub jwks_uri: Url,
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
pub registration_endpoint: Option<Url>,
|
||||||
|
pub scopes_supported: Option<Vec<String>>,
|
||||||
|
// There are only three valid response types, plus combinations of them, and none
|
||||||
|
// If we want to make these user friendly we want a struct to represent all 7 types
|
||||||
|
pub response_types_supported: Vec<String>,
|
||||||
|
// There are only two possible values here, query and fragment. Default is both.
|
||||||
|
pub response_modes_supported: Option<Vec<String>>,
|
||||||
|
// Must support at least authorization_code and implicit.
|
||||||
|
pub grant_types_supported: Option<Vec<String>>,
|
||||||
|
pub acr_values_supported: Option<Vec<String>>,
|
||||||
|
// pairwise and public are valid by spec, but servers can add more
|
||||||
|
pub subject_types_supported: Vec<String>,
|
||||||
|
// Must include at least RS256, none is only allowed with response types without id tokens
|
||||||
|
pub id_token_signing_alg_values_supported: Vec<String>,
|
||||||
|
pub id_token_encryption_alg_values_supported: Option<Vec<String>>,
|
||||||
|
pub id_token_encryption_enc_values_supported: Option<Vec<String>>,
|
||||||
|
pub userinfo_signing_alg_values_supported: Option<Vec<String>>,
|
||||||
|
pub userinfo_encryption_alg_values_supported: Option<Vec<String>>,
|
||||||
|
pub userinfo_encryption_enc_values_supported: Option<Vec<String>>,
|
||||||
|
pub request_object_signing_alg_values_supported: Option<Vec<String>>,
|
||||||
|
pub request_object_encryption_alg_values_supported: Option<Vec<String>>,
|
||||||
|
pub request_object_encryption_enc_values_supported: Option<Vec<String>>,
|
||||||
|
// Spec options are client_secret_post, client_secret_basic, client_secret_jwt, private_key_jwt
|
||||||
|
// If omitted, client_secret_basic is used
|
||||||
|
pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
|
||||||
|
// Only wanted with jwt auth methods, should have RS256, none not allowed
|
||||||
|
pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
|
||||||
|
pub display_values_supported: Option<Vec<String>>,
|
||||||
|
// Valid options are normal, aggregated, and distributed. If omitted, only use normal
|
||||||
|
pub claim_types_supported: Option<Vec<String>>,
|
||||||
|
pub claims_supported: Option<Vec<Claim>>,
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
pub service_documentation: Option<Url>,
|
||||||
|
pub claims_locales_supported: Option<Vec<String>>,
|
||||||
|
pub ui_locales_supported: Option<Vec<String>>,
|
||||||
|
// default false
|
||||||
|
pub claims_parameter_supported: Option<bool>,
|
||||||
|
// default false
|
||||||
|
pub request_parameter_supported: Option<bool>,
|
||||||
|
// default true
|
||||||
|
pub request_uri_parameter_supported: Option<bool>,
|
||||||
|
// default false
|
||||||
|
pub require_request_uri_registration: Option<bool>,
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
pub op_policy_uri: Option<Url>,
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
pub op_tos_uri: Option<Url>,
|
||||||
|
// This is a NONSTANDARD extension Google uses that is a part of the Oauth discovery draft
|
||||||
|
pub code_challenge_methods_supported: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub enum Claim {
|
||||||
|
Name(String),
|
||||||
|
FamilyName(String),
|
||||||
|
GivenName(String),
|
||||||
|
MiddleName(String),
|
||||||
|
Nickname(String),
|
||||||
|
PreferredUsername(String),
|
||||||
|
Profile(
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
Url
|
||||||
|
),
|
||||||
|
Picture(
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
Url
|
||||||
|
),
|
||||||
|
Website(
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
Url
|
||||||
|
),
|
||||||
|
Gender(String),
|
||||||
|
Birthdate(String),
|
||||||
|
Zoneinfo(String),
|
||||||
|
Locale(String),
|
||||||
|
UpdatedAt(u64),
|
||||||
|
Email(Email),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Validate)]
|
||||||
|
pub struct Email {
|
||||||
|
#[validate(email)]
|
||||||
|
pub address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Discovered {
|
||||||
|
pub config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Provider for Discovered {
|
||||||
|
type Lifetime = Expiring;
|
||||||
|
type Token = Token<Expiring>;
|
||||||
|
fn auth_uri(&self) -> &str {
|
||||||
|
self.config.authorization_endpoint.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_uri(&self) -> &str {
|
||||||
|
self.config.token_endpoint.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn discover(issuer: &Url) -> Result<Config> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn jwks(url: &Url) -> Result<JWKSet<Empty>> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
use biscuit;
|
||||||
|
use inth_oauth2;
|
||||||
|
|
||||||
|
pub enum Decode {
|
||||||
|
MissingKid,
|
||||||
|
MissingKey,
|
||||||
|
EmptySet,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Validation {
|
||||||
|
Mismatch(Mismatch),
|
||||||
|
Missing(Missing),
|
||||||
|
Expired(Expiry),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Mismatch {
|
||||||
|
Audience,
|
||||||
|
Authorized,
|
||||||
|
Issuer,
|
||||||
|
Nonce,
|
||||||
|
Subject,
|
||||||
|
}
|
||||||
|
pub enum Missing {
|
||||||
|
AuthorizedParty,
|
||||||
|
AuthTime,
|
||||||
|
Nonce,
|
||||||
|
OpenidScope,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Expiry {
|
||||||
|
Expires,
|
||||||
|
IssuedAt,
|
||||||
|
MaxAge,
|
||||||
|
}
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Oauth(inth_oauth2::ClientError);
|
||||||
|
Biscuit(biscuit::errors::Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
errors {
|
||||||
|
MissingOpenidScope
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
// TODO these should all be const, or even better, static Urls...
|
||||||
|
|
||||||
|
pub fn google() -> Url {
|
||||||
|
Url::parse("https://accounts.google.com").unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paypal() -> Url {
|
||||||
|
Url::parse("https://www.paypalobjects.com/").unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn salesforce() -> Url {
|
||||||
|
Url::parse("http://login.salesforce.com").unwrap()
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
extern crate base64;
|
||||||
|
extern crate biscuit;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate error_chain;
|
||||||
|
extern crate chrono;
|
||||||
|
extern crate inth_oauth2;
|
||||||
|
extern crate reqwest;
|
||||||
|
extern crate serde;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate serde_derive;
|
||||||
|
extern crate serde_json;
|
||||||
|
extern crate url;
|
||||||
|
extern crate url_serde;
|
||||||
|
extern crate validator;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate validator_derive;
|
||||||
|
|
||||||
|
pub mod client;
|
||||||
|
pub mod discovery;
|
||||||
|
pub mod error;
|
||||||
|
pub mod token;
|
|
@ -0,0 +1,104 @@
|
||||||
|
use base64;
|
||||||
|
use biscuit::{CompactJson, Empty, SingleOrMultiple};
|
||||||
|
use biscuit::jws::Compact;
|
||||||
|
use inth_oauth2::client::response::{FromResponse, ParseError};
|
||||||
|
use inth_oauth2::token::{self, Bearer, Lifetime};
|
||||||
|
use serde_json::Value;
|
||||||
|
use url::Url;
|
||||||
|
use url_serde;
|
||||||
|
|
||||||
|
/// Rexported lifetime token types from oauth
|
||||||
|
pub use inth_oauth2::token::{Expiring, Refresh, Static};
|
||||||
|
|
||||||
|
type IdToken = Compact<Claims, Empty>;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Claims {
|
||||||
|
#[serde(with = "url_serde")]
|
||||||
|
pub iss: Url,
|
||||||
|
// Max 255 ASCII chars
|
||||||
|
// Can't deserialize a [u8; 255]
|
||||||
|
pub sub: String,
|
||||||
|
// Either an array of audiences, or just the client_id
|
||||||
|
pub aud: SingleOrMultiple<String>,
|
||||||
|
// Not perfectly accurate for what time values we can get back...
|
||||||
|
// By spec, this is an arbitrarilly large number. In practice, an
|
||||||
|
// i64 unix time is up to 293 billion years from 1970.
|
||||||
|
//
|
||||||
|
// Make sure this cannot silently underflow, see:
|
||||||
|
// https://github.com/serde-rs/json/blob/8e01f44f479b3ea96b299efc0da9131e7aff35dc/src/de.rs#L341
|
||||||
|
pub exp: i64,
|
||||||
|
pub iat: i64,
|
||||||
|
// required for max_age request
|
||||||
|
pub auth_time: Option<i64>,
|
||||||
|
pub nonce: Option<String>,
|
||||||
|
// base64 encoded, need to decode it!
|
||||||
|
at_hash: Option<String>,
|
||||||
|
pub acr: Option<String>,
|
||||||
|
pub amr: Option<Vec<String>>,
|
||||||
|
// If exists, must be client_id
|
||||||
|
pub azp: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Claims {
|
||||||
|
/// Decodes at_hash. Returns None if it doesn't exist or something goes wrong.
|
||||||
|
///
|
||||||
|
/// See [spec 3.1.3.6](https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken)
|
||||||
|
///
|
||||||
|
/// The returned Vec is the first 128 bits of the access token hash using alg's hash alg
|
||||||
|
pub fn at_hash(&self) -> Option<Vec<u8>> {
|
||||||
|
if let Some(ref hash) = self.at_hash {
|
||||||
|
return base64::decode_config(hash.as_str(), base64::URL_SAFE).ok();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS IS CRAZY VOODOO WITCHCRAFT MAGIC
|
||||||
|
impl CompactJson for Claims {}
|
||||||
|
|
||||||
|
/// An OpenID Connect token. This is the only token allowed by spec.
|
||||||
|
/// Has an access_token for bearer, and the id_token for authentication.
|
||||||
|
/// Wraps an oauth bearer token.
|
||||||
|
pub struct Token<L: Lifetime> {
|
||||||
|
bearer: Bearer<L>,
|
||||||
|
pub id_token: IdToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L: Lifetime> Token<L> {
|
||||||
|
// Takes a json response object and parses out the id token
|
||||||
|
// TODO Support extracting a jwe token according to spec. Right now we only support jws tokens.
|
||||||
|
fn id_token(json: &Value) -> Result<IdToken, ParseError> {
|
||||||
|
let obj = json.as_object().ok_or(ParseError::ExpectedType("object"))?;
|
||||||
|
let token = obj.get("id_token").and_then(Value::as_str).ok_or(
|
||||||
|
ParseError::ExpectedFieldType("id_token", "string"),
|
||||||
|
)?;
|
||||||
|
Ok(Compact::new_encoded(token))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L: Lifetime> token::Token<L> for Token<L> {
|
||||||
|
fn access_token(&self) -> &str {
|
||||||
|
self.bearer.access_token()
|
||||||
|
}
|
||||||
|
fn scope(&self) -> Option<&str> {
|
||||||
|
self.bearer.scope()
|
||||||
|
}
|
||||||
|
fn lifetime(&self) -> &L {
|
||||||
|
self.bearer.lifetime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L: Lifetime> FromResponse for Token<L> {
|
||||||
|
fn from_response(json: &Value) -> Result<Self, ParseError> {
|
||||||
|
let bearer = Bearer::from_response(json)?;
|
||||||
|
let id_token = Self::id_token(json)?;
|
||||||
|
Ok(Self { bearer, id_token })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_response_inherit(json: &Value, prev: &Self) -> Result<Self, ParseError> {
|
||||||
|
let bearer = Bearer::from_response_inherit(json, &prev.bearer)?;
|
||||||
|
let id_token = Self::id_token(json)?;
|
||||||
|
Ok(Self { bearer, id_token })
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue