use std::error::Error as StdError;
use http::header::COOKIE;
use http::{HeaderMap, HeaderValue};
use jiff::{SignedDuration, Timestamp};
use super::*;
use crate::cloudflare::{EMAIL_HEADER, JWT_COOKIE, JWT_HEADER, presents_access};
use crate::core::{Error, generate_session_key, new_token};
use crate::store::{Credential, MemoryStore, Store, Token, TokenKind};
use crate::types::{Method, NewUser, Role};
type TestResult = Result<(), Box<dyn StdError>>;
fn config(trust_email_header: bool, verify_jwt: bool) -> CloudflareConfig {
CloudflareConfig {
enabled: true,
team_domain: "example.cloudflareaccess.com".to_owned(),
audience: "aud".to_owned(),
trust_email_header,
verify_jwt,
}
}
fn header_only() -> CloudflareConfig {
config(true, false)
}
fn auth(store: &MemoryStore, config: Option<CloudflareConfig>) -> Auth {
Auth::builder(store.clone(), generate_session_key())
.cloudflare(config)
.build()
}
fn email(email: &str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(EMAIL_HEADER, HeaderValue::from_str(email).unwrap());
headers
}
fn cookie(value: &'static str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(COOKIE, HeaderValue::from_static(value));
headers
}
fn refused<T>(result: Result<T, Error>) -> AuthError {
match result {
Ok(_) => panic!("expected a refusal"),
Err(error) => error.auth_error().cloned().expect("an AuthError"),
}
}
async fn pending(store: &MemoryStore, email: &str) -> Result<User, Box<dyn StdError>> {
let new = NewUser {
login: None,
email: Some(email.to_owned()),
role: Role::Regular,
};
Ok(store.create_user(&new, Timestamp::now()).await?.unwrap())
}
async fn active(store: &MemoryStore, email: &str) -> Result<User, Box<dyn StdError>> {
let user = pending(store, email).await?;
let cred = Credential::Password(crate::core::dummy_hash().to_owned());
assert!(store.set_credential(user.id, &cred, None).await?);
Ok(store.user(user.id).await?.unwrap())
}
fn has_warning(auth: &Auth, needle: &str) -> bool {
auth.cloudflare_warnings()
.iter()
.any(|w| w.contains(needle))
}
#[test]
fn presents_access_sees_any_access_credential() {
assert!(!presents_access(&HeaderMap::new()));
assert!(!presents_access(&cookie("session=abc; other=1")));
assert!(presents_access(&email("a@example.com")));
assert!(presents_access(&cookie("other=1; CF_Authorization=a.b.c")));
let mut headers = HeaderMap::new();
headers.insert(JWT_HEADER, HeaderValue::from_static("a.b.c"));
assert!(presents_access(&headers));
assert_eq!(JWT_COOKIE, "CF_Authorization");
}
#[tokio::test]
async fn disabled_access_is_logged_once() -> TestResult {
let store = MemoryStore::new();
active(&store, "alice@example.com").await?;
for config in [None, Some(CloudflareConfig::default())] {
let auth = auth(&store, config);
assert!(!auth.cloudflare_available(&HeaderMap::new()).await?);
assert!(auth.cloudflare_warnings().is_empty());
for _ in 0..2 {
assert!(
!auth
.cloudflare_available(&cookie("CF_Authorization=a.b.c"))
.await?
);
}
assert!(
!auth
.cloudflare_available(&email("alice@example.com"))
.await?
);
let error = refused(auth.sign_in_cloudflare(&email("alice@example.com")).await);
assert_eq!(error, AuthError::Unauthorized);
let warnings = auth.cloudflare_warnings();
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("sign-in is disabled"), "{warnings:?}");
}
Ok(())
}
#[tokio::test]
async fn signs_in_existing_users_by_email() -> TestResult {
let store = MemoryStore::new();
let alice = active(&store, "alice@example.com").await?;
let google = pending(&store, "google@example.com").await?;
let cred = Credential::Google {
sub: "sub:google".to_owned(),
email: "google@example.com".to_owned(),
};
assert!(store.set_credential(google.id, &cred, None).await?);
let auth = auth(&store, Some(header_only()));
let headers = email(" Alice@Example.COM ");
assert!(auth.cloudflare_available(&headers).await?);
let (user, session) = auth.sign_in_cloudflare(&headers).await?;
assert_eq!(user, alice);
let (live, _) = auth.session(&session.id).await?.expect("a live session");
assert_eq!(live.id, alice.id);
let (user, _) = auth
.sign_in_cloudflare(&email("google@example.com"))
.await?;
assert_eq!(user.id, google.id);
assert_eq!(user.method, Method::Google);
assert!(auth.cloudflare_warnings().is_empty());
Ok(())
}
#[tokio::test]
async fn unknown_emails_are_refused() -> TestResult {
let store = MemoryStore::new();
let auth = auth(&store, Some(header_only()));
let headers = email("carol@example.com");
assert!(!auth.cloudflare_available(&headers).await?);
let error = refused(auth.sign_in_cloudflare(&headers).await);
assert_eq!(error, AuthError::Unauthorized);
assert_eq!(store.user_count().await?, 0);
assert!(has_warning(
&auth,
"no user has the email carol@example.com"
));
Ok(())
}
#[tokio::test]
async fn pending_users_become_active() -> TestResult {
let store = MemoryStore::new();
let bob = pending(&store, "bob@example.com").await?;
let now = Timestamp::now();
let token = Token {
id: new_token(),
user: bob.id,
kind: TokenKind::Registration,
expires_at: now + SignedDuration::from_hours(1),
used_at: None,
};
store.create_token(&token, now).await?;
let auth = auth(&store, Some(header_only()));
let mut events = auth.events();
let headers = email("bob@example.com");
assert!(auth.cloudflare_available(&headers).await?);
assert_eq!(store.user(bob.id).await?.unwrap().access, Access::Pending);
let (user, session) = auth.sign_in_cloudflare(&headers).await?;
assert_eq!(user.access, Access::Active);
assert_eq!(store.user(bob.id).await?.unwrap().access, Access::Active);
assert!(auth.session(&session.id).await?.is_some());
assert_eq!(events.try_recv()?, AuthEvent::UserChanged(user.info()));
assert!(events.try_recv().is_err());
let error = refused(auth.register_info(&token.id).await);
assert_eq!(error, AuthError::Gone);
auth.sign_in_cloudflare(&headers).await?;
assert!(events.try_recv().is_err());
Ok(())
}
#[tokio::test]
async fn revoked_users_are_refused() -> TestResult {
let store = MemoryStore::new();
let dave = active(&store, "dave@example.com").await?;
assert!(store.revoke(dave.id, Timestamp::now()).await?);
let auth = auth(&store, Some(header_only()));
let headers = email("dave@example.com");
assert!(!auth.cloudflare_available(&headers).await?);
let error = refused(auth.sign_in_cloudflare(&headers).await);
assert_eq!(error, AuthError::Unauthorized);
assert_eq!(store.user(dave.id).await?.unwrap().access, Access::Revoked);
assert!(has_warning(
&auth,
"dave@example.com has had their access revoked"
));
Ok(())
}
#[tokio::test]
async fn bad_tokens_are_logged() -> TestResult {
let store = MemoryStore::new();
active(&store, "alice@example.com").await?;
let auth = auth(&store, Some(config(false, true)));
assert!(!auth.cloudflare_available(&HeaderMap::new()).await?);
assert!(auth.cloudflare_warnings().is_empty());
assert!(
!auth
.cloudflare_available(&email("alice@example.com"))
.await?
);
assert!(has_warning(&auth, "missing Access JWT"));
assert!(
!auth
.cloudflare_available(&cookie("CF_Authorization=a.b.c"))
.await?
);
assert!(has_warning(
&auth,
"Cloudflare Access sign-in failed: malformed Access JWT"
));
Ok(())
}
#[tokio::test]
async fn settings_change_at_runtime() -> TestResult {
let store = MemoryStore::new();
active(&store, "alice@example.com").await?;
let auth = auth(&store, None);
let headers = email("alice@example.com");
assert!(!auth.cloudflare_available(&headers).await?);
auth.set_cloudflare(Some(header_only()));
assert!(auth.cloudflare_available(&headers).await?);
auth.sign_in_cloudflare(&headers).await?;
auth.set_cloudflare(Some(header_only()));
assert!(auth.cloudflare_available(&headers).await?);
auth.set_cloudflare(Some(config(true, true)));
assert!(!auth.cloudflare_available(&headers).await?);
auth.set_cloudflare(Some(CloudflareConfig {
enabled: false,
..header_only()
}));
assert!(!auth.cloudflare_available(&headers).await?);
auth.set_cloudflare(Some(header_only()));
assert!(auth.cloudflare_available(&headers).await?);
auth.set_cloudflare(None);
let error = refused(auth.sign_in_cloudflare(&headers).await);
assert_eq!(error, AuthError::Unauthorized);
Ok(())
}
#[tokio::test]
async fn a_concurrent_revocation_is_not_undone() -> TestResult {
let store = MemoryStore::new();
let erin = pending(&store, "erin@example.com").await?;
let auth = auth(&store, Some(header_only()));
let mut events = auth.events();
store.revoke_before_next_write(erin.id);
let error = refused(auth.sign_in_cloudflare(&email("erin@example.com")).await);
assert_eq!(error, AuthError::Unauthorized);
assert_eq!(store.user(erin.id).await?.unwrap().access, Access::Revoked);
assert!(events.try_recv().is_err());
Ok(())
}