Repo Created

This commit is contained in:
Fr4nz D13trich 2025-11-15 17:44:12 +01:00
parent eb305e2886
commit a8c22c65db
4784 changed files with 329907 additions and 2 deletions

View file

@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
dependencies {
api project(':firebase-auth')
implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation project(':play-services-base-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"
implementation "com.android.volley:volley:$volleyVersion"
}
android {
namespace "org.microg.gms.firebase.auth.core"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = 1.8
}
}

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application>
<service android:name="org.microg.gms.firebase.auth.FirebaseAuthService">
<intent-filter>
<action android:name="com.google.firebase.auth.api.gms.service.START" />
</intent-filter>
</service>
<activity
android:name="org.microg.gms.firebase.auth.ReCaptchaActivity"
android:exported="false"
android:process=":ui"
android:theme="@style/Theme.AppCompat.Light.Dialog.Alert.NoActionBar" />
<service
android:name="org.microg.gms.firebase.auth.ReCaptchaOverlayService"
android:exported="false"
android:process=":ui" />
</application>
</manifest>

View file

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html style="margin: 5px">
<body style="margin: 5px">
<div id='recaptcha-container'></div>
<div style="padding-top: 150px">
<svg xmlns="http://www.w3.org/2000/svg"
style="margin: auto; background: none; display: block; shape-rendering: auto;" width="64px" height="64px"
viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<rect x="17.5" y="20.2285" width="15" height="59.543" fill="#000000">
<animate attributeName="y" repeatCount="indefinite" dur="1s" calcMode="spline" keyTimes="0;0.5;1"
values="10;30;30" keySplines="0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.2s"></animate>
<animate attributeName="height" repeatCount="indefinite" dur="1s" calcMode="spline" keyTimes="0;0.5;1"
values="80;40;40" keySplines="0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.2s"></animate>
</rect>
<rect x="42.5" y="30" width="15" height="40" fill="#000000">
<animate attributeName="y" repeatCount="indefinite" dur="1s" calcMode="spline" keyTimes="0;0.5;1"
values="15;30;30" keySplines="0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.1s"></animate>
<animate attributeName="height" repeatCount="indefinite" dur="1s" calcMode="spline" keyTimes="0;0.5;1"
values="70;40;40" keySplines="0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.1s"></animate>
</rect>
<rect x="67.5" y="30" width="15" height="40" fill="#000000">
<animate attributeName="y" repeatCount="indefinite" dur="1s" calcMode="spline" keyTimes="0;0.5;1"
values="15;30;30" keySplines="0 0.5 0.5 1;0 0.5 0.5 1"></animate>
<animate attributeName="height" repeatCount="indefinite" dur="1s" calcMode="spline" keyTimes="0;0.5;1"
values="70;40;40" keySplines="0 0.5 0.5 1;0 0.5 0.5 1"></animate>
</rect>
</svg>
</div>
<!-- Firebase App (the core Firebase SDK) is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/7.22.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.22.0/firebase-auth.js"></script>
<script type="text/javascript">
var firebaseConfig = {
apiKey: "%apikey%"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
window.onload = () => {
window.recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container', {
'size': 'invisible',
'callback': function (response) {
MyCallback.onReCaptchaToken(response);
}
});
recaptchaVerifier.verify();
}
</script>
</body>
</html>

View file

