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
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).
Experimental async version built on Hyper [here](https://gitlab.com/zanny/hyper-openid).
## Documentation
## License

View File

@ -1,5 +1,5 @@
use biscuit::Empty;
use biscuit::jwk::JWKSet;
use biscuit::Empty;
use inth_oauth2::provider::Provider;
use inth_oauth2::token::Expiring;
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
#[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")]
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.
#[serde(with = "url_serde")] pub token_endpoint: Url,
#[serde(default)] #[serde(with = "url_serde")] 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>>,
#[serde(with = "url_serde")]
pub token_endpoint: Url,
#[serde(default)]
#[serde(with = "url_serde")]
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
// 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.
#[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.
#[serde(default)] pub grant_types_supported: Option<Vec<String>>,
#[serde(default)] pub acr_values_supported: Option<Vec<String>>,
#[serde(default)]
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
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>,
#[serde(default)] pub id_token_encryption_alg_values_supported: Option<Vec<String>>,
#[serde(default)] pub id_token_encryption_enc_values_supported: Option<Vec<String>>,
#[serde(default)] pub userinfo_signing_alg_values_supported: Option<Vec<String>>,
#[serde(default)] 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>>,
#[serde(default)]
pub id_token_encryption_alg_values_supported: Option<Vec<String>>,
#[serde(default)]
pub id_token_encryption_enc_values_supported: Option<Vec<String>>,
#[serde(default)]
pub userinfo_signing_alg_values_supported: Option<Vec<String>>,
#[serde(default)]
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
// 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
#[serde(default)] pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
#[serde(default)] pub display_values_supported: Option<Vec<String>>,
#[serde(default)]
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
#[serde(default)] pub claim_types_supported: Option<Vec<String>>,
#[serde(default)] pub claims_supported: Option<Vec<String>>,
#[serde(default)] #[serde(with = "url_serde")] pub service_documentation: Option<Url>,
#[serde(default)] 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(with = "url_serde")] pub op_tos_uri: Option<Url>,
#[serde(default)]
pub claim_types_supported: Option<Vec<String>>,
#[serde(default)]
pub claims_supported: Option<Vec<String>>,
#[serde(default)]
#[serde(with = "url_serde")]
pub service_documentation: Option<Url>,
#[serde(default)]
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(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
#[serde(default)] pub code_challenge_methods_supported: Option<Vec<String>>,
#[serde(default)]
pub code_challenge_methods_supported: Option<Vec<String>>,
}
// This seems really dumb...
@ -101,7 +139,7 @@ pub fn discover(client: &Client, issuer: Url) -> Result<Config, Error> {
resp.json().map_err(Error::from)
}
/// Get the JWK set from the given Url. Errors are either a reqwest error or an Insecure error if
/// Get the JWK set from the given Url. Errors are either a reqwest error or an Insecure error if
/// the url isn't https.
pub fn jwks(client: &Client, url: Url) -> Result<JWKSet<Empty>, Error> {
secure(&url)?;

View File

@ -13,7 +13,7 @@ macro_rules! from {
$to::$from(e)
}
}
}
};
}
#[derive(Debug, Fail)]
@ -71,11 +71,20 @@ pub enum Validation {
#[derive(Debug, Fail)]
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 },
#[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 },
#[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 },
}
@ -96,13 +105,16 @@ pub enum Expiry {
#[fail(display = "Token expired at: {}", _0)]
Expires(::chrono::naive::NaiveDateTime),
#[fail(display = "Token is too old: {}", _0)]
MaxAge(::chrono::Duration)
MaxAge(::chrono::Duration),
}
#[derive(Debug, Fail)]
pub enum Userinfo {
#[fail(display = "Config has no userinfo url")]
NoUrl,
#[fail(display = "Token and Userinfo Subjects mismatch: '{}', '{}'", expected, actual)]
#[fail(
display = "Token and Userinfo Subjects mismatch: '{}', '{}'",
expected, actual
)]
MismatchSubject { expected: String, actual: String },
}

View File

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

View File

