1use core::{fmt, mem};
60
61use alloc::{
62 string::{String, ToString},
63 vec::Vec,
64};
65
66use imap_codec::{
67 AuthenticateDataCodec, CommandCodec,
68 fragmentizer::Fragmentizer,
69 imap_types::{
70 auth::{AuthMechanism, AuthenticateData},
71 command::{Command, CommandBody},
72 core::{IString, NString, TagGenerator},
73 response::{
74 Capability, Code, CommandContinuationRequest, Data, StatusBody, StatusKind, Tagged,
75 },
76 secret::Secret,
77 },
78};
79use io_sasl::{
80 coroutine::*,
81 xoauth2::{SaslXoauth2, SaslXoauth2Creds, SaslXoauth2Error},
82};
83use log::{debug, trace};
84use secrecy::SecretString;
85use thiserror::Error;
86
87use crate::{coroutine::*, imap_try, rfc2971::id::*, rfc3501::capability::*, send::*};
88
89#[derive(Clone, Debug, Error)]
91pub enum ImapAuthXoauth2Error {
92 #[error("IMAP AUTHENTICATE XOAUTH2 failed: NO {0}")]
94 No(String),
95 #[error("IMAP AUTHENTICATE XOAUTH2 failed: NO {info} ({err})")]
98 NoWithError {
99 info: String,
101 err: String,
103 },
104 #[error("IMAP AUTHENTICATE XOAUTH2 failed: BAD {0}")]
106 Bad(String),
107 #[error("IMAP AUTHENTICATE XOAUTH2 failed: BYE {0}")]
109 Bye(String),
110 #[error("IMAP AUTHENTICATE XOAUTH2 failed: server did not return a tagged response")]
112 MissingTagged,
113 #[error(
115 "IMAP AUTHENTICATE XOAUTH2 failed: server did not send the expected continuation request"
116 )]
117 ExpectedContinuationRequest,
118 #[error(
120 "IMAP AUTHENTICATE XOAUTH2 failed: server returned OK before the mechanism could complete"
121 )]
122 UnexpectedOk,
123 #[error("IMAP AUTHENTICATE XOAUTH2 failed: {0}")]
129 Mechanism(#[from] SaslXoauth2Error),
130 #[error("IMAP AUTHENTICATE XOAUTH2 failed: {0}")]
132 Send(#[from] ImapSendError),
133 #[error(transparent)]
135 Capability(#[from] ImapCapabilityGetError),
136 #[error(transparent)]
138 ServerId(#[from] ImapServerIdError),
139}
140
141#[derive(Clone, Debug, Default, Eq, PartialEq)]
143pub struct ImapAuthXoauth2Options {
144 pub initial_request: bool,
147 pub ensure_capabilities: bool,
150 pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
155}
156
157pub struct ImapAuthXoauth2 {
159 state: State,
160 mechanism: SaslXoauth2,
161 observed: Vec<Capability<'static>>,
162 opts: ImapAuthXoauth2Options,
163}
164
165impl ImapAuthXoauth2 {
166 pub fn new(
173 user: impl AsRef<str>,
174 token: impl AsRef<str>,
175 opts: ImapAuthXoauth2Options,
176 ) -> Self {
177 let mechanism = SaslXoauth2::new(SaslXoauth2Creds {
178 username: user.as_ref().to_string(),
179 token: SecretString::from(token.as_ref().to_string()),
180 });
181
182 Self {
183 state: State::Start,
184 mechanism,
185 observed: Vec::new(),
186 opts,
187 }
188 }
189
190 fn wants_capability(
193 &mut self,
194 code: Option<Code<'static>>,
195 data: Vec<Data<'static>>,
196 untagged: Vec<StatusBody<'static>>,
197 ) -> Option<State> {
198 let mut new_capability = None;
199
200 if let Some(Code::Capability(capability)) = code {
201 new_capability.replace(capability);
202 }
203
204 for data in data {
205 if let Data::Capability(capability) = data {
206 new_capability.replace(capability);
207 }
208 }
209
210 for StatusBody { code, .. } in untagged {
211 if let Some(Code::Capability(capability)) = code {
212 new_capability.replace(capability);
213 }
214 }
215
216 if let Some(capability) = new_capability {
217 self.observed = capability.into_iter().collect();
218 }
219
220 (self.opts.ensure_capabilities && self.observed.is_empty())
221 .then(|| State::Capability(ImapCapabilityGet::new()))
222 }
223
224 fn wants_id(&mut self) -> Option<State> {
226 let params = self.opts.auto_id.take()?;
227 let wire = (!params.is_empty()).then_some(params);
228 Some(State::Id(ImapServerId::new(ImapServerIdOptions {
229 parameters: wire,
230 })))
231 }
232
233 fn wants_continue(payload: Vec<u8>) -> State {
235 let auth = AuthenticateData::r#continue(payload);
236 let codec = AuthenticateDataCodec::new();
237 State::Continue(ImapSend::new(codec, auth))
238 }
239
240 fn resume_sasl(&mut self, arg: SaslArg<'_>) -> Result<Option<Vec<u8>>, ImapAuthXoauth2Error> {
242 match self.mechanism.resume(arg) {
243 SaslCoroutineState::Yielded(SaslYield::WantsWrite(payload)) => Ok(Some(payload)),
244 SaslCoroutineState::Yielded(SaslYield::WantsRead) => Ok(None),
245 SaslCoroutineState::Complete(result) => result.map(|()| None).map_err(Into::into),
246 }
247 }
248
249 fn no(&mut self, info: String) -> ImapAuthXoauth2Error {
253 match self.mechanism.resume(SaslArg::Done) {
254 SaslCoroutineState::Complete(Err(SaslXoauth2Error::Rejected(err))) => {
255 ImapAuthXoauth2Error::NoWithError { info, err }
256 }
257 _ => ImapAuthXoauth2Error::No(info),
258 }
259 }
260}
261
262impl ImapCoroutine for ImapAuthXoauth2 {
263 type Yield = ImapYield;
264 type Return = Result<Vec<Capability<'static>>, ImapAuthXoauth2Error>;
265
266 fn resume(
267 &mut self,
268 fragmentizer: &mut Fragmentizer,
269 arg: Option<&[u8]>,
270 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
271 loop {
272 match &mut self.state {
273 State::Start => {
274 let payload = match self.resume_sasl(SaslArg::None) {
275 Ok(payload) => payload,
276 Err(err) => return ImapCoroutineState::Complete(Err(err)),
277 };
278
279 let (initial_response, pending) = match payload {
284 Some(payload) if self.opts.initial_request => {
285 (Some(Secret::new(payload.into())), None)
286 }
287 payload => (None, payload),
288 };
289
290 let tag = TagGenerator::new().generate();
291 let body = CommandBody::Authenticate {
292 mechanism: AuthMechanism::XOAuth2,
293 initial_response,
294 };
295 let cmd = Command { tag, body };
296 trace!("send IMAP command {cmd:?}");
297
298 self.state = State::Send {
299 send: ImapSend::new(CommandCodec::new(), cmd),
300 pending,
301 };
302 debug!("{}", self.state);
303 }
304 State::Send { send, pending } => {
305 let out = imap_try!(send, fragmentizer, arg);
306
307 if let Some(bye) = out.bye {
308 let err = ImapAuthXoauth2Error::Bye(bye.text.to_string());
309 return ImapCoroutineState::Complete(Err(err));
310 }
311
312 if let Some(cr) = out.continuation_request {
313 let payload = match pending.take() {
318 Some(payload) => payload,
319 None => {
320 match self.resume_sasl(SaslArg::Input(&extract_challenge(cr))) {
321 Ok(payload) => payload.unwrap_or_default(),
322 Err(err) => return ImapCoroutineState::Complete(Err(err)),
323 }
324 }
325 };
326
327 self.state = Self::wants_continue(payload);
328 debug!("{}", self.state);
329 continue;
330 }
331
332 let inlined = pending.is_none();
337
338 let Some(Tagged { body, .. }) = out.tagged else {
339 let err = ImapAuthXoauth2Error::ExpectedContinuationRequest;
340 return ImapCoroutineState::Complete(Err(err));
341 };
342
343 let code = match body.kind {
344 StatusKind::Ok if inlined => body.code,
345 StatusKind::Ok => {
346 let err = ImapAuthXoauth2Error::UnexpectedOk;
347 return ImapCoroutineState::Complete(Err(err));
348 }
349 StatusKind::No => {
350 let err = self.no(body.text.to_string());
351 return ImapCoroutineState::Complete(Err(err));
352 }
353 StatusKind::Bad => {
354 let err = ImapAuthXoauth2Error::Bad(body.text.to_string());
355 return ImapCoroutineState::Complete(Err(err));
356 }
357 };
358
359 if let Err(err) = self.resume_sasl(SaslArg::Done) {
360 return ImapCoroutineState::Complete(Err(err));
361 }
362
363 if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
364 self.state = next;
365 debug!("{}", self.state);
366 continue;
367 }
368
369 if let Some(next) = self.wants_id() {
370 self.state = next;
371 debug!("{}", self.state);
372 continue;
373 }
374
375 let capability = mem::take(&mut self.observed);
376 return ImapCoroutineState::Complete(Ok(capability));
377 }
378 State::Continue(send) => {
379 let out = imap_try!(send, fragmentizer, arg);
380
381 if let Some(bye) = out.bye {
382 let err = ImapAuthXoauth2Error::Bye(bye.text.to_string());
383 return ImapCoroutineState::Complete(Err(err));
384 }
385
386 if let Some(cr) = out.continuation_request {
387 let payload = match self.resume_sasl(SaslArg::Input(&extract_challenge(cr)))
388 {
389 Ok(payload) => payload.unwrap_or_default(),
390 Err(err) => return ImapCoroutineState::Complete(Err(err)),
391 };
392
393 self.state = Self::wants_continue(payload);
394 debug!("{}", self.state);
395 continue;
396 }
397
398 let Some(Tagged { body, .. }) = out.tagged else {
399 let err = ImapAuthXoauth2Error::MissingTagged;
400 return ImapCoroutineState::Complete(Err(err));
401 };
402
403 let code = match body.kind {
404 StatusKind::Ok => body.code,
405 StatusKind::No => {
406 let err = self.no(body.text.to_string());
407 return ImapCoroutineState::Complete(Err(err));
408 }
409 StatusKind::Bad => {
410 let err = ImapAuthXoauth2Error::Bad(body.text.to_string());
411 return ImapCoroutineState::Complete(Err(err));
412 }
413 };
414
415 if let Err(err) = self.resume_sasl(SaslArg::Done) {
420 return ImapCoroutineState::Complete(Err(err));
421 }
422
423 if let Some(next) = self.wants_capability(code, out.data, out.untagged) {
424 self.state = next;
425 debug!("{}", self.state);
426 continue;
427 }
428
429 if let Some(next) = self.wants_id() {
430 self.state = next;
431 debug!("{}", self.state);
432 continue;
433 }
434
435 let capability = mem::take(&mut self.observed);
436 return ImapCoroutineState::Complete(Ok(capability));
437 }
438 State::Capability(capability) => {
439 self.observed = imap_try!(capability, fragmentizer, arg);
440
441 if let Some(next) = self.wants_id() {
442 self.state = next;
443 debug!("{}", self.state);
444 continue;
445 }
446
447 let capability = mem::take(&mut self.observed);
448 return ImapCoroutineState::Complete(Ok(capability));
449 }
450 State::Id(id) => {
451 imap_try!(id, fragmentizer, arg);
452 let capability = mem::take(&mut self.observed);
453 return ImapCoroutineState::Complete(Ok(capability));
454 }
455 }
456 }
457 }
458}
459
460enum State {
461 Start,
462 Send {
463 send: ImapSend<CommandCodec>,
464 pending: Option<Vec<u8>>,
465 },
466 Continue(ImapSend<AuthenticateDataCodec>),
467 Capability(ImapCapabilityGet),
468 Id(ImapServerId),
469}
470
471impl fmt::Display for State {
472 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
473 match self {
474 Self::Start => f.write_str("start mechanism"),
475 Self::Send { pending, .. } if pending.is_some() => f.write_str("send auth"),
476 Self::Send { .. } => f.write_str("send auth with ir"),
477 Self::Continue(_) => f.write_str("send response"),
478 Self::Capability(_) => f.write_str("fetch capabilities"),
479 Self::Id(_) => f.write_str("send id"),
480 }
481 }
482}
483
484fn extract_challenge(cr: CommandContinuationRequest<'static>) -> Vec<u8> {
485 match cr {
486 CommandContinuationRequest::Basic(basic) => basic.text().to_string().into_bytes(),
487 CommandContinuationRequest::Base64(data) => data.as_ref().to_vec(),
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use core::str;
494
495 use alloc::format;
496
497 use crate::sasl::auth_xoauth2::*;
498
499 #[test]
500 fn ir_success_returns_ok() {
501 let opts = ImapAuthXoauth2Options {
502 initial_request: true,
503 ..Default::default()
504 };
505
506 let mut auth = ImapAuthXoauth2::new("user@example.org", "oauth-token", opts);
507 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
508
509 let bytes = expect_wants_write(&mut auth, &mut frag, None);
510 let line = str::from_utf8(&bytes).expect("utf8 command");
511 let tag = first_word(line);
512 assert!(line.contains("AUTHENTICATE XOAUTH2 "));
513
514 expect_wants_read(&mut auth, &mut frag);
515
516 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
517 expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
518 }
519
520 #[test]
521 fn ir_invalid_token_returns_no_with_error() {
522 let opts = ImapAuthXoauth2Options {
523 initial_request: true,
524 ..Default::default()
525 };
526
527 let mut auth = ImapAuthXoauth2::new("user@example.org", "expired-token", opts);
528 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
529
530 let bytes = expect_wants_write(&mut auth, &mut frag, None);
531 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
532
533 expect_wants_read(&mut auth, &mut frag);
534
535 let (err_json_b64, err_json) = fake_json_error();
536 let challenge = format!("+ {err_json_b64}\r\n");
537 let ack = expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
538 assert_eq!(b"\r\n", &*ack);
539
540 expect_wants_read(&mut auth, &mut frag);
541
542 let reply = format!("{tag} NO SASL authentication failed\r\n");
543 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
544 let ImapAuthXoauth2Error::NoWithError { info, err } = err else {
545 panic!("expected ImapAuthXoauth2Error::NoWithError, got {err:?}");
546 };
547 assert_eq!(info, "SASL authentication failed");
548 assert_eq!(err, err_json);
549 }
550
551 #[test]
552 fn ir_tagged_bad_returns_bad_error() {
553 let opts = ImapAuthXoauth2Options {
554 initial_request: true,
555 ..Default::default()
556 };
557
558 let mut auth = ImapAuthXoauth2::new("user@example.org", "oauth-token", opts);
559 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
560
561 let bytes = expect_wants_write(&mut auth, &mut frag, None);
562 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
563
564 expect_wants_read(&mut auth, &mut frag);
565
566 let reply = format!("{tag} BAD AUTHENTICATE not enabled\r\n");
567 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
568 let ImapAuthXoauth2Error::Bad(text) = err else {
569 panic!("expected ImapAuthXoauth2Error::Bad, got {err:?}");
570 };
571 assert_eq!(text, "AUTHENTICATE not enabled");
572 }
573
574 #[test]
575 fn ir_rejected_token_acknowledged_then_ok_returns_mechanism_error() {
576 let opts = ImapAuthXoauth2Options {
577 initial_request: true,
578 ..Default::default()
579 };
580
581 let mut auth = ImapAuthXoauth2::new("user@example.org", "expired-token", opts);
582 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
583
584 let bytes = expect_wants_write(&mut auth, &mut frag, None);
585 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
586
587 expect_wants_read(&mut auth, &mut frag);
588
589 let (err_json_b64, err_json) = fake_json_error();
590 let challenge = format!("+ {err_json_b64}\r\n");
591 expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
592 expect_wants_read(&mut auth, &mut frag);
593
594 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
598 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
599 let ImapAuthXoauth2Error::Mechanism(SaslXoauth2Error::Rejected(json)) = err else {
600 panic!("expected ImapAuthXoauth2Error::Mechanism, got {err:?}");
601 };
602 assert_eq!(json, err_json);
603 }
604
605 #[test]
606 fn non_ir_success_returns_ok() {
607 let opts = ImapAuthXoauth2Options::default();
608 let mut auth = ImapAuthXoauth2::new("user@example.org", "oauth-token", opts);
609 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
610
611 let bytes = expect_wants_write(&mut auth, &mut frag, None);
612 let line = str::from_utf8(&bytes).expect("utf8 command");
613 let tag = first_word(line);
614 assert!(line.trim_end().ends_with("AUTHENTICATE XOAUTH2"));
615
616 expect_wants_read(&mut auth, &mut frag);
617
618 let creds = expect_wants_write(&mut auth, &mut frag, Some(b"+ \r\n"));
619 assert!(creds.ends_with(b"\r\n"));
620
621 expect_wants_read(&mut auth, &mut frag);
622
623 let reply = format!("{tag} OK AUTHENTICATE completed\r\n");
624 expect_complete_ok(&mut auth, &mut frag, reply.as_bytes());
625 }
626
627 #[test]
628 fn non_ir_invalid_token_returns_no_with_error() {
629 let opts = ImapAuthXoauth2Options::default();
630 let mut auth = ImapAuthXoauth2::new("user@example.org", "expired-token", opts);
631 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
632
633 let bytes = expect_wants_write(&mut auth, &mut frag, None);
634 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command"));
635
636 expect_wants_read(&mut auth, &mut frag);
637 expect_wants_write(&mut auth, &mut frag, Some(b"+ \r\n"));
638 expect_wants_read(&mut auth, &mut frag);
639
640 let (err_json_b64, err_json) = fake_json_error();
641 let challenge = format!("+ {err_json_b64}\r\n");
642 let ack = expect_wants_write(&mut auth, &mut frag, Some(challenge.as_bytes()));
643 assert_eq!(b"\r\n", &*ack);
644
645 expect_wants_read(&mut auth, &mut frag);
646
647 let reply = format!("{tag} NO SASL authentication failed\r\n");
648 let err = expect_complete_err(&mut auth, &mut frag, reply.as_bytes());
649 let ImapAuthXoauth2Error::NoWithError { info, err } = err else {
650 panic!("expected ImapAuthXoauth2Error::NoWithError, got {err:?}");
651 };
652 assert_eq!(info, "SASL authentication failed");
653 assert_eq!(err, err_json);
654 }
655
656 fn expect_wants_write(
657 cor: &mut ImapAuthXoauth2,
658 frag: &mut Fragmentizer,
659 arg: Option<&[u8]>,
660 ) -> Vec<u8> {
661 match cor.resume(frag, arg) {
662 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
663 state => panic!("expected WantsWrite, got {state:?}"),
664 }
665 }
666
667 fn expect_wants_read(cor: &mut ImapAuthXoauth2, frag: &mut Fragmentizer) {
668 match cor.resume(frag, None) {
669 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
670 state => panic!("expected WantsRead, got {state:?}"),
671 }
672 }
673
674 fn expect_complete_ok(cor: &mut ImapAuthXoauth2, frag: &mut Fragmentizer, reply: &[u8]) {
675 match cor.resume(frag, Some(reply)) {
676 ImapCoroutineState::Complete(Ok(_)) => {}
677 state => panic!("expected Complete(Ok), got {state:?}"),
678 }
679 }
680
681 fn expect_complete_err(
682 cor: &mut ImapAuthXoauth2,
683 frag: &mut Fragmentizer,
684 reply: &[u8],
685 ) -> ImapAuthXoauth2Error {
686 match cor.resume(frag, Some(reply)) {
687 ImapCoroutineState::Complete(Err(err)) => err,
688 state => panic!("expected Complete(Err), got {state:?}"),
689 }
690 }
691
692 fn first_word(line: &str) -> &str {
693 line.split_whitespace()
694 .next()
695 .expect("first whitespace-separated token")
696 }
697
698 fn fake_json_error() -> (&'static str, &'static str) {
699 (
700 "eyJzdGF0dXMiOiI0MDEiLCJzY2hlbWVzIjoiQmVhcmVyIiwic2NvcGUiOiJodHRwczovL21haWwuZ29vZ2xlLmNvbS8ifQ==",
701 "{\"status\":\"401\",\"schemes\":\"Bearer\",\"scope\":\"https://mail.google.com/\"}",
702 )
703 }
704}