rustfmt 2018 formatting

This commit is contained in:
Matthew Scheirer 2018-12-17 19:01:20 -05:00
parent 58b5cce552
commit c689a92dfa
6 changed files with 277 additions and 171 deletions

View File

@ -1,11 +1,9 @@
# OpenID Connect Client & Discovery # OpenID Connect Client & Discovery
Built on [inth-oauth2](https://crates.io/crates/inth-oauth2). Using [reqwest](https://crates.io/crates/reqwest). Using [biscuit](https://crates.io/crates/biscuit) for Javascript Object Signing and Encryption (JOSE). Built on [inth-oauth2](https://crates.io/crates/inth-oauth2). Using [reqwest](https://crates.io/crates/reqwest) for the HTTP client and [biscuit](https://crates.io/crates/biscuit) for Javascript Object Signing and Encryption (JOSE).
Implements [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) and [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html). Implements [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) and [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html).
Experimental async version built on Hyper [here](https://gitlab.com/zanny/hyper-openid).
## Documentation ## Documentation
## License ## License

View File

@ -1,5 +1,5 @@
use biscuit::Empty;
use biscuit::jwk::JWKSet; use biscuit::jwk::JWKSet;
use biscuit::Empty;
use inth_oauth2::provider::Provider; use inth_oauth2::provider::Provider;
use inth_oauth2::token::Expiring; use inth_oauth2::token::Expiring;
use reqwest::{Client, Url}; use reqwest::{Client, Url};
@ -20,56 +20,94 @@ pub(crate) fn secure(url: &Url) -> Result<(), Error> {
// TODO I wish we could impl default for this, but you cannot have a config without issuer etc // TODO I wish we could impl default for this, but you cannot have a config without issuer etc
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Config { pub struct Config {
#[serde(with = "url_serde")] pub issuer: Url, #[serde(with = "url_serde")]
#[serde(with = "url_serde")] pub authorization_endpoint: Url, pub issuer: Url,
#[serde(with = "url_serde")]
pub authorization_endpoint: Url,
// Only optional in the implicit flow // Only optional in the implicit flow
// TODO For now, we only support code flows. // TODO For now, we only support code flows.
#[serde(with = "url_serde")] pub token_endpoint: Url, #[serde(with = "url_serde")]
#[serde(default)] #[serde(with = "url_serde")] pub userinfo_endpoint: Option<Url>, pub token_endpoint: Url,
#[serde(with = "url_serde")] pub jwks_uri: Url, #[serde(default)]
#[serde(default)] #[serde(with = "url_serde")] pub registration_endpoint: Option<Url>, #[serde(with = "url_serde")]
#[serde(default)] pub scopes_supported: Option<Vec<String>>, pub userinfo_endpoint: Option<Url>,
#[serde(with = "url_serde")]
pub jwks_uri: Url,
#[serde(default)]
#[serde(with = "url_serde")]
pub registration_endpoint: Option<Url>,
#[serde(default)]
pub scopes_supported: Option<Vec<String>>,
// There are only three valid response types, plus combinations of them, and none // 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 // If we want to make these user friendly we want a struct to represent all 7 types
pub response_types_supported: Vec<String>, pub response_types_supported: Vec<String>,
// There are only two possible values here, query and fragment. Default is both. // There are only two possible values here, query and fragment. Default is both.
#[serde(default)] pub response_modes_supported: Option<Vec<String>>, #[serde(default)]
pub response_modes_supported: Option<Vec<String>>,
// Must support at least authorization_code and implicit. // Must support at least authorization_code and implicit.
#[serde(default)] pub grant_types_supported: Option<Vec<String>>, #[serde(default)]
#[serde(default)] pub acr_values_supported: Option<Vec<String>>, pub grant_types_supported: Option<Vec<String>>,
#[serde(default)]
pub acr_values_supported: Option<Vec<String>>,
// pairwise and public are valid by spec, but servers can add more // pairwise and public are valid by spec, but servers can add more
pub subject_types_supported: Vec<String>, pub subject_types_supported: Vec<String>,
// Must include at least RS256, none is only allowed with response types without id tokens // 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_signing_alg_values_supported: Vec<String>,
#[serde(default)] pub id_token_encryption_alg_values_supported: Option<Vec<String>>, #[serde(default)]
#[serde(default)] pub id_token_encryption_enc_values_supported: Option<Vec<String>>, pub id_token_encryption_alg_values_supported: Option<Vec<String>>,
#[serde(default)] pub userinfo_signing_alg_values_supported: Option<Vec<String>>, #[serde(default)]
#[serde(default)] pub userinfo_encryption_alg_values_supported: Option<Vec<String>>, pub id_token_encryption_enc_values_supported: Option<Vec<String>>,
#[serde(default)] pub userinfo_encryption_enc_values_supported: Option<Vec<String>>, #[serde(default)]
#[serde(default)] pub request_object_signing_alg_values_supported: Option<Vec<String>>, pub userinfo_signing_alg_values_supported: Option<Vec<String>>,
#[serde(default)] pub request_object_encryption_alg_values_supported: Option<Vec<String>>, #[serde(default)]
#[serde(default)] pub request_object_encryption_enc_values_supported: Option<Vec<String>>, pub userinfo_encryption_alg_values_supported: Option<Vec<String>>,
#[serde(default)]
pub userinfo_encryption_enc_values_supported: Option<Vec<String>>,
#[serde(default)]
pub request_object_signing_alg_values_supported: Option<Vec<String>>,
#[serde(default)]
pub request_object_encryption_alg_values_supported: Option<Vec<String>>,
#[serde(default)]
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 // Spec options are client_secret_post, client_secret_basic, client_secret_jwt, private_key_jwt
// If omitted, client_secret_basic is used // If omitted, client_secret_basic is used
#[serde(default)] pub token_endpoint_auth_methods_supported: Option<Vec<String>>, #[serde(default)]
pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
// Only wanted with jwt auth methods, should have RS256, none not allowed // Only wanted with jwt auth methods, should have RS256, none not allowed
#[serde(default)] pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>, #[serde(default)]
#[serde(default)] pub display_values_supported: Option<Vec<String>>, pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
#[serde(default)]
pub display_values_supported: Option<Vec<String>>,
// Valid options are normal, aggregated, and distributed. If omitted, only use normal // Valid options are normal, aggregated, and distributed. If omitted, only use normal
#[serde(default)] pub claim_types_supported: Option<Vec<String>>, #[serde(default)]
#[serde(default)] pub claims_supported: Option<Vec<String>>, pub claim_types_supported: Option<Vec<String>>,
#[serde(default)] #[serde(with = "url_serde")] pub service_documentation: Option<Url>, #[serde(default)]
#[serde(default)] pub claims_locales_supported: Option<Vec<String>>, pub claims_supported: Option<Vec<String>>,
#[serde(default)] pub ui_locales_supported: Option<Vec<String>>, #[serde(default)]
#[serde(default)] pub claims_parameter_supported: bool, #[serde(with = "url_serde")]
#[serde(default)] pub request_parameter_supported: bool, pub service_documentation: Option<Url>,
#[serde(default = "tru")] pub request_uri_parameter_supported: bool, #[serde(default)]
#[serde(default)] pub require_request_uri_registration: bool, pub claims_locales_supported: Option<Vec<String>>,
#[serde(default)]
pub ui_locales_supported: Option<Vec<String>>,
#[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<Url>, #[serde(default)]
#[serde(default)] #[serde(with = "url_serde")] pub op_tos_uri: Option<Url>, #[serde(with = "url_serde")]
pub op_policy_uri: Option<Url>,
#[serde(default)]
#[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 // This is a NONSTANDARD extension Google uses that is a part of the Oauth discovery draft
#[serde(default)] pub code_challenge_methods_supported: Option<Vec<String>>, #[serde(default)]
pub code_challenge_methods_supported: Option<Vec<String>>,
} }
// This seems really dumb... // This seems really dumb...

View File

@ -13,7 +13,7 @@ macro_rules! from {
$to::$from(e) $to::$from(e)
} }
} }
} };
} }
#[derive(Debug, Fail)] #[derive(Debug, Fail)]
@ -71,11 +71,20 @@ pub enum Validation {
#[derive(Debug, Fail)] #[derive(Debug, Fail)]
pub enum Mismatch { pub enum Mismatch {
#[fail(display = "Client ID and Token authorized party mismatch: '{}', '{}'", expected, actual)] #[fail(
display = "Client ID and Token authorized party mismatch: '{}', '{}'",
expected, actual
)]
AuthorizedParty { expected: String, actual: String }, AuthorizedParty { expected: String, actual: String },
#[fail(display = "Configured issuer and token issuer mismatch: '{}' '{}'", expected, actual)] #[fail(
display = "Configured issuer and token issuer mismatch: '{}' '{}'",
expected, actual
)]
Issuer { expected: String, actual: String }, Issuer { expected: String, actual: String },
#[fail(display = "Given nonce does not match token nonce: '{}', '{}'", expected, actual)] #[fail(
display = "Given nonce does not match token nonce: '{}', '{}'",
expected, actual
)]
Nonce { expected: String, actual: String }, Nonce { expected: String, actual: String },
} }
@ -96,13 +105,16 @@ pub enum Expiry {
#[fail(display = "Token expired at: {}", _0)] #[fail(display = "Token expired at: {}", _0)]
Expires(::chrono::naive::NaiveDateTime), Expires(::chrono::naive::NaiveDateTime),
#[fail(display = "Token is too old: {}", _0)] #[fail(display = "Token is too old: {}", _0)]
MaxAge(::chrono::Duration) MaxAge(::chrono::Duration),
} }
#[derive(Debug, Fail)] #[derive(Debug, Fail)]
pub enum Userinfo { pub enum Userinfo {
#[fail(display = "Config has no userinfo url")] #[fail(display = "Config has no userinfo url")]
NoUrl, NoUrl,
#[fail(display = "Token and Userinfo Subjects mismatch: '{}', '{}'", expected, actual)] #[fail(
display = "Token and Userinfo Subjects mismatch: '{}', '{}'",
expected, actual
)]
MismatchSubject { expected: String, actual: String }, MismatchSubject { expected: String, actual: String },
} }

