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