diff --git a/src/discovery.rs b/src/discovery.rs index 23a489a..9e5cc0a 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -4,7 +4,6 @@ use inth_oauth2::provider::Provider; use inth_oauth2::token::Expiring; use reqwest::{Client, Url}; use url_serde; -use validator::Validate; use error::Error; use token::Token; @@ -17,104 +16,63 @@ pub(crate) fn secure(url: &Url) -> Result<(), Error> { } } -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Config { - #[serde(with = "url_serde")] - pub issuer: Url, - #[serde(with = "url_serde")] - pub authorization_endpoint: Url, - #[serde(with = "url_serde")] + #[serde(with = "url_serde")] pub issuer: Url, + #[serde(with = "url_serde")] pub authorization_endpoint: Url, // 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>, + #[serde(with = "url_serde")] pub token_endpoint: Url, + #[serde(default)] #[serde(with = "url_serde")] pub userinfo_endpoint: Option, + #[serde(with = "url_serde")] pub jwks_uri: Url, + #[serde(default)] #[serde(with = "url_serde")] pub registration_endpoint: Option, + #[serde(default)] 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>, + #[serde(default)] pub response_modes_supported: Option>, // Must support at least authorization_code and implicit. - pub grant_types_supported: Option>, - pub acr_values_supported: Option>, + #[serde(default)] pub grant_types_supported: Option>, + #[serde(default)] 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>, + #[serde(default)] pub id_token_encryption_alg_values_supported: Option>, + #[serde(default)] pub id_token_encryption_enc_values_supported: Option>, + #[serde(default)] pub userinfo_signing_alg_values_supported: Option>, + #[serde(default)] pub userinfo_encryption_alg_values_supported: Option>, + #[serde(default)] pub userinfo_encryption_enc_values_supported: Option>, + #[serde(default)] pub request_object_signing_alg_values_supported: Option>, + #[serde(default)] pub request_object_encryption_alg_values_supported: Option>, + #[serde(default)] 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>, + #[serde(default)] 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>, + #[serde(default)] pub token_endpoint_auth_signing_alg_values_supported: Option>, + #[serde(default)] 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, + #[serde(default)] pub claim_types_supported: Option>, + #[serde(default)] pub claims_supported: Option>, + #[serde(default)] #[serde(with = "url_serde")] pub service_documentation: Option, + #[serde(default)] pub claims_locales_supported: Option>, + #[serde(default)] pub ui_locales_supported: Option>, + #[serde(default)] pub claims_parameter_supported: bool, + #[serde(default)] pub request_parameter_supported: bool, + #[serde(default = "tru")] pub request_uri_parameter_supported: bool, + #[serde(default)] pub require_request_uri_registration: bool, + + #[serde(default)] #[serde(with = "url_serde")] pub op_policy_uri: Option, + #[serde(default)] #[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>, + #[serde(default)] 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, +// This seems really dumb... +fn tru() -> bool { + true } pub struct Discovered { @@ -137,7 +95,8 @@ impl Provider for Discovered { /// or an Insecure if the Url isn't https. pub fn discover(client: &Client, issuer: Url) -> Result { secure(&issuer)?; - let mut resp = client.get(issuer)?.send()?; + let url = issuer.join("/.well-known/openid-configuration")?; + let mut resp = client.get(url)?.send()?; resp.json().map_err(Error::from) } @@ -148,3 +107,40 @@ pub fn jwks(client: &Client, url: Url) -> Result, Error> { let mut resp = client.get(url)?.send()?; resp.json().map_err(Error::from) } + +#[test] +fn config_google() { + let cfg = r#"{ "issuer": "https://accounts.google.com", + "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "token_endpoint": "https://www.googleapis.com/oauth2/v4/token", + "userinfo_endpoint": "https://www.googleapis.com/oauth2/v3/userinfo", + "revocation_endpoint": "https://accounts.google.com/o/oauth2/revoke", + "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs", + "response_types_supported": [ "code", "token", "id_token", "code token", + "code id_token", "token id_token", + "code token id_token", "none" ], + "subject_types_supported": [ "public" ], + "id_token_signing_alg_values_supported": [ "RS256" ], + "scopes_supported": [ "openid", "email", "profile" ], + "token_endpoint_auth_methods_supported": [ "client_secret_post", + "client_secret_basic" ], + "claims_supported": [ "aud", "email", "email_verified", "exp", "family_name", + "given_name", "iat", "iss", "locale", "name", "picture", + "sub" ], + "code_challenge_methods_supported": [ "plain", "S256" ] + }"#; + println!("{:?}", ::serde_json::from_str::(cfg).unwrap()); +} + +#[test] +fn config_minimum() { + let cfg = r#"{ "issuer": "https://example.com", + "authorization_endpoint": "https://example.com/auth", + "token_endpoint": "https://example.com/token", + "jwks_uri": "https://example.com/certs", + "response_types_supported": [ "code" ], + "subject_types_supported": [ "public" ], + "id_token_signing_alg_values_supported": [ "RS256" ] + }"#; + println!("{:?}", ::serde_json::from_str::(cfg).unwrap()); +} diff --git a/src/error.rs b/src/error.rs index 00be9cc..4b1686c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,6 +2,7 @@ pub use biscuit::errors::Error as Jose; pub use serde_json::Error as Json; pub use inth_oauth2::ClientError as Oauth; pub use reqwest::Error as Reqwest; +pub use reqwest::UrlError as Url; use std::fmt::{Display, Formatter, Result}; use std::error::Error as ErrorTrait; @@ -22,6 +23,7 @@ pub enum Error { Json(Json), Oauth(Oauth), Reqwest(Reqwest), + Url(Url), Decode(Decode), Validation(Validation), Userinfo(Userinfo), @@ -33,6 +35,7 @@ from!(Error, Jose); from!(Error, Json); from!(Error, Oauth); from!(Error, Reqwest); +from!(Error, Url); from!(Error, Decode); from!(Error, Validation); from!(Error, Userinfo); @@ -45,6 +48,7 @@ impl Display for Error { Json(ref err) => Display::fmt(err, f), Oauth(ref err) => Display::fmt(err, f), Reqwest(ref err) => Display::fmt(err, f), + Url(ref err) => Display::fmt(err, f), Decode(ref err) => Display::fmt(err, f), Validation(ref err) => Display::fmt(err, f), Userinfo(ref err) => Display::fmt(err, f), @@ -62,6 +66,7 @@ impl ErrorTrait for Error { Json(ref err) => err.description(), Oauth(ref err) => err.description(), Reqwest(ref err) => err.description(), + Url(ref err) => err.description(), Decode(ref err) => err.description(), Validation(ref err) => err.description(), Userinfo(ref err) => err.description(), @@ -77,9 +82,10 @@ impl ErrorTrait for Error { Json(ref err) => Some(err), Oauth(ref err) => Some(err), Reqwest(ref err) => Some(err), - Decode(ref err) => None, - Validation(ref err) => None, - Userinfo(ref err) => None, + Url(ref err) => Some(err), + Decode(_) => None, + Validation(_) => None, + Userinfo(_) => None, Insecure(_) => None, MissingOpenidScope => None, } @@ -95,9 +101,10 @@ pub enum Decode { impl ErrorTrait for Decode { fn description(&self) -> &str { - match self { + use Decode::*; + match *self { MissingKid => "Missing Key Id", - &Decode::MissingKey(_) => "Token key not in key set", + MissingKey(_) => "Token key not in key set", EmptySet => "JWK Set is empty", } } @@ -108,9 +115,10 @@ impl ErrorTrait for Decode { impl Display for Decode { fn fmt(&self, f: &mut Formatter) -> Result { - match self { + use Decode::*; + match *self { MissingKid => write!(f, "Token Missing Key Id when key set has multiple keys"), - &Decode::MissingKey(ref id) => + MissingKey(ref id) => write!(f, "Token wants this key id not in the key set: {}", id), EmptySet => write!(f, "JWK Set is empty!") } @@ -137,7 +145,8 @@ impl ErrorTrait for Validation { } } Missing(ref mi) => { - match mi { + use Missing::*; + match *mi { Audience => "Token missing Audience", AuthorizedParty => "Token missing AZP", AuthTime => "Token missing Auth Time", @@ -200,7 +209,8 @@ pub enum Missing { impl Display for Missing { fn fmt(&self, f: &mut Formatter) -> Result { - match self { + use Missing::*; + match *self { Audience => write!(f, "Token missing Audience"), AuthorizedParty => write!(f, "Token missing AZP"), AuthTime => write!(f, "Token missing Auth Time"), diff --git a/src/issuer.rs b/src/issuer.rs index 5850df8..5e3dc4d 100644 --- a/src/issuer.rs +++ b/src/issuer.rs @@ -13,3 +13,9 @@ pub fn paypal() -> Url { pub fn salesforce() -> Url { Url::parse("http://login.salesforce.com").unwrap() } + +#[test] +fn google_disco() { + let client = ::reqwest::Client::new().unwrap(); + let config = ::discovery::discover(&client, google()).unwrap(); +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 5d521d2..6c90546 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -425,7 +425,8 @@ pub enum Display { impl Display { fn as_str(&self) -> &'static str { - match self { + use Display::*; + match *self { Page => "page", Popup => "popup", Touch => "touch", @@ -444,8 +445,9 @@ pub enum Prompt { impl Prompt { fn as_str(&self) -> &'static str { - match self { - &Prompt::None => "none", + use Prompt::*; + match *self { + None => "none", Login => "login", Consent => "consent", SelectAccount => "select_account", @@ -456,11 +458,11 @@ impl Prompt { /// 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, + #[serde(default)] pub formatted: Option, + #[serde(default)] pub street_address: Option, + #[serde(default)] pub locality: Option, + #[serde(default)] 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, + #[serde(default)] pub postal_code: Option, + #[serde(default)] pub country: Option, } diff --git a/src/token.rs b/src/token.rs index e7bbe27..1d676f9 100644 --- a/src/token.rs +++ b/src/token.rs @@ -11,8 +11,7 @@ type IdToken = Compact; #[derive(Serialize, Deserialize)] pub struct Claims { - #[serde(with = "url_serde")] - pub iss: Url, + #[serde(with = "url_serde")] pub iss: Url, // Max 255 ASCII chars // Can't deserialize a [u8; 255] pub sub: String, @@ -27,14 +26,14 @@ pub struct Claims { pub exp: i64, pub iat: i64, // required for max_age request - pub auth_time: Option, - pub nonce: Option, + #[serde(default)] pub auth_time: Option, + #[serde(default)] pub nonce: Option, // base64 encoded, need to decode it! - at_hash: Option, - pub acr: Option, - pub amr: Option>, + #[serde(default)] at_hash: Option, + #[serde(default)] pub acr: Option, + #[serde(default)] pub amr: Option>, // If exists, must be client_id - pub azp: Option, + #[serde(default)] pub azp: Option, } impl Claims {