@ -0,0 +1,663 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.firebase.auth
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build.VERSION.SDK_INT
import android.os.Handler
import android.os.Parcel
import android.provider.Telephony
import android.telephony.SmsMessage
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
import com.google.android.gms.common.internal.GetServiceRequest
import com.google.android.gms.common.internal.IGmsCallbacks
import com.google.firebase.auth.ActionCodeSettings
import com.google.firebase.auth.EmailAuthCredential
import com.google.firebase.auth.PhoneAuthCredential
import com.google.firebase.auth.UserProfileChangeRequest
import com.google.firebase.auth.api.internal.*
import org.json.JSONArray
import org.json.JSONObject
import org.microg.gms.BaseService
import org.microg.gms.common.GmsService
import org.microg.gms.common.PackageUtils
import org.microg.gms.utils.digest
import org.microg.gms.utils.getCertificates
private const val TAG = "GmsFirebaseAuth"
fun JSONObject.getStringOrNull(key: String) = if (has(key)) getString(key) else null
fun JSONObject.getJSONArrayOrNull(key: String) = if (has(key)) getJSONArray(key) else null
fun JSONArray?.orEmpty() = this ?: JSONArray()
fun JSONObject.getJSONArrayLength(key: String) = getJSONArrayOrNull(key).orEmpty().length()
private val ActionCodeSettings.requestTypeAsString: String
get() = when (requestType) {
1 -> "PASSWORD_RESET"
2 -> "OLD_EMAIL_AGREE"
3 -> "NEW_EMAIL_ACCEPT"
4 -> "VERIFY_EMAIL"
5 -> "RECOVER_EMAIL"
6 -> "EMAIL_SIGNIN"
7 -> "VERIFY_AND_CHANGE_EMAIL"
8 -> "REVERT_SECOND_FACTOR_ADDITION"
else -> "OOB_REQ_TYPE_UNSPECIFIED"
}
private val UserProfileChangeRequest.deleteAttributeList: List<String>
get() {
val list = arrayListOf<String>()
if (shouldRemoveDisplayName) list.add("DISPLAY_NAME")
if (shouldRemovePhotoUri) list.add("PHOTO_URL")
return list
}
private fun Intent.getSmsMessages(): Array<SmsMessage> {
return if (SDK_INT >= 19) {
Telephony.Sms.Intents.getMessagesFromIntent(this)
} else {
(getSerializableExtra("pdus") as? Array<ByteArray>)?.map { SmsMessage.createFromPdu(it) }.orEmpty().toTypedArray()
}
}
class FirebaseAuthService : BaseService(TAG, GmsService.FIREBASE_AUTH) {
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService?) {
PackageUtils.getAndCheckCallingPackage(this, request.packageName)
val apiKey = request.extras?.getString(Constants.EXTRA_API_KEY)
val libraryVersion = request.extras?.getString(Constants.EXTRA_LIBRARY_VERSION)
if (apiKey == null) {
callback.onPostInitComplete(CommonStatusCodes.DEVELOPER_ERROR, null, null)
} else {
callback.onPostInitComplete(0, FirebaseAuthServiceImpl(this, lifecycle, request.packageName, libraryVersion, apiKey).asBinder(), null)
}
}
}
class FirebaseAuthServiceImpl(private val context: Context, override val lifecycle: Lifecycle, private val packageName: String, private val libraryVersion: String?, private val apiKey: String) : IFirebaseAuthService.Stub(), LifecycleOwner {
private val client by lazy { IdentityToolkitClient(context, apiKey, packageName, context.packageManager.getCertificates(packageName).firstOrNull()?.digest("SHA1")) }
private var authorizedDomain: String? = null
private suspend fun getAuthorizedDomain(): String {
authorizedDomain?.let { return it }
val authorizedDomain = try {
client.getProjectConfig().getJSONArray("authorizedDomains").getString(0)
} catch (e: Exception) {
Log.w(TAG, e)
"localhost"
}
this.authorizedDomain = authorizedDomain
return authorizedDomain
}
private suspend fun refreshTokenResponse(cachedState: String): GetTokenResponse {
var tokenResponse = GetTokenResponse.parseJson(cachedState)
if (System.currentTimeMillis() + 300000L < tokenResponse.issuedAt + tokenResponse.expiresIn * 1000) {
return tokenResponse
}
return client.getTokenByRefreshToken(tokenResponse.refreshToken).toGetTokenResponse()
}
private fun JSONObject.toGetTokenResponse() = GetTokenResponse().apply {
refreshToken = getStringOrNull("refresh_token")
accessToken = getStringOrNull("access_token")
expiresIn = getStringOrNull("expires_in")?.toLong()
tokenType = getStringOrNull("token_type")
}
private fun JSONObject.toGetAccountInfoUser(): GetAccountInfoUser = GetAccountInfoUser().apply {
localId = getStringOrNull("localId")
email = getStringOrNull("email")
isEmailVerified = optBoolean("emailVerified")
displayName = getStringOrNull("displayName")
photoUrl = getStringOrNull("photoUrl")
for (i in 0 until getJSONArrayLength("providerUserInfo")) {
getJSONArray("providerUserInfo").getJSONObject(i).run {
providerInfoList.providerUserInfos.add(ProviderUserInfo().apply {
federatedId = getStringOrNull("federatedId")
displayName = getStringOrNull("displayName")
photoUrl = getStringOrNull("photoUrl")
providerId = getStringOrNull("providerId")
phoneNumber = getStringOrNull("phoneNumber")
email = getStringOrNull("email")
rawUserInfo = this@run.toString()
})
}
}
password = getStringOrNull("rawPassword")
phoneNumber = getStringOrNull("phoneNumber")
creationTimestamp = getStringOrNull("createdAt")?.toLong() ?: 0L
lastSignInTimestamp = getStringOrNull("lastLoginAt")?.toLong() ?: 0L
}
private fun JSONObject.toCreateAuthUriResponse(): CreateAuthUriResponse = CreateAuthUriResponse().apply {
authUri = getStringOrNull("authUri")
isRegistered = optBoolean("registered")
providerId = getStringOrNull("providerId")
isForExistingProvider = optBoolean("forExistingProvider")
for (i in 0 until getJSONArrayLength("allProviders")) {
stringList.values.add(getJSONArray("allProviders").getString(i))
}
for (i in 0 until getJSONArrayLength("signinMethods")) {
signInMethods.add(getJSONArray("signinMethods").getString(i))
}
}
override fun applyActionCode(request: ApplyActionCodeAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: applyActionCode")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun applyActionCodeCompat(code: String?, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: applyActionCodeCompat")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun changeEmail(request: ChangeEmailAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: changeEmail")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun changeEmailCompat(cachedState: String?, email: String?, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: changeEmailCompat")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun changePassword(request: ChangePasswordAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: changePassword")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun changePasswordCompat(cachedState: String?, password: String?, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: changePasswordCompat")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun checkActionCode(request: CheckActionCodeAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: checkActionCode")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun checkActionCodeCompat(code: String?, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: checkActionCodeCompat")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun confirmPasswordReset(request: ConfirmPasswordResetAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: confirmPasswordReset")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun confirmPasswordResetCompat(code: String?, newPassword: String?, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: confirmPasswordResetCompat")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun createUserWithEmailAndPassword(request: CreateUserWithEmailAndPasswordAidlRequest, callbacks: IFirebaseAuthCallbacks) {
lifecycleScope.launchWhenStarted {
Log.d(TAG, "createUserWithEmailAndPassword")
try {
val tokenResult = client.signupNewUser(email = request.email, password = request.password, tenantId = request.tenantId)
val idToken = tokenResult.getString("idToken")
val refreshToken = tokenResult.getString("refreshToken")
val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse()
val accountInfoResult = client.getAccountInfo(idToken = idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser().apply { this.isNewUser = true }
Log.d(TAG, "callback: onGetTokenResponseAndUser")
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
} catch (e: Exception) {
Log.w(TAG, "callback: onFailure", e)
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
}
}
}
override fun createUserWithEmailAndPasswordCompat(email: String?, password: String?, callbacks: IFirebaseAuthCallbacks) {
createUserWithEmailAndPassword(CreateUserWithEmailAndPasswordAidlRequest().apply { this.email = email; this.password = password }, callbacks)
}
override fun delete(request: DeleteAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: delete")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun deleteCompat(cachedState: String?, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: deleteCompat")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun finalizeMfaEnrollment(request: FinalizeMfaEnrollmentAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: finalizeMfaEnrollment")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun finalizeMfaSignIn(request: FinalizeMfaSignInAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: finalizeMfaSignIn")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun getAccessToken(request: GetAccessTokenAidlRequest, callbacks: IFirebaseAuthCallbacks) {
lifecycleScope.launchWhenStarted {
Log.d(TAG, "getAccessToken")
try {
callbacks.onGetTokenResponse(client.getTokenByRefreshToken(request.refreshToken).toGetTokenResponse())
} catch (e: Exception) {
Log.w(TAG, "callback: onFailure", e)
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
}
}
}
override fun getAccessTokenCompat(refreshToken: String?, callbacks: IFirebaseAuthCallbacks) {
getAccessToken(GetAccessTokenAidlRequest().apply { this.refreshToken = refreshToken }, callbacks)
}
override fun getProvidersForEmail(request: GetProvidersForEmailAidlRequest, callbacks: IFirebaseAuthCallbacks) {
lifecycleScope.launchWhenStarted {
Log.d(TAG, "getProvidersForEmail")
try {
callbacks.onCreateAuthUriResponse(client.createAuthUri(identifier = request.email, tenantId = request.tenantId).toCreateAuthUriResponse())
} catch (e: Exception) {
Log.w(TAG, "callback: onFailure", e)
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
}
}
}
override fun getProvidersForEmailCompat(email: String?, callbacks: IFirebaseAuthCallbacks) {
getProvidersForEmail(GetProvidersForEmailAidlRequest().apply { this.email = email }, callbacks)
}
override fun linkEmailAuthCredential(request: LinkEmailAuthCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) {
lifecycleScope.launchWhenStarted {
Log.d(TAG, "linkEmailAuthCredential")
try {
val getTokenResponse = refreshTokenResponse(request.cachedState)
val accountInfoResult = client.getAccountInfo(idToken = getTokenResponse.accessToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser()
val setAccountInfo = client.setAccountInfo(idToken = getTokenResponse.accessToken, localId = accountInfoResult.localId, email = request.email, password = request.password).toGetAccountInfoUser()
accountInfoResult.email = setAccountInfo.email
accountInfoResult.isEmailVerified = setAccountInfo.isEmailVerified
accountInfoResult.providerInfoList = setAccountInfo.providerInfoList
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
} catch (e: Exception) {
Log.w(TAG, "callback: onFailure", e)
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
}
}
}
override fun linkEmailAuthCredentialCompat(email: String?, password: String?, cachedState: String?, callbacks: IFirebaseAuthCallbacks) {
linkEmailAuthCredential(LinkEmailAuthCredentialAidlRequest().apply { this.email = email; this.password = password; this.cachedState = cachedState }, callbacks)
}
override fun linkFederatedCredential(request: LinkFederatedCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: linkFederatedCredential")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun linkFederatedCredentialCompat(cachedState: String?, verifyAssertionRequest: VerifyAssertionRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: linkFederatedCredentialCompat")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun linkPhoneAuthCredential(request: LinkPhoneAuthCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: linkPhoneAuthCredential")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun linkPhoneAuthCredentialCompat(cachedState: String?, credential: PhoneAuthCredential?, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: linkPhoneAuthCredentialCompat")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun reload(request: ReloadAidlRequest, callbacks: IFirebaseAuthCallbacks) {
lifecycleScope.launchWhenStarted {
try {
Log.d(TAG, "reload")
val getTokenResponse = refreshTokenResponse(request.cachedState)
val accountInfoResult = client.getAccountInfo(idToken = getTokenResponse.accessToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser()
Log.d(TAG, "callback: onGetTokenResponseAndUser")
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
} catch (e: Exception) {
Log.w(TAG, "callback: onFailure", e)
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
}
}
}
override fun reloadCompat(cachedState: String?, callbacks: IFirebaseAuthCallbacks) {
reload(ReloadAidlRequest().apply { this.cachedState = cachedState }, callbacks)
}
override fun sendEmailVerification(request: SendEmailVerificationWithSettingsAidlRequest, callbacks: IFirebaseAuthCallbacks) {
lifecycleScope.launchWhenStarted {
try {
Log.d(TAG, "sendEmailVerification")
client.getOobConfirmationCode(
requestType = "VERIFY_EMAIL",
idToken = request.token,
iOSBundleId = request.settings?.iOSBundle,
iOSAppStoreId = request.settings?.iOSAppStoreId,
continueUrl = request.settings?.url,
androidInstallApp = request.settings?.androidInstallApp,
androidMinimumVersion = request.settings?.androidMinimumVersion,
androidPackageName = request.settings?.androidPackageName,
canHandleCodeInApp = request.settings?.handleCodeInApp
)
callbacks.onEmailVerificationResponse()
} catch (e: Exception) {
Log.w(TAG, "callback: onFailure", e)
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
}
}
}
override fun sendEmailVerificationCompat(token: String?, actionCodeSettings: ActionCodeSettings?, callbacks: IFirebaseAuthCallbacks) {
sendEmailVerification(SendEmailVerificationWithSettingsAidlRequest().apply { this.token = token; this.settings = actionCodeSettings }, callbacks)
}
override fun sendVerificationCode(request: SendVerificationCodeAidlRequest, callbacks: IFirebaseAuthCallbacks) {
lifecycleScope.launchWhenStarted {
try {
Log.d(TAG, "sendVerificationCode")
val reCaptchaToken = when {
request.request.recaptchaToken != null -> request.request.recaptchaToken
ReCaptchaOverlayService.isSupported(context) -> ReCaptchaOverlayService.awaitToken(context, apiKey, getAuthorizedDomain())
ReCaptchaActivity.isSupported(context) -> ReCaptchaActivity.awaitToken(context, apiKey, getAuthorizedDomain())
else -> throw RuntimeException("No recaptcha token available")
}
var sessionInfo: String? = null
var registered = true
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
var smsCode: String? = null
for (message in intent.getSmsMessages()) {
smsCode = Regex("\\b([0-9]{6})\\b").find(message.messageBody)?.groups?.get(1)?.value
?: continue
Log.d(TAG, "Received SMS verification code: $smsCode")
break
}
if (smsCode == null) return
registered = false
context.unregisterReceiver(this)
try {
callbacks.onVerificationCompletedResponse(PhoneAuthCredential().apply {
this.phoneNumber = request.request.phoneNumber
this.sessionInfo = sessionInfo
this.smsCode = smsCode
})
Log.d(TAG, "callback: onVerificationCompletedResponse")
} catch (e: Exception) {
Log.w(TAG, e)
}
}
}
context.registerReceiver(receiver, IntentFilter("android.provider.Telephony.SMS_RECEIVED"))
var timeout = request.request.timeoutInSeconds * 1000L
if (timeout <= 0L) timeout = 120000L
Handler().postDelayed({
if (registered) {
Log.d(TAG, "Waited ${timeout}ms for verification code SMS, timeout.")
context.unregisterReceiver(receiver)
callbacks.onVerificationAutoTimeOut(sessionInfo)
Log.d(TAG, "callback: onVerificationAutoTimeOut")
}
}, timeout)
sessionInfo = client.sendVerificationCode(phoneNumber = request.request.phoneNumber, reCaptchaToken = reCaptchaToken).getString("sessionInfo")
callbacks.onSendVerificationCodeResponse(sessionInfo)
Log.d(TAG, "callback: onSendVerificationCodeResponse")
} catch (e: Exception) {
Log.w(TAG, "callback: onFailure", e)
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
}
}
}
override fun sendVerificationCodeCompat(request: SendVerificationCodeRequest, callbacks: IFirebaseAuthCallbacks) {
sendVerificationCode(SendVerificationCodeAidlRequest().apply { this.request = request }, callbacks)
}
override fun sendGetOobConfirmationCodeEmail(request: SendGetOobConfirmationCodeEmailAidlRequest, callbacks: IFirebaseAuthCallbacks) {
lifecycleScope.launchWhenStarted {
try {
Log.d(TAG, "sendGetOobConfirmationCodeEmail")
client.getOobConfirmationCode(
requestType = request.settings?.requestTypeAsString ?: "OOB_REQ_TYPE_UNSPECIFIED",
email = request.email,
iOSBundleId = request.settings?.iOSBundle,
iOSAppStoreId = request.settings?.iOSAppStoreId,
continueUrl = request.settings?.url,
androidInstallApp = request.settings?.androidInstallApp,
androidMinimumVersion = request.settings?.androidMinimumVersion,
androidPackageName = request.settings?.androidPackageName,
canHandleCodeInApp = request.settings?.handleCodeInApp
)
Log.d(TAG, "callback: onResetPasswordResponse")
callbacks.onResetPasswordResponse(null)
} catch (e: Exception) {
Log.w(TAG, "callback: onFailure", e)
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
}
}
}
override fun sendGetOobConfirmationCodeEmailCompat(email: String?, actionCodeSettings: ActionCodeSettings?, callbacks: IFirebaseAuthCallbacks) {
sendGetOobConfirmationCodeEmail(SendGetOobConfirmationCodeEmailAidlRequest().apply { this.email = email; this.settings = actionCodeSettings }, callbacks)
}
override fun setFirebaseUiVersion(request: SetFirebaseUiVersionAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: setFirebaseUiVersion")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun setFirebaseUIVersionCompat(firebaseUiVersion: String?, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: setFirebaseUIVersionCompat")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun signInAnonymously(request: SignInAnonymouslyAidlRequest, callbacks: IFirebaseAuthCallbacks) {
lifecycleScope.launchWhenStarted {
Log.d(TAG, "signInAnonymously")
try {
val tokenResult = client.signupNewUser(tenantId = request.tenantId)
val idToken = tokenResult.getString("idToken")
val refreshToken = tokenResult.getString("refreshToken")
val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse()
val accountInfoResult = client.getAccountInfo(idToken = idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser().apply { this.isNewUser = true }
Log.d(TAG, "callback: onGetTokenResponseAndUser")
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
} catch (e: Exception) {
Log.w(TAG, "callback: onFailure", e)
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
}
}
}
override fun signInAnonymouslyCompat(callbacks: IFirebaseAuthCallbacks) {
signInAnonymously(SignInAnonymouslyAidlRequest(), callbacks)
}
override fun signInWithCredential(request: SignInWithCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) {
lifecycleScope.launchWhenStarted {
Log.d(TAG, "signInWithCredential request: ${request.request}")
try {
val tokenResult = client.verifyAssertion(request.request.requestUri, request.request.postBody, request.request.returnSecureToken, request.request.returnIdpCredential)
Log.d(TAG, "signInWithCredential callback: $tokenResult ")
val idToken = tokenResult.getString("idToken")
val refreshToken = tokenResult.getString("refreshToken")
val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse()
val accountInfoResult = client.getAccountInfo(idToken = idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser()
Log.d(TAG, "signInWithCredential callback: onGetTokenResponseAndUser")
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
} catch (e: Exception) {
Log.w(TAG, "signInWithCredential callback: onFailure", e)
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
}
}
}
override fun signInWithCredentialCompat(verifyAssertionRequest: VerifyAssertionRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "signInWithCredentialCompat verifyAssertionRequest: $verifyAssertionRequest")
signInWithCredential(SignInWithCredentialAidlRequest(verifyAssertionRequest), callbacks)
}
override fun signInWithCustomToken(request: SignInWithCustomTokenAidlRequest, callbacks: IFirebaseAuthCallbacks) {
lifecycleScope.launchWhenStarted {
Log.d(TAG, "signInWithCustomToken")
try {
val tokenResult = client.verifyCustomToken(token = request.token)
val idToken = tokenResult.getString("idToken")
val refreshToken = tokenResult.getString("refreshToken")
val isNewUser = tokenResult.optBoolean("isNewUser")
val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse()
val accountInfoResult = client.getAccountInfo(idToken = idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser().apply { this.isNewUser = isNewUser }
Log.d(TAG, "callback: onGetTokenResponseAndUser")
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
} catch (e: Exception) {
Log.w(TAG, "callback: onFailure", e)
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
}
}
}
override fun signInWithCustomTokenCompat(token: String, callbacks: IFirebaseAuthCallbacks) {
signInWithCustomToken(SignInWithCustomTokenAidlRequest().apply { this.token = token }, callbacks)
}
override fun signInWithEmailAndPassword(request: SignInWithEmailAndPasswordAidlRequest, callbacks: IFirebaseAuthCallbacks) {
lifecycleScope.launchWhenStarted {
Log.d(TAG, "signInWithEmailAndPassword")
try {
val tokenResult = client.verifyPassword(email = request.email, password = request.password, tenantId = request.tenantId)
val idToken = tokenResult.getString("idToken")
val refreshToken = tokenResult.getString("refreshToken")
val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse()
val accountInfoResult = client.getAccountInfo(idToken = idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser()
Log.d(TAG, "callback: onGetTokenResponseAndUser")
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
} catch (e: Exception) {
Log.w(TAG, "callback: onFailure", e)
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
}
}
}
override fun signInWithEmailAndPasswordCompat(email: String?, password: String?, callbacks: IFirebaseAuthCallbacks) {
signInWithEmailAndPassword(SignInWithEmailAndPasswordAidlRequest().apply { this.email = email; this.password = password }, callbacks)
}
override fun signInWithEmailLink(request: SignInWithEmailLinkAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: signInWithEmailLink")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun signInWithEmailLinkCompat(credential: EmailAuthCredential?, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: signInWithEmailLinkCompat")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun signInWithPhoneNumber(request: SignInWithPhoneNumberAidlRequest, callbacks: IFirebaseAuthCallbacks) {
lifecycleScope.launchWhenStarted {
Log.d(TAG, "signInWithPhoneNumber")
try {
val tokenResult = client.verifyPhoneNumber(
phoneNumber = request.credential.phoneNumber,
temporaryProof = request.credential.temporaryProof,
sessionInfo = request.credential.sessionInfo,
code = request.credential.smsCode
)
val idToken = tokenResult.getString("idToken")
val refreshToken = tokenResult.getString("refreshToken")
val isNewUser = tokenResult.optBoolean("isNewUser")
val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse()
val accountInfoResult = client.getAccountInfo(idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser().apply { this.isNewUser = isNewUser }
Log.d(TAG, "callback: onGetTokenResponseAndUser")
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
} catch (e: Exception) {
Log.w(TAG, "callback: onFailure", e)
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
}
}
}
override fun signInWithPhoneNumberCompat(credential: PhoneAuthCredential?, callbacks: IFirebaseAuthCallbacks) {
signInWithPhoneNumber(SignInWithPhoneNumberAidlRequest().apply { this.credential = credential }, callbacks)
}
override fun startMfaEnrollmentWithPhoneNumber(request: StartMfaPhoneNumberEnrollmentAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: startMfaEnrollmentWithPhoneNumber")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun startMfaSignInWithPhoneNumber(request: StartMfaPhoneNumberSignInAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: startMfaSignInWithPhoneNumber")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun unenrollMfa(request: UnenrollMfaAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: unenrollMfa")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun unlinkEmailCredential(request: UnlinkEmailCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: unlinkEmailCredential")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun unlinkEmailCredentialCompat(cachedState: String?, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: unlinkEmailCredentialCompat")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun unlinkFederatedCredential(request: UnlinkFederatedCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: unlinkFederatedCredential")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun unlinkFederatedCredentialCompat(provider: String?, cachedState: String?, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: unlinkFederatedCredentialCompat")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun updateProfile(request: UpdateProfileAidlRequest, callbacks: IFirebaseAuthCallbacks) {
lifecycleScope.launchWhenStarted {
Log.d(TAG, "updateProfile")
try {
val getTokenResponse = refreshTokenResponse(request.cachedState)
val accountInfoResult = client.getAccountInfo(idToken = getTokenResponse.accessToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser()
val setAccountInfo = client.setAccountInfo(idToken = getTokenResponse.accessToken, localId = accountInfoResult.localId, displayName = request.request.displayName, photoUrl = request.request.photoUrl, deleteAttribute = request.request.deleteAttributeList).toGetAccountInfoUser()
accountInfoResult.photoUrl = setAccountInfo.photoUrl
accountInfoResult.displayName = setAccountInfo.displayName
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
} catch (e: Exception) {
Log.w(TAG, "callback: onFailure", e)
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
}
}
}
override fun updateProfileCompat(cachedState: String?, userProfileChangeRequest: UserProfileChangeRequest, callbacks: IFirebaseAuthCallbacks) {
updateProfile(UpdateProfileAidlRequest().apply { this.cachedState = cachedState; this.request = userProfileChangeRequest}, callbacks)
}
override fun verifyBeforeUpdateEmail(request: VerifyBeforeUpdateEmailAidlRequest, callbacks: IFirebaseAuthCallbacks) {
Log.d(TAG, "Not yet implemented: verifyBeforeUpdateEmail")
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
}
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
if (super.onTransact(code, data, reply, flags)) return true
Log.d(TAG, "onTransact: $code, $data, $flags")
return false
}
}

View file

@ -0,0 +1,155 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.firebase.auth
import android.content.Context
import android.util.Log
import com.android.volley.NetworkResponse
import com.android.volley.ParseError
import com.android.volley.Request.Method.GET
import com.android.volley.Request.Method.POST
import com.android.volley.Response
import com.android.volley.VolleyError
import com.android.volley.toolbox.HttpHeaderParser
import com.android.volley.toolbox.JsonObjectRequest
import com.android.volley.toolbox.JsonRequest
import com.android.volley.toolbox.Volley
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import org.microg.gms.utils.singleInstanceOf
import org.microg.gms.utils.toHexString
import java.io.UnsupportedEncodingException
import java.lang.RuntimeException
import java.nio.charset.Charset
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
private const val TAG = "GmsFirebaseAuthClient"
class IdentityToolkitClient(context: Context, private val apiKey: String, private val packageName: String? = null, private val certSha1Hash: ByteArray? = null) {
private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) }
private fun buildRelyingPartyUrl(method: String) = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/$method?key=$apiKey"
private fun buildStsUrl(method: String) = "https://securetoken.googleapis.com/v1/$method?key=$apiKey"
private fun getRequestHeaders(): Map<String, String> = hashMapOf<String, String>().apply {
if (packageName != null) put("X-Android-Package", packageName)
if (certSha1Hash != null) put("X-Android-Cert", certSha1Hash.toHexString().uppercase())
}
private suspend fun request(method: String, data: JSONObject): JSONObject = suspendCoroutine { continuation ->
queue.add(object : JsonObjectRequest(POST, buildRelyingPartyUrl(method), data, {
continuation.resume(it)
}, {
Log.d(TAG, "Error: ${it.networkResponse?.data?.decodeToString() ?: it.message}")
continuation.resumeWithException(RuntimeException(it))
}) {
override fun getHeaders(): Map<String, String> = getRequestHeaders()
})
}
suspend fun createAuthUri(identifier: String? = null, tenantId: String? = null, continueUri: String? = "http://localhost"): JSONObject =
request("createAuthUri", JSONObject()
.put("identifier", identifier)
.put("tenantId", tenantId)
.put("continueUri", continueUri))
suspend fun getAccountInfo(idToken: String? = null): JSONObject =
request("getAccountInfo", JSONObject()
.put("idToken", idToken))
suspend fun getProjectConfig(): JSONObject = suspendCoroutine { continuation ->
queue.add(JsonObjectRequest(GET, buildRelyingPartyUrl("getProjectConfig"), null, { continuation.resume(it) }, { continuation.resumeWithException(RuntimeException(it)) }))
}
suspend fun getOobConfirmationCode(requestType: String, email: String? = null, newEmail: String? = null, continueUrl: String? = null, idToken: String? = null, iOSBundleId: String? = null, iOSAppStoreId: String? = null, androidMinimumVersion: String? = null, androidInstallApp: Boolean? = null, androidPackageName: String? = null, canHandleCodeInApp: Boolean? = null): JSONObject =
request("getOobConfirmationCode", JSONObject()
.put("kind", "identitytoolkit#relyingparty")
.put("requestType", requestType)
.put("email", email)
.put("newEmail", newEmail)
.put("continueUrl", continueUrl)
.put("idToken", idToken)
.put("iOSBundleId", iOSBundleId)
.put("iOSAppStoreId", iOSAppStoreId)
.put("androidMinimumVersion", androidMinimumVersion)
.put("androidInstallApp", androidInstallApp)
.put("androidPackageName", androidPackageName)
.put("canHandleCodeInApp", canHandleCodeInApp))
suspend fun sendVerificationCode(phoneNumber: String? = null, reCaptchaToken: String? = null): JSONObject =
request("sendVerificationCode", JSONObject()
.put("phoneNumber", phoneNumber)
.put("recaptchaToken", reCaptchaToken))
suspend fun setAccountInfo(idToken: String? = null, localId: String? = null, email: String? = null, password: String? = null, displayName: String? = null, photoUrl: String? = null, deleteAttribute: List<String> = emptyList()): JSONObject =
request("setAccountInfo", JSONObject()
.put("idToken", idToken)
.put("localId", localId)
.put("email", email)
.put("password", password)
.put("displayName", displayName)
.put("photoUrl", photoUrl)
.put("deleteAttribute", JSONArray().apply { deleteAttribute.map { put(it) } }))
suspend fun signupNewUser(email: String? = null, password: String? = null, tenantId: String? = null): JSONObject =
request("signupNewUser", JSONObject()
.put("email", email)
.put("password", password)
.put("tenantId", tenantId))
suspend fun verifyCustomToken(token: String? = null, returnSecureToken: Boolean = true): JSONObject =
request("verifyCustomToken", JSONObject()
.put("token", token)
.put("returnSecureToken", returnSecureToken))
suspend fun verifyAssertion(requestUri: String? = null, postBody: String? = null, returnSecureToken: Boolean = true, returnIdpCredential: Boolean = true): JSONObject =
request("verifyAssertion", JSONObject()
.put("requestUri", requestUri)
.put("postBody", postBody)
.put("returnSecureToken", returnSecureToken)
.put("returnIdpCredential", returnIdpCredential))
suspend fun verifyPassword(email: String? = null, password: String? = null, tenantId: String? = null, returnSecureToken: Boolean = true): JSONObject =
request("verifyPassword", JSONObject()
.put("email", email)
.put("password", password)
.put("tenantId", tenantId)
.put("returnSecureToken", returnSecureToken))
suspend fun verifyPhoneNumber(phoneNumber: String? = null, sessionInfo: String? = null, code: String? = null, idToken: String? = null, verificationProof: String? = null, temporaryProof: String? = null): JSONObject =
request("verifyPhoneNumber", JSONObject()
.put("verificationProof", verificationProof)
.put("code", code)
.put("idToken", idToken)
.put("temporaryProof", temporaryProof)
.put("phoneNumber", phoneNumber)
.put("sessionInfo", sessionInfo))
suspend fun getTokenByRefreshToken(refreshToken: String): JSONObject = suspendCoroutine { continuation ->
queue.add(object : JsonRequest<JSONObject>(POST, buildStsUrl("token"), "grant_type=refresh_token&refresh_token=$refreshToken", { continuation.resume(it) }, { continuation.resumeWithException(RuntimeException(it)) }) {
override fun parseNetworkResponse(response: NetworkResponse): Response<JSONObject> {
return try {
val jsonString = String(response.data, Charset.forName(HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET)))
Response.success(JSONObject(jsonString), null)
} catch (e: UnsupportedEncodingException) {
Response.error(ParseError(e))
} catch (je: JSONException) {
Response.error(ParseError(je))
}
}
override fun getBodyContentType(): String {
return "application/x-www-form-urlencoded"
}
override fun getHeaders(): Map<String, String> = getRequestHeaders()
})
}
}

View file

@ -0,0 +1,104 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.firebase.auth
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.Intent.*
import android.os.Bundle
import android.os.ResultReceiver
import android.util.Log
import android.webkit.JavascriptInterface
import android.webkit.WebSettings
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
import org.microg.gms.firebase.auth.core.R
import org.microg.gms.profile.Build
import org.microg.gms.profile.ProfileManager
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
private const val TAG = "GmsFirebaseAuthCaptcha"
class ReCaptchaActivity : AppCompatActivity() {
private val receiver: ResultReceiver?
get() = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER)
private val hostname: String
get() = intent.getStringExtra(EXTRA_HOSTNAME) ?: "localhost:5000"
private var finished = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
openWebsite()
}
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
private fun openWebsite() {
val apiKey = intent.getStringExtra(EXTRA_API_KEY) ?: return finishResult(Activity.RESULT_CANCELED)
setContentView(R.layout.activity_recaptcha)
val view = findViewById<WebView>(R.id.web)
val settings = view.settings
settings.javaScriptEnabled = true
settings.useWideViewPort = false
settings.setSupportZoom(false)
settings.displayZoomControls = false
settings.cacheMode = WebSettings.LOAD_NO_CACHE
ProfileManager.ensureInitialized(this)
settings.userAgentString = Build.generateWebViewUserAgentString(settings.userAgentString)
view.addJavascriptInterface(ReCaptchaCallback(this), "MyCallback")
val captcha = assets.open("recaptcha.html").bufferedReader().readText().replace("%apikey%", apiKey)
view.loadDataWithBaseURL("https://$hostname/", captcha, null, null, "https://$hostname/")
}
fun finishResult(resultCode: Int, token: String? = null) {
finished = true
setResult(resultCode, token?.let { Intent().apply { putExtra(EXTRA_TOKEN, it) } })
receiver?.send(resultCode, token?.let { Bundle().apply { putString(EXTRA_TOKEN, it) } })
finish()
}
override fun onDestroy() {
super.onDestroy()
if (!finished) receiver?.send(Activity.RESULT_CANCELED, null)
}
companion object {
class ReCaptchaCallback(val activity: ReCaptchaActivity) {
@JavascriptInterface
fun onReCaptchaToken(token: String) {
Log.d(TAG, "onReCaptchaToken: $token")
activity.finishResult(Activity.RESULT_OK, token)
}
}
fun isSupported(context: Context): Boolean = true
suspend fun awaitToken(context: Context, apiKey: String, hostname: String? = null) = suspendCoroutine<String> { continuation ->
val intent = Intent(context, ReCaptchaActivity::class.java)
val resultReceiver = object : ResultReceiver(null) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
try {
if (resultCode == Activity.RESULT_OK) {
continuation.resume(resultData?.getString(EXTRA_TOKEN)!!)
}
} catch (e: Exception) {
continuation.resumeWithException(e)
}
}
}
intent.putExtra(EXTRA_API_KEY, apiKey)
intent.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver)
intent.putExtra(EXTRA_HOSTNAME, hostname)
intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(FLAG_ACTIVITY_REORDER_TO_FRONT)
intent.addFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
context.startActivity(intent)
}
}
}

