about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorVincent Ambo <tazjin@gmail.com>2018-04-08T20·36+0200
committerVincent Ambo <tazjin@gmail.com>2018-04-08T20·36+0200
commit249f17b60a0313a66443a5c67403794986430e34 (patch)
treecd73f56fcb822125de967b8486b74f0887256043 /src
parentda33786939979350b58a09145b56913963380c92 (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.rs9
-rw-r--r--src/main.rs10
-rw-r--r--src/oidc.rs135
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(&params)
+            .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)
+}