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