View File

@ -26,8 +26,8 @@ pub fn yahoo() -> Url {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use reqwest::Client;
use crate::discovery::discover; use crate::discovery::discover;
use reqwest::Client;
macro_rules! test { macro_rules! test {
($issuer:ident) => { ($issuer:ident) => {
@ -36,7 +36,7 @@ mod tests {
let client = Client::new(); let client = Client::new();
discover(&client, super::$issuer()).unwrap(); discover(&client, super::$issuer()).unwrap();
} }
} };
} }
test!(google); test!(google);

View File

@ -68,10 +68,10 @@ pub mod token;
pub use crate::error::Error; pub use crate::error::Error;
use biscuit::{Empty, SingleOrMultiple};
use biscuit::jwa::{self, SignatureAlgorithm}; use biscuit::jwa::{self, SignatureAlgorithm};
use biscuit::jwk::{AlgorithmParameters, JWKSet}; use biscuit::jwk::{AlgorithmParameters, JWKSet};
use biscuit::jws::{Compact, Secret}; use biscuit::jws::{Compact, Secret};
use biscuit::{Empty, SingleOrMultiple};
use chrono::{Duration, NaiveDate, Utc}; use chrono::{Duration, NaiveDate, Utc};
use inth_oauth2::token::Token as _t; use inth_oauth2::token::Token as _t;
use reqwest::Url; use reqwest::Url;
@ -93,13 +93,13 @@ pub struct Client {
// Common pattern in the Client::decode function when dealing with mismatched keys // Common pattern in the Client::decode function when dealing with mismatched keys
macro_rules! wrong_key { macro_rules! wrong_key {
($expected:expr, $actual:expr) => ( ($expected:expr, $actual:expr) => {
Err(error::Jose::WrongKeyType { Err(error::Jose::WrongKeyType {
expected: format!("{:?}", $expected), expected: format!("{:?}", $expected),
actual: format!("{:?}", $actual) actual: format!("{:?}", $actual),
}.into() }
) .into())
) };
} }
impl Client { impl Client {
@ -115,29 +115,32 @@ impl Client {
/// Constructs a client from a given provider, key set, and parameters. Unlike ::discover(..) /// Constructs a client from a given provider, key set, and parameters. Unlike ::discover(..)
/// this function does not perform any network operations. /// this function does not perform any network operations.
pub fn new(id: String, secret: pub fn new(
String, redirect: Url, provider: Discovered, jwks: JWKSet<Empty>) -> Self { id: String,
secret: String,
redirect: Url,
provider: Discovered,
jwks: JWKSet<Empty>,
) -> Self {
Client { Client {
oauth: inth_oauth2::Client::new( oauth: inth_oauth2::Client::new(provider, id, secret, Some(redirect.into_string())),
provider, jwks,
id,
secret,
Some(redirect.into_string())),
jwks
} }
} }
/// Passthrough to the redirect_url stored in inth_oauth2 as a str. /// Passthrough to the redirect_url stored in inth_oauth2 as a str.
pub fn redirect_url(&self) -> &str { pub fn redirect_url(&self) -> &str {
self.oauth.redirect_uri.as_ref().expect("We always require a redirect to construct client!") self.oauth
.redirect_uri
.as_ref()
.expect("We always require a redirect to construct client!")
} }
/// Passthrough to the inth_oauth2::client's request token. /// Passthrough to the inth_oauth2::client's request token.
pub fn request_token(&self, pub fn request_token(&self, client: &reqwest::Client, auth_code: &str) -> Result<Token, Error> {
client: &reqwest::Client, self.oauth
auth_code: &str, .request_token(client, auth_code)
) -> Result<Token, Error> { .map_err(Error::from)
self.oauth.request_token(client, auth_code).map_err(Error::from)
} }
/// A reference to the config document of the provider obtained via discovery /// A reference to the config document of the provider obtained via discovery
@ -158,10 +161,12 @@ impl Client {
} }
} }
// Default scope value // Default scope value
None => String::from("openid") None => String::from("openid"),
}; };
let mut url = self.oauth.auth_uri(Some(&scope), options.state.as_ref().map(String::as_str)); let mut url = self
.oauth
.auth_uri(Some(&scope), options.state.as_ref().map(String::as_str));
{ {
let mut query = url.query_pairs_mut(); let mut query = url.query_pairs_mut();
if let Some(ref nonce) = options.nonce { if let Some(ref nonce) = options.nonce {
@ -171,7 +176,11 @@ impl Client {
query.append_pair("display", display.as_str()); query.append_pair("display", display.as_str());
} }
if let Some(ref prompt) = options.prompt { if let Some(ref prompt) = options.prompt {
let s = prompt.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" "); let s = prompt
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(" ");
query.append_pair("prompt", s.as_str()); query.append_pair("prompt", s.as_str());
} }
if let Some(max_age) = options.max_age { if let Some(max_age) = options.max_age {
@ -197,7 +206,11 @@ impl Client {
} }
/// Given an auth_code and auth options, request the token, decode, and validate it. /// Given an auth_code and auth options, request the token, decode, and validate it.
pub fn authenticate(&self, auth_code: &str, nonce: Option<&str>, max_age: Option<&Duration> pub fn authenticate(
&self,
auth_code: &str,
nonce: Option<&str>,
max_age: Option<&Duration>,
) -> Result<Token, Error> { ) -> Result<Token, Error> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let mut token = self.request_token(&client, auth_code)?; let mut token = self.request_token(&client, auth_code)?;
@ -217,14 +230,16 @@ impl Client {
pub fn decode_token(&self, token: &mut IdToken) -> Result<(), Error> { pub fn decode_token(&self, token: &mut IdToken) -> Result<(), Error> {
// This is an early return if the token is already decoded // This is an early return if the token is already decoded
if let Compact::Decoded { .. } = *token { if let Compact::Decoded { .. } = *token {
return Ok(()) return Ok(());
} }
let header = token.unverified_header()?; let header = token.unverified_header()?;
// If there is more than one key, the token MUST have a key id // If there is more than one key, the token MUST have a key id
let key = if self.jwks.keys.len() > 1 { let key = if self.jwks.keys.len() > 1 {
let token_kid = header.registered.key_id.ok_or(Decode::MissingKid)?; let token_kid = header.registered.key_id.ok_or(Decode::MissingKid)?;
self.jwks.find(&token_kid).ok_or(Decode::MissingKey(token_kid))? self.jwks
.find(&token_kid)
.ok_or(Decode::MissingKey(token_kid))?
} else { } else {
// TODO We would want to verify the keyset is >1 in the constructor // TODO We would want to verify the keyset is >1 in the constructor
// rather than every decode call, but we can't return an error in new(). // rather than every decode call, but we can't return an error in new().
@ -244,22 +259,19 @@ impl Client {
let alg = header.registered.algorithm; let alg = header.registered.algorithm;
match key.algorithm { match key.algorithm {
// HMAC // HMAC
AlgorithmParameters::OctectKey { ref value, .. } => { AlgorithmParameters::OctectKey { ref value, .. } => match alg {
match alg { SignatureAlgorithm::HS256
SignatureAlgorithm::HS256 | | SignatureAlgorithm::HS384
SignatureAlgorithm::HS384 | | SignatureAlgorithm::HS512 => {
SignatureAlgorithm::HS512 => {
*token = token.decode(&Secret::Bytes(value.clone()), alg)?; *token = token.decode(&Secret::Bytes(value.clone()), alg)?;
Ok(()) Ok(())
} }
_ => wrong_key!("HS256 | HS384 | HS512", alg) _ => wrong_key!("HS256 | HS384 | HS512", alg),
} },
} AlgorithmParameters::RSA(ref params) => match alg {
AlgorithmParameters::RSA(ref params) => { SignatureAlgorithm::RS256
match alg { | SignatureAlgorithm::RS384
SignatureAlgorithm::RS256 | | SignatureAlgorithm::RS512 => {
SignatureAlgorithm::RS384 |
SignatureAlgorithm::RS512 => {
let pkcs = Secret::RSAModulusExponent { let pkcs = Secret::RSAModulusExponent {
n: params.n.clone(), n: params.n.clone(),
e: params.e.clone(), e: params.e.clone(),
@ -267,9 +279,8 @@ impl Client {
*token = token.decode(&pkcs, alg)?; *token = token.decode(&pkcs, alg)?;
Ok(()) Ok(())
} }
_ => wrong_key!("RS256 | RS384 | RS512", alg) _ => wrong_key!("RS256 | RS384 | RS512", alg),
} },
}
AlgorithmParameters::EllipticCurve(_) => unimplemented!("No support for EC keys yet"), AlgorithmParameters::EllipticCurve(_) => unimplemented!("No support for EC keys yet"),
} }
} }
@ -291,7 +302,7 @@ impl Client {
&self, &self,
token: &IdToken, token: &IdToken,
nonce: Option<&str>, nonce: Option<&str>,
max_age: Option<&Duration> max_age: Option<&Duration>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let claims = token.payload()?; let claims = token.payload()?;
@ -307,14 +318,17 @@ impl Client {
if expected != actual { if expected != actual {
let expected = expected.to_string(); let expected = expected.to_string();
let actual = actual.to_string(); let actual = actual.to_string();
return Err(Validation::Mismatch( return Err(
Mismatch::Nonce { expected, actual }).into()); Validation::Mismatch(Mismatch::Nonce { expected, actual }).into()
);
} }
} }
None => return Err(Validation::Missing(Missing::Nonce).into()), None => return Err(Validation::Missing(Missing::Nonce).into()),
},
None => {
if claims.nonce.is_some() {
return Err(Validation::Missing(Missing::Nonce).into());
} }
None => if claims.nonce.is_some() {
return Err(Validation::Missing(Missing::Nonce).into())
} }
} }
@ -332,9 +346,9 @@ impl Client {
if actual != &self.oauth.client_id { if actual != &self.oauth.client_id {
let expected = self.oauth.client_id.to_string(); let expected = self.oauth.client_id.to_string();
let actual = actual.to_string(); let actual = actual.to_string();
return Err(Validation::Mismatch(Mismatch::AuthorizedParty { return Err(
expected, actual Validation::Mismatch(Mismatch::AuthorizedParty { expected, actual }).into(),
}).into()); );
} }
} }
@ -344,9 +358,10 @@ impl Client {
panic!("chrono::Utc::now() can never be before this was written!") panic!("chrono::Utc::now() can never be before this was written!")
} }
if claims.exp <= now.timestamp() { if claims.exp <= now.timestamp() {
return Err(Validation::Expired( return Err(Validation::Expired(Expiry::Expires(
Expiry::Expires( chrono::naive::NaiveDateTime::from_timestamp(claims.exp, 0),
chrono::naive::NaiveDateTime::from_timestamp(claims.exp, 0))).into()); ))
.into());
} }
if let Some(max) = max_age { if let Some(max) = max_age {
@ -373,27 +388,33 @@ impl Client {
/// - Error::Http if something goes wrong getting the document /// - Error::Http if something goes wrong getting the document
/// - Error::Json if the response is not a valid Userinfo document /// - Error::Json if the response is not a valid Userinfo document
/// - Userinfo::MismatchSubject if the returned userinfo document and tokens subject mismatch /// - Userinfo::MismatchSubject if the returned userinfo document and tokens subject mismatch
pub fn request_userinfo(&self, client: &reqwest::Client, token: &Token pub fn request_userinfo(
&self,
client: &reqwest::Client,
token: &Token,
) -> Result<Userinfo, Error> { ) -> Result<Userinfo, Error> {
match self.config().userinfo_endpoint { match self.config().userinfo_endpoint {
Some(ref url) => { Some(ref url) => {
discovery::secure(&url)?; discovery::secure(&url)?;
let claims = token.id_token.payload()?; let claims = token.id_token.payload()?;
let auth_code = token.access_token().to_string(); let auth_code = token.access_token().to_string();
let mut resp = client.get(url.clone()) let mut resp = client
.get(url.clone())
// FIXME This is a transitional hack for Reqwest 0.9 that should be refactored // FIXME This is a transitional hack for Reqwest 0.9 that should be refactored
// when upstream restores typed header support. // when upstream restores typed header support.
.header_011(reqwest::hyper_011::header::Authorization(reqwest::hyper_011::header::Bearer { token: auth_code })) .header_011(reqwest::hyper_011::header::Authorization(
reqwest::hyper_011::header::Bearer { token: auth_code },
))
.send()?; .send()?;
let info: Userinfo = resp.json()?; let info: Userinfo = resp.json()?;
if claims.sub != info.sub { if claims.sub != info.sub {
let expected = info.sub.clone(); let expected = info.sub.clone();
let actual = claims.sub.clone(); let actual = claims.sub.clone();
return Err(error::Userinfo::MismatchSubject { expected, actual }.into()) return Err(error::Userinfo::MismatchSubject { expected, actual }.into());
} }
Ok(info) Ok(info)
} }
None => Err(error::Userinfo::NoUrl.into()) None => Err(error::Userinfo::NoUrl.into()),
} }
} }
} }
@ -423,30 +444,53 @@ pub struct Options {
#[derive(Debug, Deserialize, Serialize, Validate)] #[derive(Debug, Deserialize, Serialize, Validate)]
pub struct Userinfo { pub struct Userinfo {
pub sub: String, pub sub: String,
#[serde(default)] pub name: Option<String>, #[serde(default)]
#[serde(default)] pub given_name: Option<String>, pub name: Option<String>,
#[serde(default)] pub family_name: Option<String>, #[serde(default)]
#[serde(default)] pub middle_name: Option<String>, pub given_name: Option<String>,
#[serde(default)] pub nickname: Option<String>, #[serde(default)]
#[serde(default)] pub preferred_username: Option<String>, pub family_name: Option<String>,
#[serde(default)] #[serde(with = "url_serde")] pub profile: Option<Url>, #[serde(default)]
#[serde(default)] #[serde(with = "url_serde")] pub picture: Option<Url>, pub middle_name: Option<String>,
#[serde(default)] #[serde(with = "url_serde")] pub website: Option<Url>, #[serde(default)]
#[serde(default)] #[validate(email)] pub email: Option<String>, pub nickname: Option<String>,
#[serde(default)] pub email_verified: bool, #[serde(default)]
pub preferred_username: Option<String>,
#[serde(default)]
#[serde(with = "url_serde")]
pub profile: Option<Url>,
#[serde(default)]
#[serde(with = "url_serde")]
pub picture: Option<Url>,
#[serde(default)]
#[serde(with = "url_serde")]
pub website: Option<Url>,
#[serde(default)]
#[validate(email)]
pub email: Option<String>,
#[serde(default)]
pub email_verified: bool,
// Isn't required to be just male or female // Isn't required to be just male or female
#[serde(default)] pub gender: Option<String>, #[serde(default)]
pub gender: Option<String>,
// ISO 9601:2004 YYYY-MM-DD or YYYY. // ISO 9601:2004 YYYY-MM-DD or YYYY.
#[serde(default)] pub birthdate: Option<NaiveDate>, #[serde(default)]
pub birthdate: Option<NaiveDate>,
// Region/City codes. Should also have a more concrete serializer form. // Region/City codes. Should also have a more concrete serializer form.
#[serde(default)] pub zoneinfo: Option<String>, #[serde(default)]
pub zoneinfo: Option<String>,
// Usually RFC5646 langcode-countrycode, maybe with a _ sep, could be arbitrary // Usually RFC5646 langcode-countrycode, maybe with a _ sep, could be arbitrary
#[serde(default)] pub locale: Option<String>, #[serde(default)]
pub locale: Option<String>,
// Usually E.164 format number // Usually E.164 format number
#[serde(default)] pub phone_number: Option<String>, #[serde(default)]
#[serde(default)] pub phone_number_verified: bool, pub phone_number: Option<String>,
#[serde(default)] pub address: Option<Address>, #[serde(default)]
#[serde(default)] pub updated_at: Option<i64>, pub phone_number_verified: bool,
#[serde(default)]
pub address: Option<Address>,
#[serde(default)]
pub updated_at: Option<i64>,
} }
/// The four values for the preferred display parameter in the Options. See spec for details. /// The four values for the preferred display parameter in the Options. See spec for details.
@ -493,20 +537,26 @@ impl Prompt {
/// Address Claim struct. Can be only formatted, only the rest, or both. /// Address Claim struct. Can be only formatted, only the rest, or both.
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Address { pub struct Address {
#[serde(default)] pub formatted: Option<String>, #[serde(default)]
#[serde(default)] pub street_address: Option<String>, pub formatted: Option<String>,
#[serde(default)] pub locality: Option<String>, #[serde(default)]
#[serde(default)] pub region: Option<String>, pub street_address: Option<String>,
#[serde(default)]
pub locality: Option<String>,
#[serde(default)]
pub region: Option<String>,
// Countries like the UK use alphanumeric postal codes, so you can't just use a number here // Countries like the UK use alphanumeric postal codes, so you can't just use a number here
#[serde(default)] pub postal_code: Option<String>, #[serde(default)]
#[serde(default)] pub country: Option<String>, pub postal_code: Option<String>,
#[serde(default)]
pub country: Option<String>,
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use reqwest::Url;
use crate::Client;
use crate::issuer; use crate::issuer;
use crate::Client;
use reqwest::Url;
macro_rules! test { macro_rules! test {
($issuer:ident) => { ($issuer:ident) => {
@ -518,7 +568,7 @@ mod tests {
let client = Client::discover(id, secret, redirect, issuer::$issuer()).unwrap(); let client = Client::discover(id, secret, redirect, issuer::$issuer()).unwrap();
client.auth_url(&Default::default()); client.auth_url(&Default::default());
} }
} };
} }
test!(google); test!(google);

View File

@ -14,7 +14,8 @@ type IdToken = Jws<Claims, Empty>;
/// ID Token contents. [See spec.](https://openid.net/specs/openid-connect-basic-1_0.html#IDToken) /// ID Token contents. [See spec.](https://openid.net/specs/openid-connect-basic-1_0.html#IDToken)
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct Claims { pub struct Claims {
#[serde(with = "url_serde")] pub iss: Url, #[serde(with = "url_serde")]
pub iss: Url,
// Max 255 ASCII chars // Max 255 ASCII chars
// Can't deserialize a [u8; 255] // Can't deserialize a [u8; 255]
pub sub: String, pub sub: String,
@ -29,14 +30,20 @@ pub struct Claims {
pub exp: i64, pub exp: i64,
pub iat: i64, pub iat: i64,
// required for max_age request // required for max_age request
#[serde(default)] pub auth_time: Option<i64>, #[serde(default)]
#[serde(default)] pub nonce: Option<String>, pub auth_time: Option<i64>,
#[serde(default)]
pub nonce: Option<String>,
// base64 encoded, need to decode it! // base64 encoded, need to decode it!
#[serde(default)] at_hash: Option<String>, #[serde(default)]
#[serde(default)] pub acr: Option<String>, at_hash: Option<String>,
#[serde(default)] pub amr: Option<Vec<String>>, #[serde(default)]
pub acr: Option<String>,
#[serde(default)]
pub amr: Option<Vec<String>>,
// If exists, must be client_id // If exists, must be client_id
#[serde(default)] pub azp: Option<String>, #[serde(default)]
pub azp: Option<String>,
} }
impl Claims { impl Claims {
@ -70,9 +77,10 @@ impl Token {
// TODO Support extracting a jwe token according to spec. Right now we only support jws tokens. // TODO Support extracting a jwe token according to spec. Right now we only support jws tokens.
fn id_token(json: &Value) -> Result<IdToken, ParseError> { fn id_token(json: &Value) -> Result<IdToken, ParseError> {
let obj = json.as_object().ok_or(ParseError::ExpectedType("object"))?; let obj = json.as_object().ok_or(ParseError::ExpectedType("object"))?;
let token = obj.get("id_token").and_then(Value::as_str).ok_or( let token = obj
ParseError::ExpectedFieldType("id_token", "string"), .get("id_token")
)?; .and_then(Value::as_str)
.ok_or(ParseError::ExpectedFieldType("id_token", "string"))?;
Ok(Jws::new_encoded(token)) Ok(Jws::new_encoded(token))
} }
} }