// 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 actix::prelude::*;
use crate::errors::*;
use crimp::Request;
use url::Url;
use url_serde;
use curl::easy::Form;
/// 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)
}