fixed #854 - show session details during OOB auth

This commit is contained in:
Eugene Pankov 2023-08-07 21:54:24 +02:00 committed by Eugene
parent fc1a93b9e3
commit 0bc9ae1b1a
10 changed files with 147 additions and 30 deletions

24
Cargo.lock generated
View file

@ -4976,7 +4976,7 @@ dependencies = [
[[package]] [[package]]
name = "warpgate" name = "warpgate"
version = "0.7.3" version = "0.7.4"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"anyhow", "anyhow",
@ -5012,7 +5012,7 @@ dependencies = [
[[package]] [[package]]
name = "warpgate-admin" name = "warpgate-admin"
version = "0.7.3" version = "0.7.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -5041,7 +5041,7 @@ dependencies = [
[[package]] [[package]]
name = "warpgate-common" name = "warpgate-common"
version = "0.7.3" version = "0.7.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@ -5077,7 +5077,7 @@ dependencies = [
[[package]] [[package]]
name = "warpgate-core" name = "warpgate-core"
version = "0.7.3" version = "0.7.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@ -5117,7 +5117,7 @@ dependencies = [
[[package]] [[package]]
name = "warpgate-database-protocols" name = "warpgate-database-protocols"
version = "0.7.3" version = "0.7.4"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"bytes", "bytes",
@ -5130,7 +5130,7 @@ dependencies = [
[[package]] [[package]]
name = "warpgate-db-entities" name = "warpgate-db-entities"
version = "0.7.3" version = "0.7.4"
dependencies = [ dependencies = [
"chrono", "chrono",
"poem-openapi", "poem-openapi",
@ -5143,7 +5143,7 @@ dependencies = [
[[package]] [[package]]
name = "warpgate-db-migrations" name = "warpgate-db-migrations"
version = "0.7.3" version = "0.7.4"
dependencies = [ dependencies = [
"async-std", "async-std",
"chrono", "chrono",
@ -5155,7 +5155,7 @@ dependencies = [
[[package]] [[package]]
name = "warpgate-protocol-http" name = "warpgate-protocol-http"
version = "0.7.3" version = "0.7.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -5187,7 +5187,7 @@ dependencies = [
[[package]] [[package]]
name = "warpgate-protocol-mysql" name = "warpgate-protocol-mysql"
version = "0.7.3" version = "0.7.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -5214,7 +5214,7 @@ dependencies = [
[[package]] [[package]]
name = "warpgate-protocol-ssh" name = "warpgate-protocol-ssh"
version = "0.7.3" version = "0.7.4"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"anyhow", "anyhow",
@ -5239,7 +5239,7 @@ dependencies = [
[[package]] [[package]]
name = "warpgate-sso" name = "warpgate-sso"
version = "0.7.3" version = "0.7.4"
dependencies = [ dependencies = [
"bytes", "bytes",
"data-encoding", "data-encoding",
@ -5255,7 +5255,7 @@ dependencies = [
[[package]] [[package]]
name = "warpgate-web" name = "warpgate-web"
version = "0.7.3" version = "0.7.4"
dependencies = [ dependencies = [
"rust-embed", "rust-embed",
"serde", "serde",

View file

@ -1,8 +1,11 @@
use std::collections::HashSet; use std::collections::HashSet;
use chrono::{DateTime, Utc};
use rand::Rng;
use uuid::Uuid; use uuid::Uuid;
use super::{AuthCredential, CredentialKind, CredentialPolicy, CredentialPolicyResponse}; use super::{AuthCredential, CredentialKind, CredentialPolicy, CredentialPolicyResponse};
use crate::SessionId;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum AuthResult { pub enum AuthResult {
@ -13,27 +16,43 @@ pub enum AuthResult {
pub struct AuthState { pub struct AuthState {
id: Uuid, id: Uuid,
session_id: Option<Uuid>,
username: String, username: String,
protocol: String, protocol: String,
force_rejected: bool, force_rejected: bool,
policy: Box<dyn CredentialPolicy + Sync + Send>, policy: Box<dyn CredentialPolicy + Sync + Send>,
valid_credentials: Vec<AuthCredential>, valid_credentials: Vec<AuthCredential>,
started: DateTime<Utc>,
identification_string: String,
}
fn generate_identification_string() -> String {
let mut s = String::new();
let mut rng = rand::thread_rng();
for _ in 0..4 {
s.push_str(&format!("{:X}", rng.gen_range(0..16)));
}
s
} }
impl AuthState { impl AuthState {
pub fn new( pub fn new(
id: Uuid, id: Uuid,
session_id: Option<SessionId>,
username: String, username: String,
protocol: String, protocol: String,
policy: Box<dyn CredentialPolicy + Sync + Send>, policy: Box<dyn CredentialPolicy + Sync + Send>,
) -> Self { ) -> Self {
Self { Self {
id, id,
session_id,
username, username,
protocol, protocol,
force_rejected: false, force_rejected: false,
policy, policy,
valid_credentials: vec![], valid_credentials: vec![],
started: Utc::now(),
identification_string: generate_identification_string(),
} }
} }
@ -41,6 +60,10 @@ impl AuthState {
&self.id &self.id
} }
pub fn session_id(&self) -> &Option<SessionId> {
&self.session_id
}
pub fn username(&self) -> &str { pub fn username(&self) -> &str {
&self.username &self.username
} }
@ -49,6 +72,14 @@ impl AuthState {
&self.protocol &self.protocol
} }
pub fn started(&self) -> &DateTime<Utc> {
&self.started
}
pub fn identification_string(&self) -> &str {
&self.identification_string
}
pub fn add_valid_credential(&mut self, credential: AuthCredential) { pub fn add_valid_credential(&mut self, credential: AuthCredential) {
self.valid_credentials.push(credential); self.valid_credentials.push(credential);
} }

View file

@ -6,7 +6,7 @@ use once_cell::sync::Lazy;
use tokio::sync::{broadcast, Mutex}; use tokio::sync::{broadcast, Mutex};
use uuid::Uuid; use uuid::Uuid;
use warpgate_common::auth::{AuthResult, AuthState}; use warpgate_common::auth::{AuthResult, AuthState};
use warpgate_common::WarpgateError; use warpgate_common::{WarpgateError, SessionId};
use crate::ConfigProvider; use crate::ConfigProvider;
@ -49,6 +49,7 @@ impl AuthStateStore {
pub async fn create( pub async fn create(
&mut self, &mut self,
session_id: Option<&SessionId>,
username: &str, username: &str,
protocol: &str, protocol: &str,
) -> Result<(Uuid, Arc<Mutex<AuthState>>), WarpgateError> { ) -> Result<(Uuid, Arc<Mutex<AuthState>>), WarpgateError> {
@ -63,7 +64,13 @@ impl AuthStateStore {
return Err(WarpgateError::UserNotFound) return Err(WarpgateError::UserNotFound)
}; };
let state = AuthState::new(id, username.to_string(), protocol.to_string(), policy); let state = AuthState::new(
id,
session_id.copied(),
username.to_string(),
protocol.to_string(),
policy,
);
self.store self.store
.insert(id, (Arc::new(Mutex::new(state)), Instant::now())); .insert(id, (Arc::new(Mutex::new(state)), Instant::now()));

View file

@ -1,5 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use chrono::{DateTime, Utc};
use poem::session::Session; use poem::session::Session;
use poem::web::Data; use poem::web::Data;
use poem::Request; use poem::Request;
@ -66,7 +67,10 @@ enum LogoutResponse {
#[derive(Object)] #[derive(Object)]
struct AuthStateResponseInternal { struct AuthStateResponseInternal {
pub protocol: String, pub protocol: String,
pub address: Option<String>,
pub started: DateTime<Utc>,
pub state: ApiAuthState, pub state: ApiAuthState,
pub identification_string: String,
} }
#[derive(ApiResponse)] #[derive(ApiResponse)]
@ -214,7 +218,7 @@ impl Api {
let Some(state_arc) = store.get(&state_id.0) else { let Some(state_arc) = store.get(&state_id.0) else {
return Ok(AuthStateResponse::NotFound); return Ok(AuthStateResponse::NotFound);
}; };
serialize_auth_state_inner(state_arc).await serialize_auth_state_inner(state_arc, *services).await
} }
#[oai( #[oai(
@ -237,7 +241,7 @@ impl Api {
state_arc.lock().await.reject(); state_arc.lock().await.reject();
store.complete(&state_id.0).await; store.complete(&state_id.0).await;
session.clear_auth_state(); session.clear_auth_state();
serialize_auth_state_inner(state_arc).await serialize_auth_state_inner(state_arc, *services).await
} }
#[oai( #[oai(
@ -256,7 +260,7 @@ impl Api {
let Some(state_arc) = state_arc else { let Some(state_arc) = state_arc else {
return Ok(AuthStateResponse::NotFound); return Ok(AuthStateResponse::NotFound);
}; };
serialize_auth_state_inner(state_arc).await serialize_auth_state_inner(state_arc, *services).await
} }
#[oai( #[oai(
@ -284,7 +288,7 @@ impl Api {
if let AuthResult::Accepted { .. } = auth_result { if let AuthResult::Accepted { .. } = auth_result {
services.auth_state_store.lock().await.complete(&id).await; services.auth_state_store.lock().await.complete(&id).await;
} }
serialize_auth_state_inner(state_arc).await serialize_auth_state_inner(state_arc, *services).await
} }
#[oai( #[oai(
@ -304,7 +308,7 @@ impl Api {
}; };
state_arc.lock().await.reject(); state_arc.lock().await.reject();
services.auth_state_store.lock().await.complete(&id).await; services.auth_state_store.lock().await.complete(&id).await;
serialize_auth_state_inner(state_arc).await serialize_auth_state_inner(state_arc, *services).await
} }
} }
@ -339,10 +343,25 @@ async fn get_auth_state(
async fn serialize_auth_state_inner( async fn serialize_auth_state_inner(
state_arc: Arc<Mutex<AuthState>>, state_arc: Arc<Mutex<AuthState>>,
services: &Services,
) -> poem::Result<AuthStateResponse> { ) -> poem::Result<AuthStateResponse> {
let state = state_arc.lock().await; let state = state_arc.lock().await;
let session_state_store = services.state.lock().await;
let session_state = state
.session_id()
.and_then(|session_id| session_state_store.sessions.get(&session_id));
let peer_addr = match session_state {
Some(x) => x.lock().await.remote_address,
None => None,
};
Ok(AuthStateResponse::Ok(Json(AuthStateResponseInternal { Ok(AuthStateResponse::Ok(Json(AuthStateResponseInternal {
protocol: state.protocol().to_string(), protocol: state.protocol().to_string(),
address: peer_addr.map(|x| x.ip().to_string()),
started: state.started().clone(),
state: state.verify().into(), state: state.verify().into(),
identification_string: state.identification_string().to_owned(),
}))) })))
} }

View file

@ -198,7 +198,9 @@ pub async fn get_auth_state_for_request(
match session.get_auth_state_id() { match session.get_auth_state_id() {
Some(id) => Ok(store.get(&id.0).ok_or(WarpgateError::InconsistentState)?), Some(id) => Ok(store.get(&id.0).ok_or(WarpgateError::InconsistentState)?),
None => { None => {
let (id, state) = store.create(username, crate::common::PROTOCOL_NAME).await?; let (id, state) = store
.create(None, username, crate::common::PROTOCOL_NAME)
.await?;
session.set(AUTH_STATE_ID_SESSION_KEY, AuthStateId(id)); session.set(AUTH_STATE_ID_SESSION_KEY, AuthStateId(id));
Ok(state) Ok(state)
} }

View file

@ -183,7 +183,11 @@ impl MySqlSession {
.auth_state_store .auth_state_store
.lock() .lock()
.await .await
.create(&username, crate::common::PROTOCOL_NAME) .create(
Some(&self.server_handle.lock().await.id()),
&username,
crate::common::PROTOCOL_NAME,
)
.await? .await?
.1; .1;
let mut state = state_arc.lock().await; let mut state = state_arc.lock().await;

View file

@ -228,7 +228,7 @@ impl ServerSession {
.auth_state_store .auth_state_store
.lock() .lock()
.await .await
.create(username, crate::PROTOCOL_NAME) .create(Some(&self.id), username, crate::PROTOCOL_NAME)
.await? .await?
.1; .1;
self.auth_state = Some(state); self.auth_state = Some(state);
@ -1279,6 +1279,8 @@ impl ServerSession {
let Some(auth_state) = self.auth_state.as_ref() else { let Some(auth_state) = self.auth_state.as_ref() else {
return russh::server::Auth::Reject { proceed_with_methods: None}; return russh::server::Auth::Reject { proceed_with_methods: None};
}; };
let identification_string =
auth_state.lock().await.identification_string().to_owned();
let auth_state_id = *auth_state.lock().await.id(); let auth_state_id = *auth_state.lock().await.id();
let event = self let event = self
.services .services
@ -1311,11 +1313,19 @@ impl ServerSession {
russh::server::Auth::Partial { russh::server::Auth::Partial {
name: Cow::Owned(format!( name: Cow::Owned(format!(
concat!( concat!(
"----------------------------------------------------------------\n", "-----------------------------------------------------------------------\n",
"Warpgate authentication: please open {} in your browser\n", "Warpgate authentication: please open the following URL in your browser:\n",
"----------------------------------------------------------------\n" "{}\n\n",
"Make sure you're seeing this security key: {}\n",
"-----------------------------------------------------------------------\n"
), ),
login_url login_url,
identification_string
.chars()
.into_iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(" ")
)), )),
instructions: Cow::Borrowed(""), instructions: Cow::Borrowed(""),
prompts: Cow::Owned(vec![(Cow::Borrowed("Press Enter when done: "), true)]), prompts: Cow::Owned(vec![(Cow::Borrowed("Press Enter when done: "), true)]),

View file

@ -2,7 +2,7 @@
"openapi": "3.0.0", "openapi": "3.0.0",
"info": { "info": {
"title": "Warpgate Web Admin", "title": "Warpgate Web Admin",
"version": "0.7.1" "version": "0.7.4"
}, },
"servers": [ "servers": [
{ {

View file

@ -4,6 +4,7 @@ import { Alert } from 'sveltestrap'
import { api, ApiAuthState, AuthStateResponseInternal } from 'gateway/lib/api' import { api, ApiAuthState, AuthStateResponseInternal } from 'gateway/lib/api'
import AsyncButton from 'common/AsyncButton.svelte' import AsyncButton from 'common/AsyncButton.svelte'
import DelayedSpinner from 'common/DelayedSpinner.svelte' import DelayedSpinner from 'common/DelayedSpinner.svelte'
import RelativeDate from 'admin/RelativeDate.svelte'
export let params: { stateId: string } export let params: { stateId: string }
let authState: AuthStateResponseInternal let authState: AuthStateResponseInternal
@ -29,6 +30,19 @@ async function reject () {
} }
</script> </script>
<style lang="scss">
.identification-string {
display: flex;
font-size: 3rem;
.card {
padding: 0rem 0.5rem;
border-radius: .5rem;
margin-right: .5rem;
}
}
</style>
{#await init()} {#await init()}
<DelayedSpinner /> <DelayedSpinner />
{:then} {:then}
@ -36,7 +50,25 @@ async function reject () {
<h1>Authorization request</h1> <h1>Authorization request</h1>
</div> </div>
<p>Authorize this {authState.protocol} session?</p> <div class="mb-5">
<div class="mb-2">Ensure this security key matches your authentication prompt:</div>
<div class="identification-string">
{#each authState.identificationString as char}
<div class="card bg-secondary text-light">
<div class="card-body">{char}</div>
</div>
{/each}
</div> </div>
<div class="mb-3">
<div>
Authorize this {authState.protocol} session?
</div>
<small>
Requested <RelativeDate date={authState.started} />
{#if authState.address}from {authState.address}{/if}
</small>
</div>
{#if authState.state === ApiAuthState.Success} {#if authState.state === ApiAuthState.Success}
<Alert color="success"> <Alert color="success">

View file

@ -2,7 +2,7 @@
"openapi": "3.0.0", "openapi": "3.0.0",
"info": { "info": {
"title": "Warpgate HTTP proxy", "title": "Warpgate HTTP proxy",
"version": "0.7.1" "version": "0.7.4"
}, },
"servers": [ "servers": [
{ {
@ -386,14 +386,26 @@
"type": "object", "type": "object",
"required": [ "required": [
"protocol", "protocol",
"state" "started",
"state",
"identification_string"
], ],
"properties": { "properties": {
"protocol": { "protocol": {
"type": "string" "type": "string"
}, },
"address": {
"type": "string"
},
"started": {
"type": "string",
"format": "date-time"
},
"state": { "state": {
"$ref": "#/components/schemas/ApiAuthState" "$ref": "#/components/schemas/ApiAuthState"
},
"identification_string": {
"type": "string"
} }
} }
}, },