@ -7,20 +7,20 @@
//! use oidc;
//! use reqwest;
//! use std::default::Default;
//!
//!
//! let id = "my client".to_string();
//! let secret = "a secret to everybody".to_string();
//! let redirect = reqwest::Url::parse("https://my-redirect.foo/dest")?;
//! let issuer = oidc::issuer::google();
//! let client = oidc::discover(id, secret, redirect, issuer)?;
//! let auth_url = client.auth_url(Default::default());
//!
//!
//! // ... send your user to auth_url, get an auth_code back at your redirect url handler
//!
//!
//! let token = client.authenticate(auth_code, None, None)?;
//! ```
//!
//! That example leaves you with a decoded `Token` that has been validated. Your user is
//! That example leaves you with a decoded `Token` that has been validated. Your user is
//! authenticated!
//!
//! You can also take a more nuanced approach that gives you more fine grained control:
@ -29,22 +29,22 @@
//! use oidc;
//! use reqwest;
//! use std::default::Default;
//!
//!
//! let id = "my client".to_string();
//! let secret = "a secret to everybody".to_string();
//! let redirect = reqwest::Url::parse("https://my-redirect.foo/dest")?;
//! let issuer = oidc::issuer::google();
//! let http = reqwest::Client::new();
//!
//!
//! let config = oidc::discovery::discover(&http, issuer)?;
//! let jwks = oidc::discovery::jwks(&http, config.jwks_uri.clone())?;
//! let provider = oidc::discovery::Discovered(config);
//!
//!
//! let client = oidc::new(id, secret, redirect, provider, jwks);
//! let auth_url = client.auth_url(Default::default());
//!
//! // ... send your user to auth_url, get an auth_code back at your redirect url handler
//!
//!
//! let mut token = client.request_token(&http, auth_code)?;
//! client.decode_token(&mut token)?;
//! client.validate_token(&token, None, None)?;
@ -68,10 +68,10 @@ pub mod token;
pub use crate::error::Error;
use biscuit::{Empty, SingleOrMultiple};
use biscuit::jwa::{self, SignatureAlgorithm};
use biscuit::jwk::{AlgorithmParameters, JWKSet};
use biscuit::jws::{Compact, Secret};
use biscuit::{Empty, SingleOrMultiple};
use chrono::{Duration, NaiveDate, Utc};
use inth_oauth2::token::Token as _t;
use reqwest::Url;
@ -93,13 +93,13 @@ pub struct Client {
// Common pattern in the Client::decode function when dealing with mismatched keys
macro_rules! wrong_key {
($expected:expr, $actual:expr) => (
($expected:expr, $actual:expr) => {
Err(error::Jose::WrongKeyType {
expected: format!("{:?}", $expected),
actual: format!("{:?}", $actual)
}.into()
)
)
expected: format!("{:?}", $expected),
actual: format!("{:?}", $actual),
}
.into())
};
}
impl Client {
@ -113,31 +113,34 @@ impl Client {
Ok(Self::new(id, secret, redirect, provider, jwks))
}
/// 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.
pub fn new(id: String, secret:
String, redirect: Url, provider: Discovered, jwks: JWKSet<Empty>) -> Self {
pub fn new(
id: String,
secret: String,
redirect: Url,
provider: Discovered,
jwks: JWKSet<Empty>,
) -> Self {
Client {
oauth: inth_oauth2::Client::new(
provider,
id,
secret,
Some(redirect.into_string())),
jwks
oauth: inth_oauth2::Client::new(provider, id, secret, Some(redirect.into_string())),
jwks,
}
}
/// Passthrough to the redirect_url stored in inth_oauth2 as a 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.
pub fn request_token(&self,
client: &reqwest::Client,
auth_code: &str,
) -> Result<Token, Error> {
self.oauth.request_token(client, auth_code).map_err(Error::from)
pub fn request_token(&self, client: &reqwest::Client, auth_code: &str) -> Result<Token, Error> {
self.oauth
.request_token(client, auth_code)
.map_err(Error::from)
}
/// A reference to the config document of the provider obtained via discovery
@ -145,8 +148,8 @@ impl Client {
&self.oauth.provider.0
}
/// 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
/// 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, options: &Options) -> Url {
let scope = match options.scope {
@ -158,10 +161,12 @@ impl Client {
}
}
// 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();
if let Some(ref nonce) = options.nonce {
@ -171,7 +176,11 @@ impl Client {
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(" ");
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 {
@ -197,7 +206,11 @@ impl Client {
}
/// 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> {
let client = reqwest::Client::new();
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> {
// This is an early return if the token is already decoded
if let Compact::Decoded { .. } = *token {
return Ok(())
return Ok(());
}
let header = token.unverified_header()?;
// If there is more than one key, the token MUST have a key id
let key = if self.jwks.keys.len() > 1 {
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 {
// 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().
@ -237,39 +252,35 @@ impl Client {
return wrong_key!(sig, header.registered.algorithm);
}
} else {
return wrong_key!(SignatureAlgorithm::default(), alg);
return wrong_key!(SignatureAlgorithm::default(), alg);
}
}
let alg = header.registered.algorithm;
match key.algorithm {
// HMAC
AlgorithmParameters::OctectKey { ref value, .. } => {
match alg {
SignatureAlgorithm::HS256 |
SignatureAlgorithm::HS384 |
SignatureAlgorithm::HS512 => {
*token = token.decode(&Secret::Bytes(value.clone()), alg)?;
Ok(())
}
_ => wrong_key!("HS256 | HS384 | HS512", alg)
AlgorithmParameters::OctectKey { ref value, .. } => match alg {
SignatureAlgorithm::HS256
| SignatureAlgorithm::HS384
| SignatureAlgorithm::HS512 => {
*token = token.decode(&Secret::Bytes(value.clone()), alg)?;
Ok(())
}
}
AlgorithmParameters::RSA(ref params) => {
match alg {
SignatureAlgorithm::RS256 |
SignatureAlgorithm::RS384 |
SignatureAlgorithm::RS512 => {
let pkcs = Secret::RSAModulusExponent {
n: params.n.clone(),
e: params.e.clone(),
};
*token = token.decode(&pkcs, alg)?;
Ok(())
}
_ => wrong_key!("RS256 | RS384 | RS512", alg)
_ => wrong_key!("HS256 | HS384 | HS512", alg),
},
AlgorithmParameters::RSA(ref params) => match alg {
SignatureAlgorithm::RS256
| SignatureAlgorithm::RS384
| SignatureAlgorithm::RS512 => {
let pkcs = Secret::RSAModulusExponent {
n: params.n.clone(),
e: params.e.clone(),
};
*token = token.decode(&pkcs, alg)?;
Ok(())
}
}
_ => wrong_key!("RS256 | RS384 | RS512", alg),
},
AlgorithmParameters::EllipticCurve(_) => unimplemented!("No support for EC keys yet"),
}
}
@ -288,14 +299,14 @@ impl Client {
/// - Validation::Expired::MaxAge is the token is older than the provided max_age
/// - Validation::Missing::Authtime if a max_age was given and the token has no auth time
pub fn validate_token(
&self,
token: &IdToken,
nonce: Option<&str>,
max_age: Option<&Duration>
&self,
token: &IdToken,
nonce: Option<&str>,
max_age: Option<&Duration>,
) -> Result<(), Error> {
let claims = token.payload()?;
if claims.iss != self.config().issuer {
if claims.iss != self.config().issuer {
let expected = self.config().issuer.as_str().to_string();
let actual = claims.iss.as_str().to_string();
return Err(Validation::Mismatch(Mismatch::Issuer { expected, actual }).into());
@ -307,14 +318,17 @@ impl Client {
if expected != actual {
let expected = expected.to_string();
let actual = actual.to_string();
return Err(Validation::Mismatch(
Mismatch::Nonce { expected, actual }).into());
return Err(
Validation::Mismatch(Mismatch::Nonce { expected, actual }).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 {
let expected = self.oauth.client_id.to_string();
let actual = actual.to_string();
return Err(Validation::Mismatch(Mismatch::AuthorizedParty {
expected, actual
}).into());
return Err(
Validation::Mismatch(Mismatch::AuthorizedParty { expected, actual }).into(),
);
}
}
@ -344,9 +358,10 @@ impl Client {
panic!("chrono::Utc::now() can never be before this was written!")
}
if claims.exp <= now.timestamp() {
return Err(Validation::Expired(
Expiry::Expires(
chrono::naive::NaiveDateTime::from_timestamp(claims.exp, 0))).into());
return Err(Validation::Expired(Expiry::Expires(
chrono::naive::NaiveDateTime::from_timestamp(claims.exp, 0),
))
.into());
}
if let Some(max) = max_age {
@ -373,27 +388,33 @@ impl Client {
/// - Error::Http if something goes wrong getting the document
/// - Error::Json if the response is not a valid Userinfo document
/// - 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> {
match self.config().userinfo_endpoint {
Some(ref url) => {
discovery::secure(&url)?;
let claims = token.id_token.payload()?;
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
// 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()?;
let info: Userinfo = resp.json()?;
if claims.sub != info.sub {
let expected = info.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)
}
None => Err(error::Userinfo::NoUrl.into())
None => Err(error::Userinfo::NoUrl.into()),
}
}
}
@ -403,7 +424,7 @@ impl Client {
#[derive(Default)]
pub struct Options {
/// MUST contain openid. By default this is ONLY openid. Official optional scopes are
/// email, profile, address, phone, offline_access. Check the Discovery config
/// email, profile, address, phone, offline_access. Check the Discovery config
/// `scopes_supported` to see what is available at your provider!
pub scope: Option<String>,
pub state: Option<String>,
@ -423,30 +444,53 @@ pub struct Options {
#[derive(Debug, Deserialize, Serialize, Validate)]
pub struct Userinfo {
pub sub: String,
#[serde(default)] pub name: Option<String>,
#[serde(default)] pub given_name: Option<String>,
#[serde(default)] pub family_name: Option<String>,
#[serde(default)] pub middle_name: Option<String>,
#[serde(default)] pub nickname: Option<String>,
#[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,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub given_name: Option<String>,
#[serde(default)]
pub family_name: Option<String>,
#[serde(default)]
pub middle_name: Option<String>,
#[serde(default)]
pub nickname: Option<String>,
#[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
#[serde(default)] pub gender: Option<String>,
#[serde(default)]
pub gender: Option<String>,
// 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.
#[serde(default)] pub zoneinfo: Option<String>,
#[serde(default)]
pub zoneinfo: Option<String>,
// 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
#[serde(default)] pub phone_number: Option<String>,
#[serde(default)] pub phone_number_verified: bool,
#[serde(default)] pub address: Option<Address>,
#[serde(default)] pub updated_at: Option<i64>,
#[serde(default)]
pub phone_number: Option<String>,
#[serde(default)]
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.
@ -493,20 +537,26 @@ impl Prompt {
/// Address Claim struct. Can be only formatted, only the rest, or both.
#[derive(Debug, Deserialize, Serialize)]
pub struct Address {
#[serde(default)] pub formatted: Option<String>,
#[serde(default)] pub street_address: Option<String>,
#[serde(default)] pub locality: Option<String>,
#[serde(default)] pub region: Option<String>,
#[serde(default)]
pub formatted: Option<String>,
#[serde(default)]
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
#[serde(default)] pub postal_code: Option<String>,
#[serde(default)] pub country: Option<String>,
#[serde(default)]
pub postal_code: Option<String>,
#[serde(default)]
pub country: Option<String>,
}
#[cfg(test)]
mod tests {
use reqwest::Url;
use crate::Client;
use crate::issuer;
use crate::Client;
use reqwest::Url;
macro_rules! test {
($issuer:ident) => {
@ -518,7 +568,7 @@ mod tests {
let client = Client::discover(id, secret, redirect, issuer::$issuer()).unwrap();
client.auth_url(&Default::default());
}
}
};
}
test!(google);

View File

@ -11,10 +11,11 @@ pub use biscuit::jws::Compact as Jws;
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)]
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,
@ -29,14 +30,20 @@ pub struct Claims {
pub exp: i64,
pub iat: i64,
// required for max_age request
#[serde(default)] pub auth_time: Option<i64>,
#[serde(default)] pub nonce: Option<String>,
#[serde(default)]
pub auth_time: Option<i64>,
#[serde(default)]
pub nonce: Option<String>,
// base64 encoded, need to decode it!
#[serde(default)] at_hash: Option<String>,
#[serde(default)] pub acr: Option<String>,
#[serde(default)] pub amr: Option<Vec<String>>,
#[serde(default)]
at_hash: Option<String>,
#[serde(default)]
pub acr: Option<String>,
#[serde(default)]
pub amr: Option<Vec<String>>,
// If exists, must be client_id
#[serde(default)] pub azp: Option<String>,
#[serde(default)]
pub azp: Option<String>,
}
impl Claims {
@ -70,9 +77,10 @@ impl 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"),
)?;
let token = obj
.get("id_token")
.and_then(Value::as_str)
.ok_or(ParseError::ExpectedFieldType("id_token", "string"))?;
Ok(Jws::new_encoded(token))
}
}