about summary refs log tree commit diff
path: root/kittybox-rs/src/indieauth/webauthn.rs
blob: ea3ad3d31a7c6b149dea31e358b879ebe8ce6e3e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
use axum::{
    extract::{Json, Host},
    response::{IntoResponse, Response},
    http::StatusCode, Extension, TypedHeader, headers::{authorization::Bearer, Authorization}
};
use axum_extra::extract::cookie::{CookieJar, Cookie};

use super::backend::AuthBackend;
use crate::database::Storage;

pub(crate) const CHALLENGE_ID_COOKIE: &str = "kittybox_webauthn_challenge_id";

macro_rules! bail {
    ($msg:literal, $err:expr) => {
        {
            ::tracing::error!($msg, $err);
            return ::axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response()
        }
    }
}

pub async fn webauthn_pre_register<A: AuthBackend, D: Storage + 'static>(
    Host(host): Host,
    Extension(db): Extension<D>,
    Extension(auth): Extension<A>,
    cookies: CookieJar
) -> Response {
    let uid = format!("https://{}/", host.clone());
    let uid_url: url::Url = uid.parse().unwrap();
    // This will not find an h-card in onboarding!
    let display_name = match db.get_post(&uid).await {
        Ok(hcard) => match hcard {
            Some(mut hcard) => {
                match hcard["properties"]["uid"][0].take() {
                    serde_json::Value::String(name) => name,
                    _ => String::default()
                }
            },
            None => String::default()
        },
        Err(err) => bail!("Error retrieving h-card: {}", err)
    };

    let webauthn = webauthn::WebauthnBuilder::new(
        &host,
        &uid_url
    )
        .unwrap()
        .rp_name("Kittybox")
        .build()
        .unwrap();

    let (challenge, state) = match webauthn.start_passkey_registration(
        // Note: using a nil uuid here is fine
        // Because the user corresponds to a website anyway
        // We do not track multiple users
        webauthn::prelude::Uuid::nil(),
        &uid,
        &display_name,
        Some(vec![])
    ) {
        Ok((challenge, state)) => (challenge, state),
        Err(err) => bail!("Error generating WebAuthn registration data: {}", err)
    };

    match auth.persist_registration_challenge(&uid_url, state).await {
        Ok(challenge_id) => (
            cookies.add(
                Cookie::build(CHALLENGE_ID_COOKIE, challenge_id)
                    .secure(true)
                    .finish()
            ),
            Json(challenge)
        ).into_response(),
        Err(err) => bail!("Failed to persist WebAuthn challenge: {}", err)
    }
}

pub async fn webauthn_register<A: AuthBackend>(
    Host(host): Host,
    Json(credential): Json<webauthn::prelude::RegisterPublicKeyCredential>,
    // TODO determine if we can use a cookie maybe?
    user_credential: Option<TypedHeader<Authorization<Bearer>>>,
    Extension(auth): Extension<A>
) -> Response {
    let uid = format!("https://{}/", host.clone());
    let uid_url: url::Url = uid.parse().unwrap();

    let pubkeys = match auth.list_webauthn_pubkeys(&uid_url).await {
        Ok(pubkeys) => pubkeys,
        Err(err) => bail!("Error enumerating existing WebAuthn credentials: {}", err)
    };

    if !pubkeys.is_empty() {
        if let Some(TypedHeader(Authorization(token))) = user_credential {
            // TODO check validity of the credential
        } else {
            return StatusCode::UNAUTHORIZED.into_response()
        }
    }

    return StatusCode::OK.into_response()
}

pub(crate) async fn verify<A: AuthBackend>(
    auth: &A,
    website: &url::Url,
    credential: webauthn::prelude::PublicKeyCredential,
    challenge_id: &str
) -> std::io::Result<bool> {
    let host = website.host_str().unwrap();

    let webauthn = webauthn::WebauthnBuilder::new(
        host,
        website
    )
        .unwrap()
        .rp_name("Kittybox")
        .build()
        .unwrap();

    match webauthn.finish_passkey_authentication(
        &credential,
        &auth.retrieve_authentication_challenge(&website, challenge_id).await?
    ) {
        Err(err) => {
            tracing::error!("WebAuthn error: {}", err);
            Ok(false)
        },
        Ok(authentication_result) => {
            let counter = authentication_result.counter();
            let cred_id = authentication_result.cred_id();

            if authentication_result.needs_update() {
                todo!()
            }
            Ok(true)
        }
    }
}