diff options
author | Vincent Ambo <tazjin@gmail.com> | 2018-04-08T20·36+0200 |
---|---|---|
committer | Vincent Ambo <tazjin@gmail.com> | 2018-04-08T20·36+0200 |
commit | 249f17b60a0313a66443a5c67403794986430e34 (patch) | |
tree | cd73f56fcb822125de967b8486b74f0887256043 /src | |
parent | da33786939979350b58a09145b56913963380c92 (diff) |
feat(oidc): Implement initial OIDC actor
Implements an actor that can perform OAuth2 logins (not really OIDC-compliant yet because Rust doesn't have an easy to use JWT library that supports JWKS, and I don't have time for that right now). Currently this hardcodes some Office365-specific stuff.
Diffstat (limited to 'src')
-rw-r--r-- | src/errors.rs | 9 | ||||
-rw-r--r-- | src/main.rs | 10 | ||||
-rw-r--r-- | src/oidc.rs | 135 |
3 files changed, 152 insertions, 2 deletions
diff --git a/src/errors.rs b/src/errors.rs index 3cbda5f4e55d..d07d19cd3790 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -11,6 +11,7 @@ use actix_web::http::StatusCode; use actix; use diesel; use r2d2; +use reqwest; use tera; pub type Result<T> = result::Result<T, ConverseError>; @@ -64,6 +65,14 @@ impl From<actix::MailboxError> for ConverseError { } } +impl From<reqwest::Error> for ConverseError { + fn from(error: reqwest::Error) -> ConverseError { + ConverseError::InternalError { + reason: format!("Failed to make HTTP request: {}", error), + } + } +} + // Support conversion of error type into HTTP error responses: impl ResponseError for ConverseError { diff --git a/src/main.rs b/src/main.rs index 3269e2d4dc3c..7df74b54130c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,14 +13,20 @@ extern crate serde_derive; #[macro_use] extern crate failure; -extern crate chrono; extern crate actix; extern crate actix_web; +extern crate chrono; extern crate env_logger; -extern crate r2d2; extern crate futures; +extern crate r2d2; +extern crate reqwest; extern crate serde; +extern crate url; +extern crate url_serde; +extern crate serde_json; +extern crate hyper; +pub mod oidc; pub mod db; pub mod errors; pub mod handlers; diff --git a/src/oidc.rs b/src/oidc.rs new file mode 100644 index 000000000000..bd2044ce5c9b --- /dev/null +++ b/src/oidc.rs @@ -0,0 +1,135 @@ +//! This module provides authentication via OIDC compliant +//! authentication sources. +//! +//! Currently Converse only supports a single OIDC provider. Note that +//! this has so far only been tested with Office365. + +use actix::prelude::*; +use reqwest; +use url::Url; +use url_serde; +use errors::*; +use reqwest::header::Authorization; +use hyper::header::Bearer; + +/// This structure represents the contents of an OIDC discovery +/// document. +#[derive(Deserialize, Debug, Clone)] +pub struct OidcConfig { + #[serde(with = "url_serde")] + authorization_endpoint: Url, + token_endpoint: String, + userinfo_endpoint: String, + + scopes_supported: Vec<String>, + issuer: String, +} + +#[derive(Clone, Debug)] +pub struct OidcExecutor { + pub client_id: String, + pub client_secret: String, + pub redirect_uri: String, + pub oidc_config: OidcConfig, +} + +/// This struct represents the form response returned by an OIDC +/// provider with the `code`. +#[derive(Debug, Deserialize)] +pub struct CodeResponse { + pub code: String, +} + +/// This struct represents the data extracted from the ID token and +/// stored in the user's session. +#[derive(Debug)] +pub struct Author { + pub name: String, + pub email: String, +} + +impl Actor for OidcExecutor { + type Context = Context<Self>; +} + +/// Message used to request the login URL: +pub struct GetLoginUrl; // TODO: Add a nonce parameter stored in session. + +impl Message for GetLoginUrl { + type Result = String; +} + +impl Handler<GetLoginUrl> for OidcExecutor { + type Result = String; + + fn handle(&mut self, _: GetLoginUrl, _: &mut Self::Context) -> Self::Result { + let mut url: Url = self.oidc_config.authorization_endpoint.clone(); + { + let mut params = url.query_pairs_mut(); + params.append_pair("client_id", &self.client_id); + params.append_pair("response_type", "code"); + params.append_pair("scope", "openid"); + params.append_pair("redirect_uri", &self.redirect_uri); + params.append_pair("response_mode", "form_post"); + } + return url.into_string(); + } +} + +/// Message used to request the token from the returned code and +/// retrieve userinfo from the appropriate endpoint. +pub struct RetrieveToken(pub CodeResponse); + +impl Message for RetrieveToken { + type Result = Result<Author>; +} + +#[derive(Debug, Deserialize)] +struct TokenResponse { + access_token: String, +} + +// TODO: This is currently hardcoded to Office365 fields. +#[derive(Debug, Deserialize)] +struct Userinfo { + name: String, + unique_name: String, // email in office365 +} + +impl Handler<RetrieveToken> for OidcExecutor { + type Result = Result<Author>; + + fn handle(&mut self, msg: RetrieveToken, _: &mut Self::Context) -> Self::Result { + debug!("Received OAuth2 code, requesting access_token"); + let client = reqwest::Client::new(); + let params: [(&str, &str); 5] = [ + ("client_id", &self.client_id), + ("client_secret", &self.client_secret), + ("grant_type", "authorization_code"), + ("code", &msg.0.code), + ("redirect_uri", &self.redirect_uri), + ]; + + let response: TokenResponse = client.post(&self.oidc_config.token_endpoint) + .form(¶ms) + .send()? + .json()?; + + let user: Userinfo = client.get(&self.oidc_config.userinfo_endpoint) + .header(Authorization(Bearer { token: response.access_token })) + .send()? + .json()?; + + Ok(Author { + name: user.name, + email: user.unique_name, + }) + } +} + +/// Convenience function to attempt loading an OIDC discovery document +/// from a specified URL: +pub fn load_oidc(url: &str) -> Result<OidcConfig> { + let config: OidcConfig = reqwest::get(url)?.json()?; + Ok(config) +} |