diff options
Diffstat (limited to 'web/converse/src/oidc.rs')
-rw-r--r-- | web/converse/src/oidc.rs | 170 |
1 files changed, 170 insertions, 0 deletions
diff --git a/web/converse/src/oidc.rs b/web/converse/src/oidc.rs new file mode 100644 index 000000000000..75e3eabc88f2 --- /dev/null +++ b/web/converse/src/oidc.rs @@ -0,0 +1,170 @@ +// Copyright (C) 2018-2021 Vincent Ambo <tazjin@tvl.su> +// +// This file is part of Converse. +// +// This program is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// <https://www.gnu.org/licenses/>. + +//! 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 crate::errors::*; +use actix::prelude::*; +use crimp::Request; +use curl::easy::Form; +use url::Url; +use url_serde; + +/// 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, Serialize, Deserialize)] +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. +message!(GetLoginUrl, 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); +message!(RetrieveToken, 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 mut form = Form::new(); + form.part("client_id") + .contents(&self.client_id.as_bytes()) + .add() + .expect("critical error: invalid form data"); + + form.part("client_secret") + .contents(&self.client_secret.as_bytes()) + .add() + .expect("critical error: invalid form data"); + + form.part("grant_type") + .contents("authorization_code".as_bytes()) + .add() + .expect("critical error: invalid form data"); + + form.part("code") + .contents(&msg.0.code.as_bytes()) + .add() + .expect("critical error: invalid form data"); + + form.part("redirect_uri") + .contents(&self.redirect_uri.as_bytes()) + .add() + .expect("critical error: invalid form data"); + + let response = Request::post(&self.oidc_config.token_endpoint) + .user_agent(concat!("converse-", env!("CARGO_PKG_VERSION")))? + .form(form) + .send()?; + + debug!("Received token response: {:?}", response); + let token: TokenResponse = response.as_json()?.body; + + let bearer = format!("Bearer {}", token.access_token); + let user: Userinfo = Request::get(&self.oidc_config.userinfo_endpoint) + .user_agent(concat!("converse-", env!("CARGO_PKG_VERSION")))? + .header("Authorization", &bearer)? + .send()? + .as_json()? + .body; + + 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 = Request::get(url).send()?.as_json()?.body; + Ok(config) +} |