diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e82d1c --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# 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). + +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 + +[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index d87443a..0000000 --- a/src/client.rs +++ /dev/null @@ -1,371 +0,0 @@ -use biscuit::{Empty, SingleOrMultiple}; -use biscuit::jwa::{self, SignatureAlgorithm}; -use biscuit::jwk::{AlgorithmParameters, JWKSet}; -use biscuit::jws::{Compact, Secret}; -use chrono::{Duration, Utc}; -use inth_oauth2; -use inth_oauth2::token::Token as _t; -use reqwest::{self, header, Url}; -use url_serde; -use validator::Validate; - -use std::collections::HashSet; - -use discovery::{self, Config, Discovered}; -use error::{self, Decode, Error, Expiry, Mismatch, Missing, Validation}; -use token::{Claims, Token}; - -type IdToken = Compact; - -#[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, -} - -// Common pattern in the Client::decode function when dealing with mismatched keys -macro_rules! wrong_key { - ($expected:expr, $actual:expr) => ( - Err(error::Jose::WrongKeyType { - expected: format!("{:?}", $expected), - actual: format!("{:?}", $actual) - }.into() - ) - ) -} - -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 client = reqwest::Client::new()?; - let config = discovery::discover(&client, issuer)?; - let jwks = discovery::jwks(&client, config.jwks_uri.clone())?; - 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 - } - } - - pub fn request_token(&self, - client: &reqwest::Client, - auth_code: &str, - ) -> Result { - self.oauth.request_token(client, auth_code) - } - - /// 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") { - unimplemented!() - } - 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 and auth options, request the token, decode, and validate it. - pub fn authenticate(&self, auth_code: &str, options: &Options - ) -> Result { - let client = reqwest::Client::new()?; - let mut token = self.request_token(&client, auth_code)?; - self.decode_token(&mut token.id_token)?; - self.validate_token(&token.id_token, - options.nonce.as_ref().map(String::as_ref), - options.max_age.as_ref())?; - Ok(token) - } - - pub fn decode_token(&self, token: &mut IdToken) -> Result<(), Error> { - // This is an early escape if the token is already decoded - token.encoded()?; - - 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)? - } else { - self.jwks.keys.first().as_ref().ok_or(Decode::EmptySet)? - }; - - if let Some(alg) = key.common.algorithm.as_ref() { - if let &jwa::Algorithm::Signature(alg) = alg { - if header.registered.algorithm != alg { - return wrong_key!(alg, header.registered.algorithm); - } - } else { - 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::RSA(ref params) => { - match alg { - SignatureAlgorithm::RS256 | - SignatureAlgorithm::RS384 | - SignatureAlgorithm::RS512 => { - let pkcs = Secret::Pkcs { - 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"), - } - } - - pub fn validate_token( - &self, - token: &IdToken, - nonce: Option<&str>, - max_age: Option<&Duration> - ) -> Result<(), Error> { - let claims = token.payload()?; - - if claims.iss != self.config().issuer { - return Err(Validation::Mismatch(Mismatch::Issuer).into()); - } - - if let Some(ref nonce) = nonce { - match claims.nonce { - Some(ref test) => { - if test != nonce { - return Err(Validation::Mismatch(Mismatch::Nonce).into()); - } - } - None => return Err(Validation::Missing(Missing::Nonce).into()), - } - } - - if !claims.aud.contains(&self.oauth.client_id) { - return Err(Validation::Mismatch(Mismatch::Audience).into()); - } - // By spec, if there are multiple auds, we must have an azp - if let SingleOrMultiple::Multiple(_) = claims.aud { - if let None = claims.azp { - return Err(Validation::Missing(Missing::AuthorizedParty).into()); - } - } - // If there is an authorized party, it must be our client_id - if let Some(ref azp) = claims.azp { - if azp != &self.oauth.client_id { - return Err(Validation::Mismatch(Mismatch::Authorized).into()); - } - } - - let now = Utc::now(); - // Now should never be less than the time this code was written! - if now.timestamp() < 1504758600 { - panic!("chrono::Utc::now() can never be before this was written!") - } - if claims.exp <= now.timestamp() { - return Err(Validation::Expired(Expiry::Expires).into()); - } - - if let Some(age) = max_age { - match claims.auth_time { - Some(time) => { - // This is not currently risky business. That could change. - if time >= (now - *age).timestamp() { - return Err(error::Validation::Expired(error::Expiry::MaxAge).into()); - } - } - None => return Err(Validation::Missing(Missing::AuthTime).into()), - } - } - - Ok(()) - } - - pub fn request_userinfo(&self, client: &reqwest::Client, token: &Token) -> Result { - match self.config().userinfo_endpoint { - Some(ref url) => { - discovery::secure(&url)?; - if url.origin() != self.config().issuer.origin() { - return Err(error::Userinfo::MismatchIssuer.into()); - } - let claims = token.id_token.payload()?; - let auth_code = token.access_token().to_string(); - let mut resp = client.get(url.clone())? - .header(header::Authorization(header::Bearer { token: auth_code })).send()?; - let info: Userinfo = resp.json()?; - if claims.sub != info.sub { - return Err(error::Userinfo::MismatchSubject.into()) - } - Ok(info) - } - None => Err(error::Userinfo::NoUrl.into()) - } - } -} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index b38b11f..3a7d1b0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -23,6 +23,7 @@ pub enum Error { Validation(Validation), Userinfo(Userinfo), Insecure, + MissingOpenidScope, } from!(Error, Jose); diff --git a/src/lib.rs b/src/lib.rs index 38f9d3a..7901bc6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,64 @@ +//! # OpenID Connect Client +//! +//! There are two ways to interact with this library - the batteries included magic methods, and +//! the slightly more boilerplate but more fine grained ones. For most users the following is what +//! you want. +//! ``` +//! use oidc; +//! use url; +//! use std::default::Default; +//! +//! let id = "my client".to_string(); +//! let secret = "a secret to everybody".to_string(); +//! let redirect = url::Url::parse("https://my-redirect.foo")?; +//! let issuer = oidc::issuer::google(); +//! let client = oidc::discover(id, secret, redirect, issuer)?; +//! let scope = "openid"; +//! let state = "randomstring"; +//! 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, Options::default())?; +//! ``` +//! 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: +//! ``` +//! use oidc; +//! use reqwest; +//! use url; +//! use std::default::Default; +//! +//! let id = "my client".to_string(); +//! let secret = "a secret to everybody".to_string(); +//! let redirect = url::Url::parse("https://my-redirect.foo")?; +//! 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)?; +//! +//! let userinfo = client.request_userinfo(&http, &token)?; +//! ``` +//! This more complicated version uses the discovery module directly. Important distinctions to make +//! between the two: +//! - The complex pattern avoids constructing a new reqwest client every time an outbound method is +//! called. Especially for token decoding having to rebuild reqwest every time can be a large +//! performance penalty. +//! - Tokens don't come decoded or validated. You need to do both manually. +//! - This version demonstrates userinfo. It is not required by spec, so make sure its available! extern crate base64; extern crate biscuit; extern crate chrono; @@ -14,9 +75,381 @@ extern crate validator; #[macro_use] extern crate validator_derive; -pub mod client; pub mod discovery; pub mod error; +pub mod issuer; pub mod token; -pub use error::Error; \ No newline at end of file +pub use error::Error; + +use biscuit::{Empty, SingleOrMultiple}; +use biscuit::jwa::{self, SignatureAlgorithm}; +use biscuit::jwk::{AlgorithmParameters, JWKSet}; +use biscuit::jws::{Compact, Secret}; +use chrono::{Duration, Utc}; +use inth_oauth2::token::Token as _t; +use reqwest::{header, Url}; +use validator::Validate; + +use discovery::{Config, Discovered}; +use error::{Decode, Expiry, Mismatch, Missing, Validation}; +use token::{Claims, Token}; + +type IdToken = Compact; + + +pub struct Client { + oauth: inth_oauth2::Client, + jwks: JWKSet, +} + +// Common pattern in the Client::decode function when dealing with mismatched keys +macro_rules! wrong_key { + ($expected:expr, $actual:expr) => ( + Err(error::Jose::WrongKeyType { + expected: format!("{:?}", $expected), + actual: format!("{:?}", $actual) + }.into() + ) + ) +} + +impl Client { + /// Constructs a client from an issuer url and client parameters via discovery + pub fn discover(id: String, secret: String, redirect: Url, issuer: Url) -> Result { + discovery::secure(&redirect)?; + let client = reqwest::Client::new()?; + let config = discovery::discover(&client, issuer)?; + let jwks = discovery::jwks(&client, config.jwks_uri.clone())?; + let provider = Discovered { config }; + Ok(Self::new(id, secret, redirect, provider, jwks)) + } + + /// 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) -> Self { + Client { + oauth: inth_oauth2::Client::new( + provider, + id, + secret, + Some(redirect.into_string())), + jwks + } + } + + pub fn request_token(&self, + client: &reqwest::Client, + auth_code: &str, + ) -> Result { + self.oauth.request_token(client, auth_code).map_err(Error::from) + } + + /// 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, options: &Options) -> Result{ + let scope = match options.scope { + Some(ref scope) => { + if !scope.contains("openid") { + return Err(Error::MissingOpenidScope) + } + scope + } + // Default scope value + None => "openid" + }; + + 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 { + 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 and auth options, request the token, decode, and validate it. + pub fn authenticate(&self, auth_code: &str, options: &Options + ) -> Result { + let client = reqwest::Client::new()?; + let mut token = self.request_token(&client, auth_code)?; + self.decode_token(&mut token.id_token)?; + self.validate_token(&token.id_token, + options.nonce.as_ref().map(String::as_ref), + options.max_age.as_ref())?; + Ok(token) + } + + pub fn decode_token(&self, token: &mut IdToken) -> Result<(), Error> { + // This is an early escape if the token is already decoded + token.encoded()?; + + 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)? + } else { + self.jwks.keys.first().as_ref().ok_or(Decode::EmptySet)? + }; + + if let Some(alg) = key.common.algorithm.as_ref() { + if let &jwa::Algorithm::Signature(alg) = alg { + if header.registered.algorithm != alg { + return wrong_key!(alg, header.registered.algorithm); + } + } else { + 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::RSA(ref params) => { + match alg { + SignatureAlgorithm::RS256 | + SignatureAlgorithm::RS384 | + SignatureAlgorithm::RS512 => { + let pkcs = Secret::Pkcs { + 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"), + } + } + + pub fn validate_token( + &self, + token: &IdToken, + nonce: Option<&str>, + max_age: Option<&Duration> + ) -> Result<(), Error> { + let claims = token.payload()?; + + if claims.iss != self.config().issuer { + return Err(Validation::Mismatch(Mismatch::Issuer).into()); + } + + if let Some(ref nonce) = nonce { + match claims.nonce { + Some(ref test) => { + if test != nonce { + return Err(Validation::Mismatch(Mismatch::Nonce).into()); + } + } + None => return Err(Validation::Missing(Missing::Nonce).into()), + } + } + + if !claims.aud.contains(&self.oauth.client_id) { + return Err(Validation::Mismatch(Mismatch::Audience).into()); + } + // By spec, if there are multiple auds, we must have an azp + if let SingleOrMultiple::Multiple(_) = claims.aud { + if let None = claims.azp { + return Err(Validation::Missing(Missing::AuthorizedParty).into()); + } + } + // If there is an authorized party, it must be our client_id + if let Some(ref azp) = claims.azp { + if azp != &self.oauth.client_id { + return Err(Validation::Mismatch(Mismatch::Authorized).into()); + } + } + + let now = Utc::now(); + // Now should never be less than the time this code was written! + if now.timestamp() < 1504758600 { + panic!("chrono::Utc::now() can never be before this was written!") + } + if claims.exp <= now.timestamp() { + return Err(Validation::Expired(Expiry::Expires).into()); + } + + if let Some(age) = max_age { + match claims.auth_time { + Some(time) => { + // This is not currently risky business. That could change. + if time >= (now - *age).timestamp() { + return Err(error::Validation::Expired(Expiry::MaxAge).into()); + } + } + None => return Err(Validation::Missing(Missing::AuthTime).into()), + } + } + + Ok(()) + } + + pub fn request_userinfo(&self, client: &reqwest::Client, token: &Token) -> Result { + match self.config().userinfo_endpoint { + Some(ref url) => { + discovery::secure(&url)?; + if url.origin() != self.config().issuer.origin() { + return Err(error::Userinfo::MismatchIssuer.into()); + } + let claims = token.id_token.payload()?; + let auth_code = token.access_token().to_string(); + let mut resp = client.get(url.clone())? + .header(header::Authorization(header::Bearer { token: auth_code })).send()?; + let info: Userinfo = resp.json()?; + if claims.sub != info.sub { + return Err(error::Userinfo::MismatchSubject.into()) + } + Ok(info) + } + None => Err(error::Userinfo::NoUrl.into()) + } + } +} + +/// 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 scope: Option, + pub state: Option, + 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, +} + +/// 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, +} + +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", + } + } +} + +/// 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, +} diff --git a/src/token.rs b/src/token.rs index 5144842..e7bbe27 100644 --- a/src/token.rs +++ b/src/token.rs @@ -57,6 +57,7 @@ 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. +#[derive(Serialize, Deserialize)] pub struct Token { bearer: Bearer, pub id_token: IdToken,