View file

@ -0,0 +1,173 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.firebase.auth
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.graphics.PixelFormat
import android.os.Bundle
import android.os.IBinder
import android.os.ResultReceiver
import android.provider.Settings
import android.util.DisplayMetrics
import android.util.Log
import android.view.*
import android.webkit.JavascriptInterface
import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.FrameLayout
import org.microg.gms.firebase.auth.core.R
import org.microg.gms.profile.Build
import org.microg.gms.profile.ProfileManager
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
private const val TAG = "GmsFirebaseAuthCaptcha"
class ReCaptchaOverlayService : Service() {
private var receiver: ResultReceiver? = null
private var hostname: String? = null
private var apiKey: String? = null
private var finished = false
private var container: View? = null
private var windowManager: WindowManager? = null
override fun onBind(intent: Intent): IBinder? {
init(intent)
return null
}
override fun onUnbind(intent: Intent?): Boolean {
finishResult(Activity.RESULT_CANCELED)
return super.onUnbind(intent)
}
private fun init(intent: Intent) {
apiKey = intent.getStringExtra(EXTRA_API_KEY) ?: return finishResult(Activity.RESULT_CANCELED)
receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER)
hostname = intent.getStringExtra(EXTRA_HOSTNAME) ?: "localhost:5000"
windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
show()
}
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
private fun show() {
val layoutParamsType = if (android.os.Build.VERSION.SDK_INT >= 26) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
}
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT,
layoutParamsType,
0,
PixelFormat.TRANSLUCENT)
params.gravity = Gravity.CENTER or Gravity.START
params.x = 0
params.y = 0
val interceptorLayout: FrameLayout = object : FrameLayout(this) {
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (event.action == KeyEvent.ACTION_DOWN) {
if (event.keyCode == KeyEvent.KEYCODE_BACK || event.keyCode == KeyEvent.KEYCODE_HOME) {
finishResult(Activity.RESULT_CANCELED)
return true
}
}
return super.dispatchKeyEvent(event)
}
}
val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as? LayoutInflater?
if (inflater != null) {
val container = inflater.inflate(R.layout.activity_recaptcha, interceptorLayout)
this.container = container
container.setBackgroundResource(androidx.appcompat.R.drawable.abc_dialog_material_background)
val pad = (5.0 * (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
container.setOnTouchListener { v, _ ->
v.performClick()
finishResult(Activity.RESULT_CANCELED)
return@setOnTouchListener true
}
val view = container.findViewById<WebView>(R.id.web)
view.setPadding(pad, pad, pad, pad)
val settings = view.settings
settings.javaScriptEnabled = true
settings.useWideViewPort = false
settings.setSupportZoom(false)
settings.displayZoomControls = false
settings.cacheMode = WebSettings.LOAD_NO_CACHE
ProfileManager.ensureInitialized(this)
settings.userAgentString = Build.generateWebViewUserAgentString(settings.userAgentString)
view.addJavascriptInterface(ReCaptchaCallback(this), "MyCallback")
val captcha = assets.open("recaptcha.html").bufferedReader().readText().replace("%apikey%", apiKey!!)
view.loadDataWithBaseURL("https://$hostname/", captcha, null, null, "https://$hostname/")
windowManager?.addView(container, params)
}
}
fun finishResult(resultCode: Int, token: String? = null) {
if (!finished) {
finished = true
receiver?.send(resultCode, token?.let { Bundle().apply { putString(EXTRA_TOKEN, it) } })
}
container?.let { windowManager?.removeView(it) }
}
companion object {
private val recaptchaServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.d(TAG, "onReCaptchaToken: onServiceConnected: $name")
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.d(TAG, "onReCaptchaToken: onServiceDisconnected: $name")
}
}
class ReCaptchaCallback(private val overlay: ReCaptchaOverlayService) {
@JavascriptInterface
fun onReCaptchaToken(token: String) {
Log.d(TAG, "onReCaptchaToken: $token")
overlay.finishResult(Activity.RESULT_OK, token)
}
}
fun isSupported(context: Context): Boolean = android.os.Build.VERSION.SDK_INT < 23 || Settings.canDrawOverlays(context)
suspend fun awaitToken(context: Context, apiKey: String, hostname: String? = null) = suspendCoroutine { continuation ->
val intent = Intent(context, ReCaptchaOverlayService::class.java)
val resultReceiver = object : ResultReceiver(null) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
context.unbindService(recaptchaServiceConnection)
try {
if (resultCode == Activity.RESULT_OK) {
continuation.resume(resultData?.getString(EXTRA_TOKEN)!!)
}
} catch (e: Exception) {
continuation.resumeWithException(e)
}
}
}
intent.putExtra(EXTRA_API_KEY, apiKey)
intent.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver)
intent.putExtra(EXTRA_HOSTNAME, hostname)
context.bindService(intent, recaptchaServiceConnection, BIND_AUTO_CREATE)
}
}
}

View file

@ -0,0 +1,11 @@
/**
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.firebase.auth
const val EXTRA_TOKEN = "token"
const val EXTRA_API_KEY = "api_key"
const val EXTRA_HOSTNAME = "hostname"
const val EXTRA_RESULT_RECEIVER = "receiver"

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="450dp"
android:orientation="vertical">
<WebView
android:id="@+id/web"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:minHeight="450dp" />
</FrameLayout>