From d0954586c9d021c50c0c6407d7becd87af76d193 Mon Sep 17 00:00:00 2001 From: Matthew Scheirer Date: Sat, 16 Sep 2017 14:44:55 -0400 Subject: [PATCH] Storing error_chain version before trashing it --- .gitignore | 4 + Cargo.toml | 25 ++++++ LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++ src/client.rs | 202 +++++++++++++++++++++++++++++++++++++++++++++++ src/discovery.rs | 133 +++++++++++++++++++++++++++++++ src/error.rs | 45 +++++++++++ src/issuer.rs | 15 ++++ src/lib.rs | 21 +++++ src/token.rs | 104 ++++++++++++++++++++++++ 9 files changed, 750 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 src/client.rs create mode 100644 src/discovery.rs create mode 100644 src/error.rs create mode 100644 src/issuer.rs create mode 100644 src/lib.rs create mode 100644 src/token.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..360b42d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target/ +**/*.rs.bk +Cargo.lock +.vscode diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a41088f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "oidc" +version = "0.1.0" +authors = ["Matthew Scheirer "] +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" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/LICENSE @@ -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. diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..4d7ac5d --- /dev/null +++ b/src/client.rs @@ -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, + pub display: Option, + pub prompt: Option>, + pub max_age: Option, + pub ui_locales: Option, + pub claims_locales: Option, + pub id_token_hint: Option, + pub login_hint: Option, + pub acr_values: Option, +} + +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, + pub given_name: Option, + pub family_name: Option, + pub middle_name: Option, + pub nickname: Option, + pub preferred_username: Option, + #[serde(with = "url_serde")] + pub profile: Option, + #[serde(with = "url_serde")] + pub picture: Option, + #[serde(with = "url_serde")] + pub website: Option, + #[validate(email)] + pub email: Option, + pub email_verified: Option, + // Isn't required to be just male or female + pub gender: Option, + // ISO 9601:2004 YYYY-MM-DD or YYYY. Would be nice to serialize to chrono::Date. + pub birthdate: Option, + // Region/City codes. Should also have a more concrete serializer form. + pub zoneinfo: Option, + // Usually RFC5646 langcode-countrycode, maybe with a _ sep, could be arbitrary + pub locale: Option, + // Usually E.164 format number + pub phone_number: Option, + pub phone_number_verified: Option, + pub address: Option
, + pub updated_at: Option, +} + +/// Address Claim struct. Can be only formatted, only the rest, or both. +#[derive(Deserialize)] +pub struct Address { + pub formatted: Option, + pub street_address: Option, + pub locality: Option, + pub region: Option, + // Countries like the UK use alphanumeric postal codes, so you can't just use a number here + pub postal_code: Option, + pub country: Option, +} + +pub struct Client { + oauth: inth_oauth2::Client, + jwks: JWKSet, +} + +impl Client { + /// Constructs a client from an issuer url and client parameters via discovery + pub fn discover(issuer: &Url, params: Params) -> Result { + 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) -> 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{ + 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::>().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, Option)> { + unimplemented!() + } +} \ No newline at end of file diff --git a/src/discovery.rs b/src/discovery.rs new file mode 100644 index 0000000..dcf433b --- /dev/null +++ b/src/discovery.rs @@ -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, + #[serde(with = "url_serde")] + pub jwks_uri: Url, + #[serde(with = "url_serde")] + pub registration_endpoint: Option, + pub scopes_supported: Option>, + // 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, + // There are only two possible values here, query and fragment. Default is both. + pub response_modes_supported: Option>, + // Must support at least authorization_code and implicit. + pub grant_types_supported: Option>, + pub acr_values_supported: Option>, + // pairwise and public are valid by spec, but servers can add more + pub subject_types_supported: Vec, + // Must include at least RS256, none is only allowed with response types without id tokens + pub id_token_signing_alg_values_supported: Vec, + pub id_token_encryption_alg_values_supported: Option>, + pub id_token_encryption_enc_values_supported: Option>, + pub userinfo_signing_alg_values_supported: Option>, + pub userinfo_encryption_alg_values_supported: Option>, + pub userinfo_encryption_enc_values_supported: Option>, + pub request_object_signing_alg_values_supported: Option>, + pub request_object_encryption_alg_values_supported: Option>, + pub request_object_encryption_enc_values_supported: Option>, + // 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>, + // Only wanted with jwt auth methods, should have RS256, none not allowed + pub token_endpoint_auth_signing_alg_values_supported: Option>, + pub display_values_supported: Option>, + // Valid options are normal, aggregated, and distributed. If omitted, only use normal + pub claim_types_supported: Option>, + pub claims_supported: Option>, + #[serde(with = "url_serde")] + pub service_documentation: Option, + pub claims_locales_supported: Option>, + pub ui_locales_supported: Option>, + // default false + pub claims_parameter_supported: Option, + // default false + pub request_parameter_supported: Option, + // default true + pub request_uri_parameter_supported: Option, + // default false + pub require_request_uri_registration: Option, + #[serde(with = "url_serde")] + pub op_policy_uri: Option, + #[serde(with = "url_serde")] + pub op_tos_uri: Option, + // This is a NONSTANDARD extension Google uses that is a part of the Oauth discovery draft + pub code_challenge_methods_supported: Option>, +} + +#[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; + 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 { + unimplemented!() +} + +pub fn jwks(url: &Url) -> Result> { + unimplemented!() +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..b473e1c --- /dev/null +++ b/src/error.rs @@ -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 + } +} \ No newline at end of file diff --git a/src/issuer.rs b/src/issuer.rs new file mode 100644 index 0000000..58ffa2e --- /dev/null +++ b/src/issuer.rs @@ -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() +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..05164e1 --- /dev/null +++ b/src/lib.rs @@ -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; \ No newline at end of file diff --git a/src/token.rs b/src/token.rs new file mode 100644 index 0000000..e2ab00f --- /dev/null +++ b/src/token.rs @@ -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; + +#[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, + // 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, + pub nonce: Option, + // base64 encoded, need to decode it! + at_hash: Option, + pub acr: Option, + pub amr: Option>, + // If exists, must be client_id + pub azp: Option, +} + +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> { + 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 { + bearer: Bearer, + pub id_token: IdToken, +} + +impl Token { + // 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 { + 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 token::Token for Token { + 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 FromResponse for Token { + fn from_response(json: &Value) -> Result { + 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 { + let bearer = Bearer::from_response_inherit(json, &prev.bearer)?; + let id_token = Self::id_token(json)?; + Ok(Self { bearer, id_token }) + } +}