-
Notifications
You must be signed in to change notification settings - Fork 17
/
email.rs
149 lines (136 loc) · 5.25 KB
/
email.rs
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
141
142
143
144
145
146
147
148
149
use crate::agents::mailer::SendMail;
use crate::bridges::{complete_auth, BridgeData};
use crate::crypto::random_zbase32;
use crate::email_address::EmailAddress;
use crate::error::BrokerError;
use crate::web::{html_response, json_response, Context, HandlerResult};
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use serde::{Deserialize, Serialize};
use serde_json::json;
const QUERY_ESCAPE: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>');
/// Data we store in the session.
#[derive(Clone, Serialize, Deserialize)]
pub struct EmailBridgeData {
pub code: String,
}
/// Provide authentication through an email loop.
///
/// If the email address' host does not support any native form of authentication, create a
/// randomly-generated one-time pad. Then, send an email containing a link with the secret.
/// Clicking the link will trigger the `confirmation` handler, returning an authentication result
/// to the relying party.
///
/// A form is rendered as an alternative way to confirm, without following the link. Submitting the
/// form results in the same callback as the email link.
pub async fn auth(ctx: &mut Context, email_addr: EmailAddress) -> HandlerResult {
// Generate a 12-character one-time pad.
let code = random_zbase32(12, &ctx.app.rng).await;
// For display, we split it in two groups of 6.
let code_fmt = [&code[0..6], &code[6..12]].join(" ");
// Generate the URL used to verify email address ownership.
let href = format!(
"{}/confirm?session={}&code={}",
ctx.app.public_url,
utf8_percent_encode(&ctx.session_id, QUERY_ESCAPE),
utf8_percent_encode(&code, QUERY_ESCAPE)
);
let display_origin = ctx
.return_params
.as_ref()
.expect("email::request called without redirect_uri set")
.redirect_uri
.origin()
.unicode_serialization();
let catalog = ctx.catalog();
let subject = format!(
"{} {}",
catalog.gettext("Finish logging in to"),
display_origin
);
let params = &[
("display_origin", display_origin.as_str()),
("code", &code_fmt),
("link", &href),
("title", catalog.gettext("Finish logging in to")),
("explanation", catalog.gettext("You received this email so that we may confirm your email address and finish your login to:")),
("click", catalog.gettext("Click here to login")),
("alternate", catalog.gettext("Alternatively, enter the following code on the login page:")),
];
let html_body = ctx.app.templates.email_html.render(params);
let text_body = ctx.app.templates.email_text.render(params);
// Store the code in the session for use in the verify handler. We should never fail to claim
// the session, because we only get here after all other options have failed.
if !ctx
.save_session(BridgeData::Email(EmailBridgeData { code }))
.await?
{
return Err(BrokerError::Internal(
"email fallback failed to claim session".to_owned(),
));
}
// Send the mail.
let ok = ctx
.app
.mailer
.send(SendMail {
to: email_addr,
subject,
html_body,
text_body,
})
.await;
if !ok {
return Err(BrokerError::Internal("Failed to send mail".to_owned()));
}
// Render a form for the user.
if ctx.want_json() {
Ok(json_response(
&json!({
"result": "verification_code_sent",
"session": &ctx.session_id,
}),
None,
))
} else {
let catalog = ctx.catalog();
Ok(html_response(ctx.app.templates.confirm_email.render(&[
("display_origin", display_origin.as_str()),
("session_id", &ctx.session_id),
("title", catalog.gettext("Confirm your address")),
(
"explanation",
catalog.gettext("We've sent you an email to confirm your address."),
),
(
"use",
catalog.gettext("Use the link in that email to login to"),
),
(
"alternate",
catalog.gettext(
"Alternatively, enter the code from the email to continue in this browser tab:",
),
),
])))
}
}
/// Request handler for one-time pad email loop confirmation.
///
/// Retrieves the session based session ID and the expected one-time pad. Verifies the code and
/// returns the resulting token to the relying party.
pub async fn confirmation(ctx: &mut Context) -> HandlerResult {
let mut params = ctx.form_params();
let session_id = try_get_provider_param!(params, "session");
let code = try_get_provider_param!(params, "code")
.replace(char::is_whitespace, "")
.to_lowercase();
#[allow(clippy::match_wildcard_for_single_variants)]
let bridge_data = match ctx.load_session(&session_id).await? {
BridgeData::Email(bridge_data) => bridge_data,
_ => return Err(BrokerError::ProviderInput("invalid session".to_owned())),
};
if code != bridge_data.code {
return Err(BrokerError::ProviderInput("incorrect code".to_owned()));
}
complete_auth(ctx).await
}