0
0
mirror of https://github.com/thunderbird/thunderbird-android.git synced 2024-09-20 04:02:14 +02:00

Add support for the OAUTHBEARER SASL method (SMTP)

This commit is contained in:
cketti 2022-06-05 22:36:18 +02:00
parent d3be6e249b
commit bbd104f38a
2 changed files with 84 additions and 17 deletions

View File

@ -24,6 +24,7 @@ import com.fsck.k9.mail.oauth.OAuth2TokenProvider
import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser
import com.fsck.k9.mail.ssl.TrustedSocketFactory
import com.fsck.k9.mail.transport.smtp.SmtpHelloResponse.Hello
import com.fsck.k9.sasl.buildOAuthBearerInitialClientResponse
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.IOException
@ -61,7 +62,7 @@ class SmtpTransport(
private var is8bitEncodingAllowed = false
private var isEnhancedStatusCodesProvided = false
private var largestAcceptableMessage = 0
private var retryXoauthWithNewToken = false
private var retryOAuthWithNewToken = false
private var isPipeliningSupported = false
private val logger: SmtpLogger = object : SmtpLogger {
@ -157,6 +158,7 @@ class SmtpTransport(
var authCramMD5Supported = false
var authExternalSupported = false
var authXoauth2Supported = false
var authOAuthBearerSupported = false
val saslMechanisms = extensions["AUTH"]
if (saslMechanisms != null) {
authLoginSupported = saslMechanisms.contains("LOGIN")
@ -164,6 +166,7 @@ class SmtpTransport(
authCramMD5Supported = saslMechanisms.contains("CRAM-MD5")
authExternalSupported = saslMechanisms.contains("EXTERNAL")
authXoauth2Supported = saslMechanisms.contains("XOAUTH2")
authOAuthBearerSupported = saslMechanisms.contains("OAUTHBEARER")
}
parseOptionalSizeValue(extensions["SIZE"])
@ -190,10 +193,14 @@ class SmtpTransport(
}
}
AuthType.XOAUTH2 -> {
if (authXoauth2Supported && oauthTokenProvider != null) {
saslXoauth2()
if (oauthTokenProvider == null) {
throw MessagingException("No OAuth2TokenProvider available.")
} else if (authOAuthBearerSupported) {
saslOAuth(OAuthMethod.OAUTHBEARER)
} else if (authXoauth2Supported) {
saslOAuth(OAuthMethod.XOAUTH2)
} else {
throw MessagingException("Authentication method XOAUTH2 is unavailable.")
throw MessagingException("Server doesn't support SASL OAUTHBEARER or XOAUTH2.")
}
}
AuthType.EXTERNAL -> {
@ -550,10 +557,10 @@ class SmtpTransport(
}
}
private fun saslXoauth2() {
retryXoauthWithNewToken = true
private fun saslOAuth(method: OAuthMethod) {
retryOAuthWithNewToken = true
try {
attemptXoauth2(username)
attempOAuth(method, username)
} catch (negativeResponse: NegativeSmtpReplyException) {
if (negativeResponse.replyCode != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
throw negativeResponse
@ -561,10 +568,10 @@ class SmtpTransport(
oauthTokenProvider!!.invalidateToken()
if (!retryXoauthWithNewToken) {
if (!retryOAuthWithNewToken) {
handlePermanentFailure(negativeResponse)
} else {
handleTemporaryFailure(username, negativeResponse)
handleTemporaryFailure(method, username, negativeResponse)
}
}
}
@ -573,13 +580,17 @@ class SmtpTransport(
throw AuthenticationFailedException(negativeResponse.message!!, negativeResponse)
}
private fun handleTemporaryFailure(username: String, negativeResponseFromOldToken: NegativeSmtpReplyException) {
private fun handleTemporaryFailure(
method: OAuthMethod,
username: String,
negativeResponseFromOldToken: NegativeSmtpReplyException
) {
// Token was invalid. We could avoid this double check if we had a reasonable chance of knowing if a token was
// invalid before use (e.g. due to expiry). But we don't. This is the intended behaviour per AccountManager.
Timber.v(negativeResponseFromOldToken, "Authentication exception, re-trying with new token")
try {
attemptXoauth2(username)
attempOAuth(method, username)
} catch (negativeResponseFromNewToken: NegativeSmtpReplyException) {
if (negativeResponseFromNewToken.replyCode != SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
throw negativeResponseFromNewToken
@ -593,14 +604,14 @@ class SmtpTransport(
}
}
private fun attemptXoauth2(username: String) {
private fun attempOAuth(method: OAuthMethod, username: String) {
val token = oauthTokenProvider!!.getToken(OAuth2TokenProvider.OAUTH2_TIMEOUT.toLong())
val authString = Authentication.computeXoauth(username, token)
val authString = method.buildInitialClientResponse(username, token)
val response = executeSensitiveCommand("AUTH XOAUTH2 %s", authString)
val response = executeSensitiveCommand("%s %s", method.command, authString)
if (response.replyCode == SMTP_CONTINUE_REQUEST) {
val replyText = response.joinedText
retryXoauthWithNewToken = XOAuth2ChallengeParser.shouldRetry(replyText, host)
retryOAuthWithNewToken = XOAuth2ChallengeParser.shouldRetry(replyText, host)
// Per Google spec, respond to challenge with empty response
executeCommand("")
@ -622,3 +633,23 @@ class SmtpTransport(
}
}
}
private enum class OAuthMethod {
XOAUTH2 {
override val command = "AUTH XOAUTH2"
override fun buildInitialClientResponse(username: String, token: String): String {
return Authentication.computeXoauth(username, token)
}
},
OAUTHBEARER {
override val command = "AUTH OAUTHBEARER"
override fun buildInitialClientResponse(username: String, token: String): String {
return buildOAuthBearerInitialClientResponse(username, token)
}
};
abstract val command: String
abstract fun buildInitialClientResponse(username: String, token: String): String
}

View File

@ -170,6 +170,42 @@ class SmtpTransportTest {
server.verifyInteractionCompleted()
}
@Test
fun `open() with OAUTHBEARER method`() {
val server = MockSmtpServer().apply {
output("220 localhost Simple Mail Transfer Service Ready")
expect("EHLO [127.0.0.1]")
output("250-localhost Hello client.localhost")
output("250 AUTH OAUTHBEARER")
expect("AUTH OAUTHBEARER bixhPXVzZXIsAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=")
output("235 2.7.0 Authentication successful")
}
val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2)
transport.open()
server.verifyConnectionStillOpen()
server.verifyInteractionCompleted()
}
@Test
fun `open() with OAUTHBEARER method when XOAUTH2 method is also available`() {
val server = MockSmtpServer().apply {
output("220 localhost Simple Mail Transfer Service Ready")
expect("EHLO [127.0.0.1]")
output("250-localhost Hello client.localhost")
output("250 AUTH XOAUTH2 OAUTHBEARER")
expect("AUTH OAUTHBEARER bixhPXVzZXIsAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE=")
output("235 2.7.0 Authentication successful")
}
val transport = startServerAndCreateSmtpTransport(server, authenticationType = AuthType.XOAUTH2)
transport.open()
server.verifyConnectionStillOpen()
server.verifyInteractionCompleted()
}
@Test
fun `open() with XOAUTH2 extension`() {
val server = MockSmtpServer().apply {
@ -386,7 +422,7 @@ class SmtpTransportTest {
transport.open()
fail("Exception expected")
} catch (e: MessagingException) {
assertThat(e).hasMessageThat().isEqualTo("Authentication method XOAUTH2 is unavailable.")
assertThat(e).hasMessageThat().isEqualTo("Server doesn't support SASL OAUTHBEARER or XOAUTH2.")
}
server.verifyConnectionClosed()
@ -546,7 +582,7 @@ class SmtpTransportTest {
output("250-smtp.gmail.com at your service, [86.147.34.216]")
output("250-SIZE 35882577")
output("250-8BITMIME")
output("250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH")
output("250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN XOAUTH")
output("250-ENHANCEDSTATUSCODES")
output("250-PIPELINING")
output("250-CHUNKING")