a291c8690a
GitOrigin-RevId: e6e19f3d81a982a62e1bba08f0b4f7fdc21b4ea0
170 lines
5.2 KiB
Rust
170 lines
5.2 KiB
Rust
// 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)
|
|
}
|