Skip to main content

io_imap/rfc7628/
auth_oauthbearer.rs

1//! IMAP SASL OAUTHBEARER coroutine; supports both the non-IR and
2//! SASL-IR (RFC 4959) flows.
3//!
4//! SASL-IR: <https://www.rfc-editor.org/rfc/rfc4959>
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use std::{
10//!     io::{Read, Write},
11//!     net::TcpStream,
12//! };
13//!
14//! use io_imap::{
15//!     codec::fragmentizer::Fragmentizer,
16//!     coroutine::{ImapCoroutine, ImapCoroutineState, ImapYield},
17//!     rfc7628::auth_oauthbearer::{ImapAuthOauthbearer, ImapAuthOauthbearerOptions},
18//! };
19//!
20//! // Ready stream needed (TCP-connected, TLS-negociated)
21//! let mut stream = TcpStream::connect("localhost:143").unwrap();
22//!
23//! let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
24//! let mut buf = [0u8; 4096];
25//!
26//! let opts = ImapAuthOauthbearerOptions::default();
27//! let mut coroutine = ImapAuthOauthbearer::new(
28//!     "alice@example.org",
29//!     "imap.example.org",
30//!     993,
31//!     "oauth-token",
32//!     opts,
33//! );
34//! let mut arg = None;
35//!
36//! let capability = loop {
37//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
38//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
39//!             stream.write_all(&bytes).unwrap();
40//!         }
41//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
42//!             let n = stream.read(&mut buf).unwrap();
43//!             arg = Some(&buf[..n]);
44//!         }
45//!         ImapCoroutineState::Complete(Ok(capability)) => break capability,
46//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
47//!     }
48//! };
49//!
50//! println!("{capability:?}");
51//! ```
52
53use core::{fmt, mem};
54
55use alloc::{
56    borrow::Cow,
57    string::{String, ToString},
58    vec::Vec,
59};
60
61use imap_codec::{
62    AuthenticateDataCodec, CommandCodec,
63    fragmentizer::Fragmentizer,
64    imap_types::{
65        auth::{AuthMechanism, AuthenticateData},
66        command::{Command, CommandBody},
67        core::{IString, NString, TagGenerator},
68        response::{
69            Capability, Code, CommandContinuationRequest, Data, StatusBody, StatusKind, Tagged,
70        },
71        secret::Secret,
72    },
73};
74use log::trace;
75use thiserror::Error;
76
77use crate::{coroutine::*, imap_try, rfc2971::id::*, rfc3501::capability::*, send::*};
78
79/// Failure causes during the SASL OAUTHBEARER flow.
80#[derive(Clone, Debug, Error)]
81pub enum ImapAuthOauthbearerError {
82    #[error("IMAP AUTHENTICATE OAUTHBEARER failed: NO {0}")]
83    No(String),
84    #[error("IMAP AUTHENTICATE OAUTHBEARER failed: NO {info} ({err})")]
85    NoWithError { info: String, err: String },
86    #[error("IMAP AUTHENTICATE OAUTHBEARER failed: BAD {0}")]
87    Bad(String),
88    #[error("IMAP AUTHENTICATE OAUTHBEARER failed: BYE {0}")]
89    Bye(String),
90
91    #[error("IMAP AUTHENTICATE OAUTHBEARER failed: server did not return a tagged response")]
92    MissingTagged,
93    #[error(
94        "IMAP AUTHENTICATE OAUTHBEARER failed: server did not send the expected continuation request"
95    )]
96    ExpectedContinuationRequest,
97    #[error("IMAP AUTHENTICATE OAUTHBEARER failed: expected NO got {kind:?} ({info})")]
98    UnexpectedStatus { kind: StatusKind, info: String },
99    #[error(
100        "IMAP AUTHENTICATE OAUTHBEARER failed: server returned OK before the mechanism could complete"
101    )]
102    UnexpectedOk,
103
104    #[error("IMAP AUTHENTICATE OAUTHBEARER failed: {0}")]
105    Send(#[from] SendImapCommandError),
106    #[error(transparent)]
107    Capability(#[from] ImapCapabilityGetError),
108    #[error(transparent)]
109    ServerId(#[from] ImapServerIdError),
110}
111
112/// Options for [`ImapAuthOauthbearer::new`].
113#[derive(Clone, Debug, Default, Eq, PartialEq)]
114pub struct ImapAuthOauthbearerOptions {
115    /// `true` selects SASL-IR (RFC 4959, inline credentials);
116    /// `false` selects the non-IR upload-after-challenge flow.
117    pub initial_request: bool,
118    pub ensure_capabilities: bool,
119    pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
120}
121
122/// I/O-free SASL OAUTHBEARER coroutine.
123pub struct ImapAuthOauthbearer {
124    state: State,
125    error: Option<String>,
126    observed: Vec<Capability<'static>>,
127    opts: ImapAuthOauthbearerOptions,
128}
129
130impl ImapAuthOauthbearer {
131    pub fn new(
132        user: impl AsRef<str>,
133        host: impl AsRef<str>,
134        port: u16,
135        token: impl AsRef<str>,
136        opts: ImapAuthOauthbearerOptions,
137    ) -> Self {
138        let tag = TagGenerator::new().generate();
139
140        let u = user.as_ref();
141        let h = host.as_ref();
142        let t = token.as_ref();
143
144        let payload = format!("n,a={u},\x01host={h}\x01port={port}\x01auth=Bearer {t}\x01\x01");
145        let payload = payload.into_bytes().into();
146
147        let state = if opts.initial_request {
148            let body = CommandBody::Authenticate {
149                mechanism: AuthMechanism::OAuthBearer,
150                initial_response: Some(Secret::new(payload)),
151            };
152            let cmd = Command { tag, body };
153            trace!("send IMAP command {cmd:?}");
154            State::SendIr(SendImapCommand::new(CommandCodec::new(), cmd))
155        } else {
156            let body = CommandBody::Authenticate {
157                mechanism: AuthMechanism::OAuthBearer,
158                initial_response: None,
159            };
160            let cmd = Command { tag, body };
161            trace!("send IMAP command {cmd:?}");
162            let send = SendImapCommand::new(CommandCodec::new(), cmd);
163            State::Send { payload, send }
164        };
165
166        Self {
167            state,
168            error: None,
169            observed: Vec::new(),
170            opts,
171        }
172    }
173
174    fn wants_capability(
175        &mut self,
176        code: Option<Code<'static>>,
177        data: Vec<Data<'static>>,
178        untagged: Vec<StatusBody<'static>>,
179    ) -> Option<State> {
180        let mut new_capability = None;
181
182        if let Some(Code::Capability(capability)) = code {
183            new_capability.replace(capability);
184        }
185
186        for data in data {
187            if let Data::Capability(capability) = data {
188                new_capability.replace(capability);
189            }
190        }
191
192        for StatusBody { code, .. } in untagged {
193            if let Some(Code::Capability(capability)) = code {
194                new_capability.replace(capability);
195            }
196        }
197
198        if let Some(capability) = new_capability {
199            self.observed = capability.into_iter().collect();
200        }
201
202        (self.opts.ensure_capabilities && self.observed.is_empty())
203            .then(|| State::Capability(ImapCapabilityGet::new()))
204    }
205
206    fn wants_id(&mut self) -> Option<State> {
207        let params = self.opts.auto_id.take()?;
208        let wire = (!params.is_empty()).then_some(params);
209        Some(State::Id(ImapServerId::new(ImapServerIdOptions {
210            parameters: wire,
211        })))
212    }
213
214    fn extract_json_error(cr: &CommandContinuationRequest<'_>) -> String {
215        let err = match cr {
216            CommandContinuationRequest::Basic(err) => err.text().to_string().into(),
217            CommandContinuationRequest::Base64(err) => String::from_utf8_lossy(err),
218        };
219
220        err.to_string()
221    }
222}
223
224impl ImapCoroutine for ImapAuthOauthbearer {
225    type Yield = ImapYield;
226    type Return = Result<Vec<Capability<'static>>, ImapAuthOauthbearerError>;
227
228    fn resume(
229        &mut self,
230        fragmentizer: &mut Fragmentizer,
231        arg: Option<&[u8]>,
232    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
233        loop {
234            trace!("auth OAUTHBEARER: {}", self.state);
235            match &mut self.state {
236                State::Send { send, payload } => {
237                    let out = imap_try!(send, fragmentizer, arg);
238
239                    if let Some(bye) = out.bye {
240                        let err = ImapAuthOauthbearerError::Bye(bye.text.to_string());
241                        return ImapCoroutineState::Complete(Err(err));
242                    }
243
244                    if out.continuation_request.is_some() {
245                        let payload = mem::take(payload).into_owned();
246                        let auth = AuthenticateData::r#continue(payload);
247                        let codec = AuthenticateDataCodec::new();
248                        self.state = State::Continue(SendImapCommand::new(codec, auth));
249                        continue;
250                    }
251
252                    if let Some(Tagged { body, .. }) = out.tagged {
253                        let err = match body.kind {
254                            StatusKind::Ok => ImapAuthOauthbearerError::UnexpectedOk,
255                            StatusKind::No => ImapAuthOauthbearerError::No(body.text.to_string()),
256                            StatusKind::Bad => ImapAuthOauthbearerError::Bad(body.text.to_string()),
257                        };
258
259                        return ImapCoroutineState::Complete(Err(err));
260                    }
261
262                    let err = ImapAuthOauthbearerError::ExpectedContinuationRequest;
263                    return ImapCoroutineState::Complete(Err(err));
264                }
265                State::SendIr(send) => {
266                    let out = imap_try!(send, fragmentizer, arg);
267
268                    if let Some(bye) = out.bye {
269                        let err = ImapAuthOauthbearerError::Bye(bye.text.to_string());
270                        return ImapCoroutineState::Complete(Err(err));
271                    }
272
273                    if let Some(cr) = out.continuation_request {
274                        self.error.replace(Self::extract_json_error(&cr));
275                        let auth = AuthenticateData::r#continue(vec![0x01]);
276                        let codec = AuthenticateDataCodec::new();
277                        self.state = State::AcknowledgeError(SendImapCommand::new(codec, auth));
278                        continue;
279                    }
280
281                    let Some(Tagged { body, .. }) = out.tagged else {
282                        let err = ImapAuthOauthbearerError::MissingTagged;
283                        return ImapCoroutineState::Complete(Err(err));
284                    };
285
286                    let code = match body.kind {
287                        StatusKind::Ok => body.code,
288                        StatusKind::No => {
289                            let err = ImapAuthOauthbearerError::No(body.text.to_string());
290                            return ImapCoroutineState::Complete(Err(err));
291                        }
292                        StatusKind::Bad => {
293                            let err = ImapAuthOauthbearerError::Bad(body.text.to_string());
294                            return ImapCoroutineState::Complete(Err(err));
295                        }
296                    };
297
298                    if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
299                        self.state = next;
300                        continue;
301                    }
302
303                    if let Some(next) = self.wants_id() {
304                        self.state = next;
305                        continue;
306                    }
307
308                    let capability = mem::take(&mut self.observed);
309                    return ImapCoroutineState::Complete(Ok(capability));
310                }
311                State::Continue(send) => {
312                    let out = imap_try!(send, fragmentizer, arg);
313
314                    if let Some(bye) = out.bye {
315                        let err = ImapAuthOauthbearerError::Bye(bye.text.to_string());
316                        return ImapCoroutineState::Complete(Err(err));
317                    }
318
319                    if let Some(cr) = out.continuation_request {
320                        self.error.replace(Self::extract_json_error(&cr));
321                        let auth = AuthenticateData::r#continue(vec![0x01]);
322                        let codec = AuthenticateDataCodec::new();
323                        self.state = State::AcknowledgeError(SendImapCommand::new(codec, auth));
324                        continue;
325                    }
326
327                    let Some(Tagged { body, .. }) = out.tagged else {
328                        let err = ImapAuthOauthbearerError::MissingTagged;
329                        return ImapCoroutineState::Complete(Err(err));
330                    };
331
332                    let code = match body.kind {
333                        StatusKind::Ok => body.code,
334                        StatusKind::No => {
335                            let err = ImapAuthOauthbearerError::No(body.text.to_string());
336                            return ImapCoroutineState::Complete(Err(err));
337                        }
338                        StatusKind::Bad => {
339                            let err = ImapAuthOauthbearerError::Bad(body.text.to_string());
340                            return ImapCoroutineState::Complete(Err(err));
341                        }
342                    };
343
344                    if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
345                        self.state = next;
346                        continue;
347                    }
348
349                    if let Some(next) = self.wants_id() {
350                        self.state = next;
351                        continue;
352                    }
353
354                    let capability = mem::take(&mut self.observed);
355                    return ImapCoroutineState::Complete(Ok(capability));
356                }
357                State::AcknowledgeError(send) => {
358                    let out = imap_try!(send, fragmentizer, arg);
359
360                    if let Some(bye) = out.bye {
361                        let err = ImapAuthOauthbearerError::Bye(bye.text.to_string());
362                        return ImapCoroutineState::Complete(Err(err));
363                    }
364
365                    let Some(Tagged { body, .. }) = out.tagged else {
366                        let err = ImapAuthOauthbearerError::MissingTagged;
367                        return ImapCoroutineState::Complete(Err(err));
368                    };
369
370                    let info = body.text.to_string();
371
372                    let StatusKind::No = body.kind else {
373                        let kind = body.kind;
374                        let err = ImapAuthOauthbearerError::UnexpectedStatus { kind, info };
375                        return ImapCoroutineState::Complete(Err(err));
376                    };
377
378                    let err = match self.error.take() {
379                        Some(err) => ImapAuthOauthbearerError::NoWithError { info, err },
380                        None => ImapAuthOauthbearerError::No(info),
381                    };
382
383                    return ImapCoroutineState::Complete(Err(err));
384                }
385                State::Capability(capability) => {
386                    self.observed = imap_try!(capability, fragmentizer, arg);
387
388                    if let Some(next) = self.wants_id() {
389                        self.state = next;
390                        continue;
391                    }
392
393                    let capability = mem::take(&mut self.observed);
394                    return ImapCoroutineState::Complete(Ok(capability));
395                }
396                State::Id(id) => {
397                    imap_try!(id, fragmentizer, arg);
398                    let capability = mem::take(&mut self.observed);
399                    return ImapCoroutineState::Complete(Ok(capability));
400                }
401            }
402        }
403    }
404}
405
406enum State {
407    Send {
408        send: SendImapCommand<CommandCodec>,
409        payload: Cow<'static, [u8]>,
410    },
411    SendIr(SendImapCommand<CommandCodec>),
412    Continue(SendImapCommand<AuthenticateDataCodec>),
413    AcknowledgeError(SendImapCommand<AuthenticateDataCodec>),
414    Capability(ImapCapabilityGet),
415    Id(ImapServerId),
416}
417
418impl fmt::Display for State {
419    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420        match self {
421            Self::Send { .. } => f.write_str("send auth"),
422            Self::SendIr(_) => f.write_str("send auth with ir"),
423            Self::Continue(_) => f.write_str("send credentials"),
424            Self::AcknowledgeError(_) => f.write_str("acknowledge error"),
425            Self::Capability(_) => f.write_str("fetch capabilities"),
426            Self::Id(_) => f.write_str("send id"),
427        }
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use core::str;
434
435    use super::*;
436
437    #[test]
438    fn ir_success_returns_ok() {
439        let opts = ImapAuthOauthbearerOptions {
440            initial_request: true,
441            ..Default::default()
442        };
443
444        let mut auth = ImapAuthOauthbearer::new(
445            "user@example.org",
446            "imap.example.org",
447            993,
448            "oauth-token",
449            opts,
450        );
451        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
452
453        let bytes = expect_wants_write(&mut auth, &mut frag, None);
454        let line = str::from_utf8(&bytes).expect("utf8 command");
455        let tag = first_word(line);
456        assert!(line.contains("AUTHENTICATE OAUTHBEARER "));
457
458        expect_wants_read(&mut auth, &mut frag);
459
460        let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
461        expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
462    }
463
464    #[test]
465    fn ir_invalid_token_returns_no_with_error() {
466        let opts = ImapAuthOauthbearerOptions {
467            initial_request: true,
468            ..Default::default()
469        };
470
471        let mut auth = ImapAuthOauthbearer::new(
472            "user@example.org",
473            "imap.example.org",
474            993,
475            "expired-token",
476            opts,
477        );
478        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
479
480        let bytes = expect_wants_write(&mut auth, &mut frag, None);
481        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
482
483        expect_wants_read(&mut auth, &mut frag);
484
485        let (err_json_b64, err_json) = fake_json_error();
486        let challenge = format!("+ {err_json_b64}\r\n");
487        let ack = expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
488        assert_eq!(b"AQ==\r\n", &*ack);
489
490        expect_wants_read(&mut auth, &mut frag);
491
492        let reply = format!("{tag} NO SASL authentication failed\r\n");
493        let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
494        let ImapAuthOauthbearerError::NoWithError { info, err } = err else {
495            panic!("expected ImapAuthOauthbearerError::NoWithError, got {err:?}");
496        };
497        assert_eq!(info, "SASL authentication failed");
498        assert_eq!(err, err_json);
499    }
500
501    #[test]
502    fn ir_tagged_bad_returns_bad_error() {
503        let opts = ImapAuthOauthbearerOptions {
504            initial_request: true,
505            ..Default::default()
506        };
507
508        let mut auth = ImapAuthOauthbearer::new(
509            "user@example.org",
510            "imap.example.org",
511            993,
512            "oauth-token",
513            opts,
514        );
515        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
516
517        let bytes = expect_wants_write(&mut auth, &mut frag, None);
518        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
519
520        expect_wants_read(&mut auth, &mut frag);
521
522        let reply = format!("{tag} BAD AUTHENTICATE not enabled\r\n");
523        let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
524        let ImapAuthOauthbearerError::Bad(text) = err else {
525            panic!("expected ImapAuthOauthbearerError::Bad, got {err:?}");
526        };
527        assert_eq!(text, "AUTHENTICATE not enabled");
528    }
529
530    #[test]
531    fn non_ir_success_returns_ok() {
532        let opts = ImapAuthOauthbearerOptions::default();
533        let mut auth = ImapAuthOauthbearer::new(
534            "user@example.org",
535            "imap.example.org",
536            993,
537            "oauth-token",
538            opts,
539        );
540        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
541
542        let bytes = expect_wants_write(&mut auth, &mut frag, None);
543        let line = str::from_utf8(&bytes).expect("utf8 command");
544        let tag = first_word(line);
545        assert!(line.trim_end().ends_with("AUTHENTICATE OAUTHBEARER"));
546
547        expect_wants_read(&mut auth, &mut frag);
548
549        let creds = expect_wants_write(&mut auth, &mut frag, Some(b"+ \r\n"));
550        assert!(creds.ends_with(b"\r\n"));
551
552        expect_wants_read(&mut auth, &mut frag);
553
554        let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
555        expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
556    }
557
558    #[test]
559    fn non_ir_invalid_token_returns_no_with_error() {
560        let opts = ImapAuthOauthbearerOptions::default();
561        let mut auth = ImapAuthOauthbearer::new(
562            "user@example.org",
563            "imap.example.org",
564            993,
565            "expired-token",
566            opts,
567        );
568        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
569
570        let bytes = expect_wants_write(&mut auth, &mut frag, None);
571        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
572
573        expect_wants_read(&mut auth, &mut frag);
574        expect_wants_write(&mut auth, &mut frag, Some(b"+ \r\n"));
575        expect_wants_read(&mut auth, &mut frag);
576
577        let (err_json_b64, err_json) = fake_json_error();
578        let challenge = format!("+ {err_json_b64}\r\n");
579        let ack = expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
580        assert_eq!(b"AQ==\r\n", &*ack);
581
582        expect_wants_read(&mut auth, &mut frag);
583
584        let reply = format!("{tag} NO SASL authentication failed\r\n");
585        let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
586        let ImapAuthOauthbearerError::NoWithError { info, err } = err else {
587            panic!("expected ImapAuthOauthbearerError::NoWithError, got {err:?}");
588        };
589        assert_eq!(info, "SASL authentication failed");
590        assert_eq!(err, err_json);
591    }
592
593    // --- utils
594
595    fn expect_wants_write(
596        cor: &mut ImapAuthOauthbearer,
597        frag: &mut Fragmentizer,
598        arg: Option<&[u8]>,
599    ) -> Vec<u8> {
600        match cor.resume(frag, arg) {
601            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
602            state => panic!("expected WantsWrite, got {state:?}"),
603        }
604    }
605
606    fn expect_wants_read(cor: &mut ImapAuthOauthbearer, frag: &mut Fragmentizer) {
607        match cor.resume(frag, None) {
608            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
609            state => panic!("expected WantsRead, got {state:?}"),
610        }
611    }
612
613    fn expect_complete_ok(cor: &mut ImapAuthOauthbearer, frag: &mut Fragmentizer, reply: &[u8]) {
614        match cor.resume(frag, Some(reply)) {
615            ImapCoroutineState::Complete(Ok(_)) => {}
616            state => panic!("expected Complete(Ok), got {state:?}"),
617        }
618    }
619
620    fn expect_complete_err(
621        cor: &mut ImapAuthOauthbearer,
622        frag: &mut Fragmentizer,
623        reply: &[u8],
624    ) -> ImapAuthOauthbearerError {
625        match cor.resume(frag, Some(reply)) {
626            ImapCoroutineState::Complete(Err(err)) => err,
627            state => panic!("expected Complete(Err), got {state:?}"),
628        }
629    }
630
631    fn first_word(line: &str) -> &str {
632        line.split_whitespace()
633            .next()
634            .expect("first whitespace-separated token")
635    }
636
637    fn fake_json_error() -> (&'static str, &'static str) {
638        (
639            "eyJzdGF0dXMiOiJpbnZhbGlkX3Rva2VuIiwic2NvcGUiOiJleGFtcGxlX3Njb3BlIiwib3BlbmlkLWNvbmZpZ3VyYXRpb24iOiJodHRwczovL2V4YW1wbGUuY29tLy53ZWxsLWtub3duL29wZW5pZC1jb25maWd1cmF0aW9uIn0=",
640            "{\"status\":\"invalid_token\",\"scope\":\"example_scope\",\"openid-configuration\":\"https://example.com/.well-known/openid-configuration\"}",
641        )
642    }
643}