Refactor OauthConfiguration to be placed in common module with added OAuthProvder and OAuthProviderSettings

This commit is contained in:
Wolf-Martell Montwé 2023-06-20 12:21:25 +02:00
parent 9e0f1bd815
commit e64d030d22
No known key found for this signature in database
GPG key ID: 6D45B21512ACBF72
17 changed files with 342 additions and 89 deletions

View file

@ -105,8 +105,8 @@ android {
buildConfigField("String", "OAUTH_MICROSOFT_CLIENT_ID", "\"e647013a-ada4-4114-b419-e43d250f99c5\"")
buildConfigField(
"String",
"OAUTH_MICROSOFT_REDIRECT_URI",
"\"msauth://com.fsck.k9/Dx8yUsuhyU3dYYba1aA16Wxu5eM%3D\"",
"OAUTH_MICROSOFT_REDIRECT_URI_ID",
"\"Dx8yUsuhyU3dYYba1aA16Wxu5eM%3D\"",
)
manifestPlaceholders["appAuthRedirectScheme"] = "com.fsck.k9"
@ -137,8 +137,8 @@ android {
buildConfigField("String", "OAUTH_MICROSOFT_CLIENT_ID", "\"e647013a-ada4-4114-b419-e43d250f99c5\"")
buildConfigField(
"String",
"OAUTH_MICROSOFT_REDIRECT_URI",
"\"msauth://com.fsck.k9.debug/VZF2DYuLYAu4TurFd6usQB2JPts%3D\"",
"OAUTH_MICROSOFT_REDIRECT_URI_ID",
"\"VZF2DYuLYAu4TurFd6usQB2JPts%3D\"",
)
manifestPlaceholders["appAuthRedirectScheme"] = "com.fsck.k9.debug"

View file

@ -1,7 +1,7 @@
package com.fsck.k9
import app.k9mail.ui.widget.list.messageListWidgetModule
import com.fsck.k9.auth.createOAuthConfigurationProvider
import com.fsck.k9.auth.createOAuthProviderSettings
import com.fsck.k9.backends.backendsModule
import com.fsck.k9.controller.ControllerExtension
import com.fsck.k9.crypto.EncryptionExtractor
@ -29,7 +29,7 @@ private val mainAppModule = module {
single(named("controllerExtensions")) { emptyList<ControllerExtension>() }
single<EncryptionExtractor> { OpenPgpEncryptionExtractor.newInstance() }
single<StoragePersister> { K9StoragePersister(get()) }
single { createOAuthConfigurationProvider() }
single { createOAuthProviderSettings() }
}
val appModules = listOf(

View file

@ -1,50 +0,0 @@
package com.fsck.k9.auth
import app.k9mail.core.common.oauth.OAuthConfiguration
import app.k9mail.core.common.oauth.OAuthConfigurationProvider
import com.fsck.k9.BuildConfig
fun createOAuthConfigurationProvider(): OAuthConfigurationProvider {
val redirectUriSlash = BuildConfig.APPLICATION_ID + ":/oauth2redirect"
val redirectUriDoubleSlash = BuildConfig.APPLICATION_ID + "://oauth2redirect"
val googleConfig = OAuthConfiguration(
clientId = BuildConfig.OAUTH_GMAIL_CLIENT_ID,
scopes = listOf("https://mail.google.com/"),
authorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth",
tokenEndpoint = "https://oauth2.googleapis.com/token",
redirectUri = redirectUriSlash,
)
return OAuthConfigurationProvider(
configurations = mapOf(
listOf("imap.gmail.com", "imap.googlemail.com", "smtp.gmail.com", "smtp.googlemail.com") to googleConfig,
listOf("imap.mail.yahoo.com", "smtp.mail.yahoo.com") to OAuthConfiguration(
clientId = BuildConfig.OAUTH_YAHOO_CLIENT_ID,
scopes = listOf("mail-w"),
authorizationEndpoint = "https://api.login.yahoo.com/oauth2/request_auth",
tokenEndpoint = "https://api.login.yahoo.com/oauth2/get_token",
redirectUri = redirectUriDoubleSlash,
),
listOf("imap.aol.com", "smtp.aol.com") to OAuthConfiguration(
clientId = BuildConfig.OAUTH_AOL_CLIENT_ID,
scopes = listOf("mail-w"),
authorizationEndpoint = "https://api.login.aol.com/oauth2/request_auth",
tokenEndpoint = "https://api.login.aol.com/oauth2/get_token",
redirectUri = redirectUriDoubleSlash,
),
listOf("outlook.office365.com", "smtp.office365.com") to OAuthConfiguration(
clientId = BuildConfig.OAUTH_MICROSOFT_CLIENT_ID,
scopes = listOf(
"https://outlook.office.com/IMAP.AccessAsUser.All",
"https://outlook.office.com/SMTP.Send",
"offline_access",
),
authorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
tokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
redirectUri = BuildConfig.OAUTH_MICROSOFT_REDIRECT_URI,
),
),
googleConfiguration = googleConfig,
)
}

View file

@ -0,0 +1,20 @@
package com.fsck.k9.auth
import app.k9mail.core.common.oauth.OAuthProvider
import app.k9mail.core.common.oauth.OAuthProviderSettings
import com.fsck.k9.BuildConfig
fun createOAuthProviderSettings(): OAuthProviderSettings {
return OAuthProviderSettings(
applicationId = BuildConfig.APPLICATION_ID,
clientIds = mapOf(
OAuthProvider.AOL to BuildConfig.OAUTH_AOL_CLIENT_ID,
OAuthProvider.GMAIL to BuildConfig.OAUTH_GMAIL_CLIENT_ID,
OAuthProvider.MICROSOFT to BuildConfig.OAUTH_MICROSOFT_CLIENT_ID,
OAuthProvider.YAHOO to BuildConfig.OAUTH_YAHOO_CLIENT_ID,
),
redirectUriIds = mapOf(
OAuthProvider.MICROSOFT to BuildConfig.OAUTH_MICROSOFT_REDIRECT_URI_ID,
),
)
}

View file

@ -1,5 +1,6 @@
package app.k9mail.core.android.common
import app.k9mail.core.android.common.test.externalModule
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.android.ext.koin.androidContext
@ -14,7 +15,10 @@ internal class CoreCommonAndroidModuleKtTest {
@Test
fun `should have a valid di module`() {
koinApplication {
modules(coreCommonAndroidModule)
modules(
externalModule,
coreCommonAndroidModule,
)
androidContext(RuntimeEnvironment.getApplication())
checkModules()
}

View file

@ -1,6 +1,7 @@
package app.k9mail.core.android.common.contact
import app.k9mail.core.android.common.coreCommonAndroidModule
import app.k9mail.core.android.common.test.externalModule
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.android.ext.koin.androidContext
@ -11,13 +12,15 @@ import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
internal class ContactKoinModuleKtTest {
@Test
fun `should have a valid di module`() {
koinApplication {
modules(coreCommonAndroidModule)
modules(contactModule)
modules(
externalModule,
coreCommonAndroidModule,
contactModule,
)
androidContext(RuntimeEnvironment.getApplication())
checkModules()
}

View file

@ -0,0 +1,15 @@
package app.k9mail.core.android.common.test
import app.k9mail.core.common.oauth.OAuthProvider
import app.k9mail.core.common.oauth.OAuthProviderSettings
import org.koin.dsl.module
internal val externalModule = module {
single<OAuthProviderSettings> {
OAuthProviderSettings(
applicationId = "test",
clientIds = OAuthProvider.values().associateWith { "testClientId" },
redirectUriIds = OAuthProvider.values().associateWith { "testRedirectUriId" },
)
}
}

View file

@ -1,9 +1,20 @@
package app.k9mail.core.common
import app.k9mail.core.common.oauth.InMemoryOauthConfigurationProvider
import app.k9mail.core.common.oauth.OAuthConfigurationProvider
import app.k9mail.core.common.oauth.OauthConfigurationFactory
import kotlinx.datetime.Clock
import org.koin.core.module.Module
import org.koin.dsl.module
val coreCommonModule: Module = module {
single<Clock> { Clock.System }
single<OAuthConfigurationProvider> {
InMemoryOauthConfigurationProvider(
configurations = OauthConfigurationFactory.createConfigurations(
settings = get(),
),
)
}
}

View file

@ -0,0 +1,22 @@
package app.k9mail.core.common.oauth
internal class InMemoryOauthConfigurationProvider(
private val configurations: Map<List<String>, OAuthConfiguration>,
) : OAuthConfigurationProvider {
private val hostnameMapping: Map<String, OAuthConfiguration> = buildMap {
for ((hostnames, configuration) in configurations) {
for (hostname in hostnames) {
put(hostname.lowercase(), configuration)
}
}
}
override fun getConfiguration(hostname: String): OAuthConfiguration? {
return hostnameMapping[hostname.lowercase()]
}
override fun isGoogle(hostname: String): Boolean {
return getConfiguration(hostname)?.provider == OAuthProvider.GMAIL
}
}

View file

@ -1,6 +1,7 @@
package app.k9mail.core.common.oauth
data class OAuthConfiguration(
val provider: OAuthProvider,
val clientId: String,
val scopes: List<String>,
val authorizationEndpoint: String,

View file

@ -1,22 +1,8 @@
package app.k9mail.core.common.oauth
class OAuthConfigurationProvider(
private val configurations: Map<List<String>, OAuthConfiguration>,
private val googleConfiguration: OAuthConfiguration,
) {
private val hostnameMapping: Map<String, OAuthConfiguration> = buildMap {
for ((hostnames, configuration) in configurations) {
for (hostname in hostnames) {
put(hostname.lowercase(), configuration)
}
}
}
interface OAuthConfigurationProvider {
fun getConfiguration(hostname: String): OAuthConfiguration? {
return hostnameMapping[hostname.lowercase()]
}
fun getConfiguration(hostname: String): OAuthConfiguration?
fun isGoogle(hostname: String): Boolean {
return getConfiguration(hostname) == googleConfiguration
}
fun isGoogle(hostname: String): Boolean
}

View file

@ -0,0 +1,8 @@
package app.k9mail.core.common.oauth
enum class OAuthProvider {
GMAIL,
MICROSOFT,
YAHOO,
AOL,
}

View file

@ -0,0 +1,31 @@
package app.k9mail.core.common.oauth
data class OAuthProviderSettings(
val applicationId: String,
val clientIds: Map<OAuthProvider, String>,
val redirectUriIds: Map<OAuthProvider, String>,
) {
init {
require(applicationId.isNotBlank()) {
"Application id must be set"
}
require(clientIds.isNotEmpty()) {
"Client ids must be set"
}
for (provider in OAuthProvider.values()) {
require(clientIds[provider].isNullOrBlank().not()) {
"Client id for $provider must be set"
}
}
require(redirectUriIds.isNotEmpty()) {
"Redirect URI ids must be set"
}
require(redirectUriIds[OAuthProvider.MICROSOFT].isNullOrBlank().not()) {
"Microsoft redirect URI id must be set"
}
}
}

View file

@ -0,0 +1,82 @@
package app.k9mail.core.common.oauth
internal object OauthConfigurationFactory {
fun createConfigurations(
settings: OAuthProviderSettings,
): Map<List<String>, OAuthConfiguration> {
return mapOf(
createAolConfiguration(settings),
createGmailConfiguration(settings),
createMicrosoftConfiguration(settings),
createYahooConfiguration(settings),
)
}
private fun createAolConfiguration(
settings: OAuthProviderSettings,
): Pair<List<String>, OAuthConfiguration> {
return listOf("imap.aol.com", "smtp.aol.com") to OAuthConfiguration(
provider = OAuthProvider.AOL,
clientId = settings.clientIds[OAuthProvider.AOL]!!,
scopes = listOf("mail-w"),
authorizationEndpoint = "https://api.login.aol.com/oauth2/request_auth",
tokenEndpoint = "https://api.login.aol.com/oauth2/get_token",
redirectUri = "${settings.applicationId}://oauth2redirect",
)
}
private fun createGmailConfiguration(
settings: OAuthProviderSettings,
): Pair<List<String>, OAuthConfiguration> {
return listOf(
"imap.gmail.com",
"imap.googlemail.com",
"smtp.gmail.com",
"smtp.googlemail.com",
) to OAuthConfiguration(
provider = OAuthProvider.GMAIL,
clientId = settings.clientIds[OAuthProvider.GMAIL]!!,
scopes = listOf("https://mail.google.com/"),
authorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth",
tokenEndpoint = "https://oauth2.googleapis.com/token",
redirectUri = "${settings.applicationId}:/oauth2redirect",
)
}
private fun createMicrosoftConfiguration(
settings: OAuthProviderSettings,
): Pair<List<String>, OAuthConfiguration> {
return listOf(
"imap.mail.yahoo.com",
"smtp.mail.yahoo.com",
) to OAuthConfiguration(
provider = OAuthProvider.MICROSOFT,
clientId = settings.clientIds[OAuthProvider.MICROSOFT]!!,
scopes = listOf(
"https://outlook.office.com/IMAP.AccessAsUser.All",
"https://outlook.office.com/SMTP.Send",
"offline_access",
),
authorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
tokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
redirectUri = "msauth://${settings.applicationId}://${settings.redirectUriIds[OAuthProvider.MICROSOFT]}",
)
}
private fun createYahooConfiguration(
settings: OAuthProviderSettings,
): Pair<List<String>, OAuthConfiguration> {
return listOf(
"imap.mail.yahoo.com",
"smtp.mail.yahoo.com",
) to OAuthConfiguration(
provider = OAuthProvider.YAHOO,
clientId = settings.clientIds[OAuthProvider.YAHOO]!!,
scopes = listOf("mail-w"),
authorizationEndpoint = "https://api.login.yahoo.com/oauth2/request_auth",
tokenEndpoint = "https://api.login.yahoo.com/oauth2/get_token",
redirectUri = "${settings.applicationId}://oauth2redirect",
)
}
}

View file

@ -1,15 +1,37 @@
package app.k9mail.core.common
import app.k9mail.core.common.oauth.OAuthProvider
import app.k9mail.core.common.oauth.OAuthProviderSettings
import org.junit.Test
import org.koin.core.annotation.KoinExperimentalAPI
import org.koin.dsl.koinApplication
import org.koin.dsl.module
import org.koin.test.check.checkModules
import org.koin.test.verify.verify
@OptIn(KoinExperimentalAPI::class)
internal class CoreCommonModuleKtTest {
private val externalModule = module {
single<OAuthProviderSettings> {
OAuthProviderSettings(
applicationId = "test",
clientIds = OAuthProvider.values().associateWith { "testClientId" },
redirectUriIds = OAuthProvider.values().associateWith { "testRedirectUriId" },
)
}
}
@Test
fun `should have a valid di module`() {
coreCommonModule.verify(
extraTypes = listOf(
OAuthProviderSettings::class,
),
)
koinApplication {
modules(coreCommonModule)
modules(externalModule, coreCommonModule)
checkModules()
}
}

View file

@ -0,0 +1,95 @@
package app.k9mail.core.common.oauth
import assertk.assertFailure
import assertk.assertions.hasMessage
import assertk.assertions.isInstanceOf
import org.junit.Test
class OAuthProviderSettingsTest {
@Test
fun `should succeed with all arguments set`() {
OAuthProviderSettings(
applicationId = "test",
clientIds = ALL_CLIENT_IDS,
redirectUriIds = ALL_REDIRECT_URI_IDS,
)
}
@Test
fun `should fail with empty application id`() {
assertFailure {
OAuthProviderSettings(
applicationId = "",
clientIds = emptyMap(),
redirectUriIds = emptyMap(),
)
}.isInstanceOf<IllegalArgumentException>()
.hasMessage("Application id must be set")
}
@Test
fun `should fail with blank application id`() {
assertFailure {
OAuthProviderSettings(
applicationId = " ",
clientIds = emptyMap(),
redirectUriIds = emptyMap(),
)
}.isInstanceOf<IllegalArgumentException>()
.hasMessage("Application id must be set")
}
@Test
fun `should fail for empty clientIds`() {
assertFailure {
OAuthProviderSettings(
applicationId = "test",
clientIds = emptyMap(),
redirectUriIds = emptyMap(),
)
}.isInstanceOf<IllegalArgumentException>()
.hasMessage("Client ids must be set")
}
@Test
fun `should fail with incomplete clientIds`() {
assertFailure {
OAuthProviderSettings(
applicationId = "test",
clientIds = (OAuthProvider.values().toList() - OAuthProvider.GMAIL).associateWith { "test" },
redirectUriIds = emptyMap(),
)
}.isInstanceOf<IllegalArgumentException>()
.hasMessage("Client id for GMAIL must be set")
}
@Test
fun `should fail for empty redirectUriIds`() {
assertFailure {
OAuthProviderSettings(
applicationId = "test",
clientIds = ALL_CLIENT_IDS,
redirectUriIds = emptyMap(),
)
}.isInstanceOf<IllegalArgumentException>()
.hasMessage("Redirect URI ids must be set")
}
@Test
fun `should fail for empty microsoft redirectUriId`() {
assertFailure {
OAuthProviderSettings(
applicationId = "test",
clientIds = ALL_CLIENT_IDS,
redirectUriIds = mapOf(OAuthProvider.GMAIL to "test"),
)
}.isInstanceOf<IllegalArgumentException>()
.hasMessage("Microsoft redirect URI id must be set")
}
companion object {
private val ALL_CLIENT_IDS = OAuthProvider.values().associateWith { "test" }
private val ALL_REDIRECT_URI_IDS = OAuthProvider.values().associateWith { "test" }
}
}

View file

@ -4,6 +4,7 @@ import androidx.test.core.app.ApplicationProvider
import app.k9mail.core.android.testing.RobolectricTest
import app.k9mail.core.common.oauth.OAuthConfiguration
import app.k9mail.core.common.oauth.OAuthConfigurationProvider
import app.k9mail.core.common.oauth.OAuthProvider
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNotNull
@ -11,14 +12,22 @@ import assertk.assertions.isNull
import com.fsck.k9.mail.AuthType
import com.fsck.k9.mail.ConnectionSecurity
import org.junit.Test
import org.mockito.Mockito.mock
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.stub
class ProvidersXmlDiscoveryTest : RobolectricTest() {
private val xmlProvider = ProvidersXmlProvider(ApplicationProvider.getApplicationContext())
private val oAuthConfigurationProvider = createOAuthConfigurationProvider()
private val oAuthConfigurationProvider = mock<OAuthConfigurationProvider>()
private val providersXmlDiscovery = ProvidersXmlDiscovery(xmlProvider, oAuthConfigurationProvider)
@Test
fun discover_withGmailDomain_shouldReturnCorrectSettings() {
oAuthConfigurationProvider.stub {
on { getConfiguration("imap.gmail.com") } doReturn createOAuthConfiguration()
on { getConfiguration("smtp.gmail.com") } doReturn createOAuthConfiguration()
}
val connectionSettings = providersXmlDiscovery.discover("user@gmail.com")
assertThat(connectionSettings).isNotNull()
@ -45,20 +54,14 @@ class ProvidersXmlDiscoveryTest : RobolectricTest() {
assertThat(connectionSettings).isNull()
}
private fun createOAuthConfigurationProvider(): OAuthConfigurationProvider {
val googleConfig = OAuthConfiguration(
private fun createOAuthConfiguration(): OAuthConfiguration {
return OAuthConfiguration(
provider = OAuthProvider.GMAIL,
clientId = "irrelevant",
scopes = listOf("irrelevant"),
authorizationEndpoint = "irrelevant",
tokenEndpoint = "irrelevant",
redirectUri = "irrelevant",
)
return OAuthConfigurationProvider(
configurations = mapOf(
listOf("imap.gmail.com", "smtp.gmail.com") to googleConfig,
),
googleConfiguration = googleConfig,
)
}
}