mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-23 19:50:18 +00:00
[Google OAuth] Support custom Client IDs (bitfireAT/davx5#294)
* Support custom Client IDs * Refactoring --------- Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
parent
ff4af11bcf
commit
ba36d01e11
|
@ -4,11 +4,9 @@
|
|||
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import net.openid.appauth.AuthorizationRequest
|
||||
import net.openid.appauth.AuthorizationService
|
||||
import net.openid.appauth.AuthorizationServiceConfiguration
|
||||
import net.openid.appauth.ResponseTypeValues
|
||||
|
||||
|
@ -27,14 +25,12 @@ object GoogleOAuth {
|
|||
Uri.parse("https://oauth2.googleapis.com/token")
|
||||
)
|
||||
|
||||
fun authRequestBuilder() =
|
||||
fun authRequestBuilder(clientId: String?) =
|
||||
AuthorizationRequest.Builder(
|
||||
serviceConfig,
|
||||
CLIENT_ID,
|
||||
clientId ?: CLIENT_ID,
|
||||
ResponseTypeValues.CODE,
|
||||
Uri.parse(BuildConfig.APPLICATION_ID + ":/oauth/redirect")
|
||||
)
|
||||
|
||||
fun createAuthService(context: Context) = AuthorizationService(context)
|
||||
|
||||
}
|
|
@ -47,6 +47,7 @@ class HttpClient private constructor(
|
|||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface HttpClientEntryPoint {
|
||||
fun authorizationService(): AuthorizationService
|
||||
fun settingsManager(): SettingsManager
|
||||
}
|
||||
|
||||
|
@ -176,7 +177,7 @@ class HttpClient private constructor(
|
|||
certificateAlias = credentials.certificateAlias
|
||||
|
||||
credentials.authState?.let { authState ->
|
||||
val newAuthService = GoogleOAuth.createAuthService(context)
|
||||
val newAuthService = EntryPointAccessors.fromApplication(context, HttpClientEntryPoint::class.java).authorizationService()
|
||||
authService = newAuthService
|
||||
BearerAuthInterceptor.fromAuthState(newAuthService, authState, authStateCallback)?.let { bearerAuthInterceptor ->
|
||||
orig.addNetworkInterceptor(bearerAuthInterceptor)
|
||||
|
|
22
app/src/main/java/at/bitfire/davdroid/network/OAuthModule.kt
Normal file
22
app/src/main/java/at/bitfire/davdroid/network/OAuthModule.kt
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import net.openid.appauth.AuthorizationService
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object OAuthModule {
|
||||
|
||||
@Provides
|
||||
fun authorizationService(@ApplicationContext context: Context): AuthorizationService = AuthorizationService(context)
|
||||
|
||||
}
|
|
@ -13,7 +13,9 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
|
@ -26,11 +28,14 @@ import androidx.compose.material.Card
|
|||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
@ -47,20 +52,30 @@ import at.bitfire.davdroid.log.Logger
|
|||
import at.bitfire.davdroid.network.GoogleOAuth
|
||||
import at.bitfire.davdroid.ui.UiUtils
|
||||
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import net.openid.appauth.AuthState
|
||||
import net.openid.appauth.AuthorizationException
|
||||
import net.openid.appauth.AuthorizationRequest
|
||||
import net.openid.appauth.AuthorizationResponse
|
||||
import net.openid.appauth.AuthorizationService
|
||||
import net.openid.appauth.TokenResponse
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.net.URI
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class GoogleLoginFragment: Fragment() {
|
||||
|
||||
companion object {
|
||||
|
||||
val URI_TESTED_WITH_GOOGLE: Uri = Uri.parse("https://www.davx5.com/tested-with/google")
|
||||
|
||||
fun googleBaseUri(googleAccount: String): URI =
|
||||
URI("https", "www.google.com", "/calendar/dav/$googleAccount/events/", null)
|
||||
|
||||
}
|
||||
|
||||
private val loginModel by activityViewModels<LoginModel>()
|
||||
|
@ -83,7 +98,15 @@ class GoogleLoginFragment: Fragment() {
|
|||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val v = ComposeView(requireActivity())
|
||||
v.setContent {
|
||||
GoogleLogin()
|
||||
GoogleLogin(onLogin = { account, clientId ->
|
||||
loginModel.baseURI = googleBaseUri(account)
|
||||
|
||||
val authRequest = GoogleOAuth.authRequestBuilder(clientId)
|
||||
.setScopes(*GoogleOAuth.SCOPES)
|
||||
.setLoginHint(account)
|
||||
.build()
|
||||
authRequestContract.launch(authRequest)
|
||||
})
|
||||
}
|
||||
|
||||
model.credentials.observe(viewLifecycleOwner) { credentials ->
|
||||
|
@ -100,77 +123,12 @@ class GoogleLoginFragment: Fragment() {
|
|||
}
|
||||
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun GoogleLogin() {
|
||||
MdcTheme {
|
||||
Column(Modifier.padding(8.dp).verticalScroll(rememberScrollState())) {
|
||||
Text(
|
||||
stringResource(R.string.login_type_google),
|
||||
style = MaterialTheme.typography.h5,
|
||||
modifier = Modifier.padding(vertical = 16.dp))
|
||||
@HiltViewModel
|
||||
class Model @Inject constructor(
|
||||
application: Application,
|
||||
val authService: AuthorizationService
|
||||
): AndroidViewModel(application) {
|
||||
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
Text(
|
||||
stringResource(R.string.login_google_guide),
|
||||
modifier = Modifier.padding(vertical = 8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
UiUtils.launchUri(requireActivity(), Uri.parse("https://www.davx5.com/tested-with/google"))
|
||||
},
|
||||
colors = ButtonDefaults.outlinedButtonColors(),
|
||||
modifier = Modifier.wrapContentSize()
|
||||
) {
|
||||
Text(stringResource(R.string.intro_more_info))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val email = remember { mutableStateOf("") }
|
||||
val emailError = remember { mutableStateOf<Boolean>(false) }
|
||||
OutlinedTextField(
|
||||
email.value,
|
||||
singleLine = true,
|
||||
onValueChange = { account ->
|
||||
email.value = account
|
||||
loginModel.baseURI = googleBaseUri(account)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
|
||||
label = { Text(stringResource(R.string.login_google_account)) },
|
||||
isError = emailError.value,
|
||||
placeholder = { Text("example@gmail.com") },
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp)
|
||||
)
|
||||
|
||||
Button({
|
||||
val valid = email.value.orEmpty().contains('@')
|
||||
emailError.value = !valid
|
||||
|
||||
if (valid) {
|
||||
val authRequest = GoogleOAuth.authRequestBuilder()
|
||||
.setScopes(*GoogleOAuth.SCOPES)
|
||||
.setLoginHint(email.value)
|
||||
.build()
|
||||
authRequestContract.launch(authRequest)
|
||||
}
|
||||
}, modifier = Modifier.wrapContentSize()) {
|
||||
Text(stringResource(R.string.login_login))
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(R.string.login_google_disclaimer, getString(R.string.app_name)),
|
||||
style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier.padding(top = 24.dp))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Model(application: Application): AndroidViewModel(application) {
|
||||
|
||||
val authService = GoogleOAuth.createAuthService(getApplication())
|
||||
val credentials = MutableLiveData<Credentials>()
|
||||
|
||||
fun authenticate(resp: AuthorizationResponse) = viewModelScope.launch(Dispatchers.IO) {
|
||||
|
@ -194,4 +152,100 @@ class GoogleLoginFragment: Fragment() {
|
|||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun GoogleLogin(
|
||||
onLogin: (account: String, clientId: String?) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
MdcTheme {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(8.dp)
|
||||
.verticalScroll(rememberScrollState())) {
|
||||
Text(
|
||||
stringResource(R.string.login_type_google),
|
||||
style = MaterialTheme.typography.h5,
|
||||
modifier = Modifier.padding(vertical = 16.dp))
|
||||
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
Row {
|
||||
Image(Icons.Default.Warning, contentDescription = "", modifier = Modifier.padding(top = 8.dp, end = 8.dp, bottom = 8.dp))
|
||||
Text(stringResource(R.string.login_google_see_tested_with))
|
||||
}
|
||||
Text(stringResource(R.string.login_google_unexpected_warnings), modifier = Modifier.padding(vertical = 8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
UiUtils.launchUri(context, GoogleLoginFragment.URI_TESTED_WITH_GOOGLE)
|
||||
},
|
||||
colors = ButtonDefaults.outlinedButtonColors(),
|
||||
modifier = Modifier.wrapContentSize()
|
||||
) {
|
||||
Text(stringResource(R.string.intro_more_info))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val email = remember { mutableStateOf("") }
|
||||
val emailError = remember { mutableStateOf(false) }
|
||||
OutlinedTextField(
|
||||
email.value,
|
||||
singleLine = true,
|
||||
onValueChange = { email.value = it },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
|
||||
label = { Text(stringResource(R.string.login_google_account)) },
|
||||
isError = emailError.value,
|
||||
placeholder = { Text("example@gmail.com") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
)
|
||||
|
||||
val userClientId = remember { mutableStateOf("") }
|
||||
val userClientIdError = remember { mutableStateOf(false) }
|
||||
OutlinedTextField(
|
||||
userClientId.value,
|
||||
singleLine = true,
|
||||
onValueChange = { clientId ->
|
||||
userClientId.value = clientId
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||
label = { Text(stringResource(R.string.login_google_client_id)) },
|
||||
isError = userClientIdError.value,
|
||||
placeholder = { Text("[...].apps.googleusercontent.com") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
)
|
||||
|
||||
Button({
|
||||
val validEmail = email.value.contains('@')
|
||||
emailError.value = !validEmail
|
||||
|
||||
if (validEmail) {
|
||||
val clientId = StringUtils.trimToNull(userClientId.value.trim())
|
||||
onLogin(email.value, clientId)
|
||||
}
|
||||
}, modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.wrapContentSize()) {
|
||||
Text(stringResource(R.string.login_login))
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(R.string.login_google_disclaimer, stringResource(R.string.app_name)),
|
||||
style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier.padding(top = 24.dp))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun PreviewGoogleLogin() {
|
||||
GoogleLogin(onLogin = { _, _ -> })
|
||||
}
|
|
@ -293,8 +293,12 @@
|
|||
<string name="login_no_certificate_found">No certificate found</string>
|
||||
<string name="login_install_certificate">Install certificate</string>
|
||||
<string name="login_type_google">Google Contacts / Calendar</string>
|
||||
<string name="login_google_guide">Please read our guide about the login to Google. There may be unexpected warnings which are explained there.</string>
|
||||
<string name="login_google_see_tested_with">Please see our \"Tested with Google\" page for up-to-date information.</string>
|
||||
<string name="login_google_unexpected_warnings">You may experience unexpected warnings and/or have to create your own client ID.</string>
|
||||
<string name="login_google_account">Google account</string>
|
||||
<string name="login_google_client_id">Client ID (optional)</string>
|
||||
<string name="login_google_client_id_description">You may use your own client ID, in case our does not work.</string>
|
||||
<string name="login_google_client_id_description_link">Show me how!</string>
|
||||
<string name="login_google_disclaimer">%s is not affiliated to, nor has it been authorized, sponsored or otherwise approved by Google LLC.</string>
|
||||
|
||||
<string name="login_configuration_detection">Configuration detection</string>
|
||||
|
|
Loading…
Reference in a new issue