rustfmt 2018 formatting
This commit is contained in:
parent
58b5cce552
commit
c689a92dfa
|
@ -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
|
||||||
|
|
106
src/discovery.rs
106
src/discovery.rs
|
@ -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...
|
||||||
|
|
24
src/error.rs
24
src/error.rs
|
@ -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 },
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
246
src/lib.rs
246
src/lib.rs
|
@ -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().
|
||||||
|
@ -237,39 +252,35 @@ impl Client {
|
||||||
return wrong_key!(sig, header.registered.algorithm);
|
return wrong_key!(sig, header.registered.algorithm);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return wrong_key!(SignatureAlgorithm::default(), alg);
|
return wrong_key!(SignatureAlgorithm::default(), alg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) => match alg {
|
||||||
SignatureAlgorithm::RS256 |
|
SignatureAlgorithm::RS256
|
||||||
SignatureAlgorithm::RS384 |
|
| SignatureAlgorithm::RS384
|
||||||
SignatureAlgorithm::RS512 => {
|
| 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(),
|
||||||
};
|
};
|
||||||
*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,11 +302,11 @@ 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()?;
|
||||||
|
|
||||||
if claims.iss != self.config().issuer {
|
if claims.iss != self.config().issuer {
|
||||||
let expected = self.config().issuer.as_str().to_string();
|
let expected = self.config().issuer.as_str().to_string();
|
||||||
let actual = claims.iss.as_str().to_string();
|
let actual = claims.iss.as_str().to_string();
|
||||||
return Err(Validation::Mismatch(Mismatch::Issuer { expected, actual }).into());
|
return Err(Validation::Mismatch(Mismatch::Issuer { expected, actual }).into());
|
||||||
|
@ -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() {
|
None => {
|
||||||
return Err(Validation::Missing(Missing::Nonce).into())
|
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);
|
||||||
|
|
28
src/token.rs
28
src/token.rs
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue