1use 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#[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#[derive(Clone, Debug, Default, Eq, PartialEq)]
114pub struct ImapAuthOauthbearerOptions {
115 pub initial_request: bool,
118 pub ensure_capabilities: bool,
119 pub auto_id: Option<Vec<(IString<'static>, NString<'static>)>>,
120}
121
122pub 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 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}