Skip to main content

io_smtp/rfc7628/
auth_oauthbearer.rs

1//! SMTP SASL OAUTHBEARER coroutine; supports both the non-IR and
2//! SASL-IR (RFC 4954 ยง4) flows.
3//!
4//! OAUTHBEARER: <https://www.rfc-editor.org/rfc/rfc7628>
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use std::{
10//!     borrow::Cow,
11//!     io::{Read, Write},
12//!     net::TcpStream,
13//! };
14//!
15//! use secrecy::SecretString;
16//!
17//! use io_smtp::{
18//!     coroutine::{SmtpCoroutine, SmtpCoroutineState, SmtpYield},
19//!     rfc5321::types::{domain::Domain, ehlo_domain::EhloDomain},
20//!     rfc7628::auth_oauthbearer::{SmtpAuthOauthbearer, SmtpAuthOauthbearerOptions},
21//! };
22//!
23//! // Ready stream needed (TCP-connected, TLS-negociated, EHLO consumed)
24//! let mut stream = TcpStream::connect("localhost:25").unwrap();
25//!
26//! let mut buf = [0u8; 4096];
27//!
28//! let token = SecretString::from("ya29.tokenvalue".to_string());
29//! let domain = EhloDomain::Domain(Domain(Cow::Borrowed("client.example.org")));
30//! let opts = SmtpAuthOauthbearerOptions::default();
31//! let mut coroutine = SmtpAuthOauthbearer::new(&token, Some("alice@example.org"), domain, opts);
32//! let mut arg = None;
33//!
34//! loop {
35//!     match coroutine.resume(arg.take()) {
36//!         SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => {
37//!             stream.write_all(&bytes).unwrap();
38//!         }
39//!         SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {
40//!             let n = stream.read(&mut buf).unwrap();
41//!             arg = Some(&buf[..n]);
42//!         }
43//!         SmtpCoroutineState::Complete(Ok(())) => break,
44//!         SmtpCoroutineState::Complete(Err(err)) => panic!("{err}"),
45//!     }
46//! }
47//! ```
48
49use core::fmt;
50
51use alloc::{
52    borrow::Cow,
53    string::{String, ToString},
54    vec::Vec,
55};
56
57use base64::{Engine, engine::general_purpose::STANDARD as base64};
58use bounded_static::IntoBoundedStatic;
59use log::trace;
60use secrecy::{ExposeSecret, SecretBox, SecretString};
61use thiserror::Error;
62
63use crate::{
64    coroutine::*,
65    rfc4954::{auth::SmtpAuthCommand, auth_data::SmtpAuthData},
66    rfc5321::{
67        ehlo::{SmtpEhlo, SmtpEhloError},
68        types::{ehlo_domain::EhloDomain, reply_code::ReplyCode},
69    },
70    send::*,
71    smtp_try,
72};
73
74/// The SASL mechanism name as it appears on the wire.
75pub const OAUTHBEARER: &str = "OAUTHBEARER";
76
77/// Options for [`SmtpAuthOauthbearer::new`].
78#[derive(Clone, Debug, Eq, PartialEq)]
79pub struct SmtpAuthOauthbearerOptions {
80    /// `true` selects SASL-IR (inline credentials); `false` selects
81    /// the non-IR challenge-response flow.
82    pub initial_request: bool,
83    /// Refresh capabilities with an `EHLO` after a successful auth.
84    pub ensure_capabilities: bool,
85}
86
87impl Default for SmtpAuthOauthbearerOptions {
88    fn default() -> Self {
89        Self {
90            initial_request: true,
91            ensure_capabilities: true,
92        }
93    }
94}
95
96/// Failure causes during the SMTP AUTH OAUTHBEARER exchange.
97#[derive(Debug, Error)]
98pub enum SmtpAuthOauthbearerError {
99    #[error("SMTP AUTH OAUTHBEARER failed: rejected {code} {message}")]
100    Rejected { code: u16, message: String },
101    #[error("SMTP AUTH OAUTHBEARER failed: server did not send the expected continuation request")]
102    ExpectedContinuationRequest,
103    #[error("SMTP AUTH OAUTHBEARER failed: {0}")]
104    Send(#[from] SendSmtpCommandError),
105    #[error(transparent)]
106    Ehlo(#[from] SmtpEhloError),
107}
108
109/// I/O-free SMTP AUTH OAUTHBEARER coroutine. The connection MUST be
110/// TLS-protected before calling this. The optional `username` is
111/// embedded in the GS2 header; most servers ignore it.
112pub struct SmtpAuthOauthbearer {
113    state: State,
114    domain: Option<EhloDomain<'static>>,
115    payload: Option<Vec<u8>>,
116    error_detail: Option<String>,
117    opts: SmtpAuthOauthbearerOptions,
118}
119
120impl SmtpAuthOauthbearer {
121    pub fn new(
122        token: &SecretString,
123        username: Option<&str>,
124        domain: EhloDomain<'_>,
125        opts: SmtpAuthOauthbearerOptions,
126    ) -> Self {
127        let payload = build_payload(token, username);
128
129        let state = if opts.initial_request {
130            let cmd = SmtpAuthCommand {
131                mechanism: Cow::Borrowed(OAUTHBEARER),
132                initial_response: Some(SecretBox::new(payload.clone().into_boxed_slice())),
133            };
134            State::Send(SendSmtpCommand::new(cmd))
135        } else {
136            let cmd = SmtpAuthCommand {
137                mechanism: Cow::Borrowed(OAUTHBEARER),
138                initial_response: None,
139            };
140            State::Send(SendSmtpCommand::new(cmd))
141        };
142
143        Self {
144            state,
145            domain: Some(domain.into_static()),
146            payload: Some(payload),
147            error_detail: None,
148            opts,
149        }
150    }
151}
152
153impl SmtpCoroutine for SmtpAuthOauthbearer {
154    type Yield = SmtpYield;
155    type Return = Result<(), SmtpAuthOauthbearerError>;
156
157    fn resume(&mut self, arg: Option<&[u8]>) -> SmtpCoroutineState<Self::Yield, Self::Return> {
158        loop {
159            trace!("auth oauthbearer: {}", self.state);
160
161            match &mut self.state {
162                State::Send(send) => {
163                    let out = smtp_try!(send, arg);
164
165                    if out.response.code == ReplyCode::AUTH_SUCCESSFUL {
166                        if self.opts.initial_request {
167                            self.advance_after_auth();
168                            continue;
169                        }
170                        return SmtpCoroutineState::Complete(Err(
171                            SmtpAuthOauthbearerError::ExpectedContinuationRequest,
172                        ));
173                    }
174
175                    if out.response.code == ReplyCode::AUTH_CONTINUE {
176                        if self.opts.initial_request {
177                            let text = out.response.text().0.trim_start();
178                            if let Ok(detail_bytes) = base64.decode(text.as_bytes()) {
179                                self.error_detail = String::from_utf8(detail_bytes).ok();
180                            }
181
182                            let ack = SmtpAuthData::r#continue(vec![0x01u8]);
183                            self.state = State::AckError(SendSmtpCommand::new(ack));
184                            continue;
185                        }
186
187                        let payload = self.payload.take().expect("payload taken twice");
188                        let data = SmtpAuthData::r#continue(payload.into_boxed_slice());
189                        self.state = State::Continue(SendSmtpCommand::new(data));
190                        continue;
191                    }
192
193                    let code = out.response.code.code();
194                    let message = out.response.text().to_string();
195                    return SmtpCoroutineState::Complete(Err(SmtpAuthOauthbearerError::Rejected {
196                        code,
197                        message,
198                    }));
199                }
200                State::Continue(send) => {
201                    let out = smtp_try!(send, arg);
202
203                    if out.response.code == ReplyCode::AUTH_SUCCESSFUL {
204                        self.advance_after_auth();
205                        continue;
206                    }
207
208                    if out.response.code == ReplyCode::AUTH_CONTINUE {
209                        let text = out.response.text().0.trim_start();
210                        if let Ok(detail_bytes) = base64.decode(text.as_bytes()) {
211                            self.error_detail = String::from_utf8(detail_bytes).ok();
212                        }
213
214                        let ack = SmtpAuthData::r#continue(vec![0x01u8]);
215                        self.state = State::AckError(SendSmtpCommand::new(ack));
216                        continue;
217                    }
218
219                    let code = out.response.code.code();
220                    let message = out.response.text().to_string();
221                    return SmtpCoroutineState::Complete(Err(SmtpAuthOauthbearerError::Rejected {
222                        code,
223                        message,
224                    }));
225                }
226                State::AckError(send) => {
227                    let _ = smtp_try!(send, arg);
228
229                    let message = self
230                        .error_detail
231                        .take()
232                        .unwrap_or_else(|| "authentication failed".into());
233
234                    return SmtpCoroutineState::Complete(Err(SmtpAuthOauthbearerError::Rejected {
235                        code: 535,
236                        message,
237                    }));
238                }
239                State::Ehlo(ehlo) => {
240                    let _ = smtp_try!(ehlo, arg);
241                    return SmtpCoroutineState::Complete(Ok(()));
242                }
243                State::Done => return SmtpCoroutineState::Complete(Ok(())),
244            }
245        }
246    }
247}
248
249impl SmtpAuthOauthbearer {
250    fn advance_after_auth(&mut self) {
251        let _ = self.payload.take();
252        if self.opts.ensure_capabilities {
253            let domain = self.domain.take().expect("domain taken twice");
254            self.state = State::Ehlo(SmtpEhlo::new(domain));
255        } else {
256            self.state = State::Done;
257        }
258    }
259}
260
261enum State {
262    Send(SendSmtpCommand<SmtpAuthCommand<'static>>),
263    Continue(SendSmtpCommand<SmtpAuthData>),
264    AckError(SendSmtpCommand<SmtpAuthData>),
265    Ehlo(SmtpEhlo),
266    Done,
267}
268
269impl fmt::Display for State {
270    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271        match self {
272            Self::Send(_) => f.write_str("send auth oauthbearer"),
273            Self::Continue(_) => f.write_str("send credentials"),
274            Self::AckError(_) => f.write_str("ack error detail"),
275            Self::Ehlo(_) => f.write_str("refresh capabilities"),
276            Self::Done => f.write_str("done"),
277        }
278    }
279}
280
281/// Build the OAUTHBEARER wire payload:
282/// `n,` [`a=<u>`] `,\x01auth=Bearer <t>\x01\x01`.
283fn build_payload(token: &SecretString, username: Option<&str>) -> Vec<u8> {
284    let mut payload = Vec::new();
285    payload.extend_from_slice(b"n,");
286    if let Some(user) = username {
287        payload.extend_from_slice(b"a=");
288        payload.extend_from_slice(user.as_bytes());
289    }
290    payload.push(b',');
291    payload.push(0x01);
292    payload.extend_from_slice(b"auth=Bearer ");
293    payload.extend_from_slice(token.expose_secret().as_bytes());
294    payload.push(0x01);
295    payload.push(0x01);
296    payload
297}
298
299#[cfg(test)]
300mod tests {
301    use crate::rfc5321::types::domain::Domain;
302
303    use super::*;
304
305    fn domain() -> EhloDomain<'static> {
306        EhloDomain::Domain(Domain(Cow::Borrowed("example.com")))
307    }
308
309    fn token() -> SecretString {
310        SecretString::from("ya29.tokenvalue".to_string())
311    }
312
313    #[test]
314    fn ir_success_then_ehlo_returns_ok() {
315        let opts = SmtpAuthOauthbearerOptions::default();
316        let mut auth =
317            SmtpAuthOauthbearer::new(&token(), Some("alice@example.com"), domain(), opts);
318
319        let _ = expect_wants_write(&mut auth, None);
320        expect_wants_read(&mut auth);
321        let _ehlo = expect_wants_write(&mut auth, Some(b"235 OK\r\n"));
322        expect_wants_read(&mut auth);
323        expect_complete_ok(&mut auth, b"250 server.example.com\r\n");
324    }
325
326    #[test]
327    fn ir_success_without_ehlo_returns_ok() {
328        let opts = SmtpAuthOauthbearerOptions {
329            initial_request: true,
330            ensure_capabilities: false,
331        };
332        let mut auth = SmtpAuthOauthbearer::new(&token(), None, domain(), opts);
333        let _ = expect_wants_write(&mut auth, None);
334        expect_wants_read(&mut auth);
335        expect_complete_ok(&mut auth, b"235 OK\r\n");
336    }
337
338    #[test]
339    fn error_detail_returns_rejected() {
340        let opts = SmtpAuthOauthbearerOptions {
341            initial_request: true,
342            ensure_capabilities: false,
343        };
344        let mut auth = SmtpAuthOauthbearer::new(&token(), None, domain(), opts);
345        let _ = expect_wants_write(&mut auth, None);
346        expect_wants_read(&mut auth);
347
348        let challenge = b"334 eyJzdGF0dXMiOiI0MDEifQ==\r\n";
349        let _ack = expect_wants_write(&mut auth, Some(challenge));
350        expect_wants_read(&mut auth);
351
352        let err = expect_complete_err(&mut auth, b"535 authentication failed\r\n");
353        let SmtpAuthOauthbearerError::Rejected { code, message } = err else {
354            panic!("expected SmtpAuthOauthbearerError::Rejected, got {err:?}");
355        };
356        assert_eq!(code, 535);
357        assert!(message.contains("status") || message.contains("401"));
358    }
359
360    #[test]
361    fn rejected_returns_rejected_error() {
362        let opts = SmtpAuthOauthbearerOptions::default();
363        let mut auth = SmtpAuthOauthbearer::new(&token(), None, domain(), opts);
364        let _ = expect_wants_write(&mut auth, None);
365        expect_wants_read(&mut auth);
366
367        let err = expect_complete_err(&mut auth, b"504 mechanism disabled\r\n");
368        let SmtpAuthOauthbearerError::Rejected { code, .. } = err else {
369            panic!("expected SmtpAuthOauthbearerError::Rejected, got {err:?}");
370        };
371        assert_eq!(code, 504);
372    }
373
374    #[test]
375    fn eof_returns_eof_error() {
376        let opts = SmtpAuthOauthbearerOptions::default();
377        let mut auth = SmtpAuthOauthbearer::new(&token(), None, domain(), opts);
378        let _ = expect_wants_write(&mut auth, None);
379        expect_wants_read(&mut auth);
380
381        let err = expect_complete_err(&mut auth, b"");
382        assert!(matches!(
383            err,
384            SmtpAuthOauthbearerError::Send(SendSmtpCommandError::Eof)
385        ));
386    }
387
388    // --- utils
389
390    fn expect_wants_write(cor: &mut SmtpAuthOauthbearer, arg: Option<&[u8]>) -> Vec<u8> {
391        match cor.resume(arg) {
392            SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => bytes,
393            state => panic!("expected WantsWrite, got {state:?}"),
394        }
395    }
396
397    fn expect_wants_read(cor: &mut SmtpAuthOauthbearer) {
398        match cor.resume(None) {
399            SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {}
400            state => panic!("expected WantsRead, got {state:?}"),
401        }
402    }
403
404    fn expect_complete_ok(cor: &mut SmtpAuthOauthbearer, reply: &[u8]) {
405        match cor.resume(Some(reply)) {
406            SmtpCoroutineState::Complete(Ok(())) => {}
407            state => panic!("expected Complete(Ok), got {state:?}"),
408        }
409    }
410
411    fn expect_complete_err(
412        cor: &mut SmtpAuthOauthbearer,
413        reply: &[u8],
414    ) -> SmtpAuthOauthbearerError {
415        match cor.resume(Some(reply)) {
416            SmtpCoroutineState::Complete(Err(err)) => err,
417            state => panic!("expected Complete(Err), got {state:?}"),
418        }
419    }
420}