rustfmt 2018 formatting
This commit is contained in:
parent
58b5cce552
commit
c689a92dfa
|
@ -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
|
||||
|
|
110
src/discovery.rs
110
src/discovery.rs
|
@ -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)?;
|
||||
|
|
24
src/error.rs
24
src/error.rs
|
@ -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 },
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
276
src/lib.rs
276
src/lib.rs
|
@ -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);
|
||||
|
|
30
src/token.rs
30
src/token.rs
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue