All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.firebase.auth.FirebaseAuth.kt Maven / Gradle / Ivy

package com.google.firebase.auth

import android.util.Log
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.TaskCompletionSource
import com.google.android.gms.tasks.Tasks
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseException
import com.google.firebase.FirebasePlatform
import com.google.firebase.auth.internal.InternalAuthProvider
import com.google.firebase.internal.InternalTokenResult
import com.google.firebase.internal.api.FirebaseNoSignedInUserException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.longOrNull
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import java.io.IOException
import java.util.Base64
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit

val jsonParser = Json { ignoreUnknownKeys = true }

@Serializable
class FirebaseUserImpl private constructor(
    @Transient
    private val app: FirebaseApp = FirebaseApp.getInstance(),
    override val isAnonymous: Boolean,
    override val uid: String,
    val idToken: String,
    val refreshToken: String,
    val expiresIn: Int,
    val createdAt: Long
) : FirebaseUser() {

    constructor(app: FirebaseApp, data: JsonObject, isAnonymous: Boolean = data["isAnonymous"]?.jsonPrimitive?.booleanOrNull ?: false) : this(
        app,
        isAnonymous,
        data["uid"]?.jsonPrimitive?.contentOrNull ?: data["user_id"]?.jsonPrimitive?.contentOrNull ?: data["localId"]?.jsonPrimitive?.contentOrNull ?: "",
        data["idToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("id_token").jsonPrimitive.content,
        data["refreshToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("refresh_token").jsonPrimitive.content,
        data["expiresIn"]?.jsonPrimitive?.intOrNull ?: data.getValue("expires_in").jsonPrimitive.int,
        data["createdAt"]?.jsonPrimitive?.longOrNull ?: System.currentTimeMillis()
    )

    val claims: Map by lazy {
        jsonParser
            .parseToJsonElement(String(Base64.getUrlDecoder().decode(idToken.split(".")[1])))
            .jsonObject
            .run { value as Map? }
            .orEmpty()
    }

    val JsonElement.value get(): Any? = when (this) {
        is JsonNull -> null
        is JsonArray -> map { it.value }
        is JsonObject -> jsonObject.mapValues { (_, it) -> it.value }
        is JsonPrimitive -> booleanOrNull ?: doubleOrNull ?: content
        else -> TODO()
    }

    override fun delete(): Task {
        val source = TaskCompletionSource()
        val body = RequestBody.create(FirebaseAuth.getInstance(app).json, JsonObject(mapOf("idToken" to JsonPrimitive(idToken))).toString())
        val request = Request.Builder()
            .url("https://www.googleapis.com/identitytoolkit/v3/relyingparty/deleteAccount?key=" + app.options.apiKey)
            .post(body)
            .build()
        FirebaseAuth.getInstance(app).client.newCall(request).enqueue(object : Callback {

            override fun onFailure(call: Call, e: IOException) {
                source.setException(FirebaseException(e.toString(), e))
            }

            @Throws(IOException::class)
            override fun onResponse(call: Call, response: Response) {
                if (!response.isSuccessful) {
                    FirebaseAuth.getInstance(app).signOut()
                    source.setException(
                        FirebaseAuth.getInstance(app).createAuthInvalidUserException(
                            "deleteAccount",
                            request,
                            response
                        )
                    )
                } else {
                    source.setResult(null)
                }
            }
        })
        return source.task
    }

    override fun reload(): Task {
        val source = TaskCompletionSource()
        FirebaseAuth.getInstance(app).refreshToken(this, source) { null }
        return source.task
    }

    override fun getIdToken(forceRefresh: Boolean) = FirebaseAuth.getInstance(app).getAccessToken(forceRefresh)
}

class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {

    val json = MediaType.parse("application/json; charset=utf-8")
    val client: OkHttpClient = OkHttpClient.Builder()
        .connectTimeout(60, TimeUnit.SECONDS)
        .readTimeout(60, TimeUnit.SECONDS)
        .writeTimeout(60, TimeUnit.SECONDS)
        .build()

    companion object {

        @JvmStatic
        fun getInstance(): FirebaseAuth = getInstance(FirebaseApp.getInstance())

        @JvmStatic
        fun getInstance(app: FirebaseApp): FirebaseAuth = app.get(FirebaseAuth::class.java)
    }

    private val internalIdTokenListeners = CopyOnWriteArrayList()
    private val idTokenListeners = CopyOnWriteArrayList()
    private val authStateListeners = CopyOnWriteArrayList()

    val currentUser: FirebaseUser?
        get() = user

    val FirebaseApp.key get() = "com.google.firebase.auth.FIREBASE_USER${"[$name]".takeUnless { isDefaultApp }.orEmpty()}"

    private var user: FirebaseUserImpl? = FirebasePlatform.firebasePlatform
        .runCatching { retrieve(app.key)?.let { FirebaseUserImpl(app, jsonParser.parseToJsonElement(it).jsonObject) } }
        .onFailure { it.printStackTrace() }
        .getOrNull()

        private set(value) {
            if (field != value) {
                val prev = field
                field = value

                if (value == null) {
                    FirebasePlatform.firebasePlatform.clear(app.key)
                } else {
                    FirebasePlatform.firebasePlatform.store(app.key, jsonParser.encodeToString(FirebaseUserImpl.serializer(), value))
                }

                GlobalScope.launch(Dispatchers.Main) {
                    if (prev?.uid != value?.uid) {
                        authStateListeners.forEach { l -> l.onAuthStateChanged(this@FirebaseAuth) }
                    }

                    if (prev?.idToken != value?.idToken) {
                        val result = InternalTokenResult(value?.idToken)
                        for (listener in internalIdTokenListeners) {
                            Log.i("FirebaseAuth", "Calling onIdTokenChanged for ${value?.uid} on listener $listener")
                            listener.onIdTokenChanged(result)
                        }
                        for (listener in idTokenListeners) {
                            listener.onIdTokenChanged(this@FirebaseAuth)
                        }
                    }
                }
            }
        }

    fun signInAnonymously(): Task {
        val source = TaskCompletionSource()
        val body = RequestBody.create(json, JsonObject(mapOf("returnSecureToken" to JsonPrimitive(true))).toString())
        val request = Request.Builder()
            .url("https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=" + app.options.apiKey)
            .post(body)
            .build()
        client.newCall(request).enqueue(object : Callback {

            override fun onFailure(call: Call, e: IOException) {
                source.setException(FirebaseException(e.toString(), e))
            }

            @Throws(IOException::class)
            override fun onResponse(call: Call, response: Response) {
                if (!response.isSuccessful) {
                    source.setException(
                        createAuthInvalidUserException("accounts:signUp", request, response)
                    )
                } else {
                    val body = response.body()!!.use { it.string() }
                    user = FirebaseUserImpl(app, jsonParser.parseToJsonElement(body).jsonObject, true)
                    source.setResult(AuthResult { user })
                }
            }
        })
        return source.task
    }

    fun signInWithCustomToken(customToken: String): Task {
        val source = TaskCompletionSource()
        val body = RequestBody.create(
            json,
            JsonObject(mapOf("token" to JsonPrimitive(customToken), "returnSecureToken" to JsonPrimitive(true))).toString()
        )
        val request = Request.Builder()
            .url("https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=" + app.options.apiKey)
            .post(body)
            .build()
        client.newCall(request).enqueue(object : Callback {

            override fun onFailure(call: Call, e: IOException) {
                source.setException(FirebaseException(e.toString(), e))
            }

            @Throws(IOException::class)
            override fun onResponse(call: Call, response: Response) {
                if (!response.isSuccessful) {
                    source.setException(
                        createAuthInvalidUserException("verifyCustomToken", request, response)
                    )
                } else {
                    val body = response.body()!!.use { it.string() }
                    val user = FirebaseUserImpl(app, jsonParser.parseToJsonElement(body).jsonObject)
                    refreshToken(user, source) { AuthResult { it } }
                }
            }
        })
        return source.task
    }

    fun signInWithEmailAndPassword(email: String, password: String): Task {
        val source = TaskCompletionSource()
        val body = RequestBody.create(
            json,
            JsonObject(mapOf("email" to JsonPrimitive(email), "password" to JsonPrimitive(password), "returnSecureToken" to JsonPrimitive(true))).toString()
        )
        val request = Request.Builder()
            .url("https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=" + app.options.apiKey)
            .post(body)
            .build()
        client.newCall(request).enqueue(object : Callback {

            override fun onFailure(call: Call, e: IOException) {
                source.setException(FirebaseException(e.toString(), e))
            }

            @Throws(IOException::class)
            override fun onResponse(call: Call, response: Response) {
                if (!response.isSuccessful) {
                    source.setException(
                        createAuthInvalidUserException("verifyPassword", request, response)
                    )
                } else {
                    val body = response.body()!!.use { it.string() }
                    val user = FirebaseUserImpl(app, jsonParser.parseToJsonElement(body).jsonObject)
                    refreshToken(user, source) { AuthResult { it } }
                }
            }
        })
        return source.task
    }

    internal fun createAuthInvalidUserException(
        action: String,
        request: Request,
        response: Response
    ): FirebaseAuthInvalidUserException {
        val body = response.body()!!.use { it.string() }
        val jsonObject = jsonParser.parseToJsonElement(body).jsonObject

        return FirebaseAuthInvalidUserException(
            jsonObject["error"]?.jsonObject
                ?.get("message")?.jsonPrimitive
                ?.contentOrNull
                ?: "UNKNOWN_ERROR",
            "$action API returned an error, " +
                "with url [${request.method()}] ${request.url()} ${request.body()} -- " +
                "response [${response.code()}] ${response.message()} $body"
        )
    }

    fun signOut() {
        // todo cancel token refresher
        user = null
    }

    override fun getAccessToken(forceRefresh: Boolean): Task {
        val user = user ?: return Tasks.forException(FirebaseNoSignedInUserException("Please sign in before trying to get a token."))

        if (!forceRefresh && user.createdAt + user.expiresIn * 1000 - 5 * 60 * 1000 > System.currentTimeMillis()) {
//            Log.i("FirebaseAuth", "returning existing token for user ${user.uid} from getAccessToken")
            return Tasks.forResult(GetTokenResult(user.idToken, user.claims))
        }
//        Log.i("FirebaseAuth", "Refreshing access token forceRefresh=$forceRefresh createdAt=${user.createdAt} expiresIn=${user.expiresIn}")
        val source = TaskCompletionSource()
        refreshToken(user, source) { GetTokenResult(it.idToken, user.claims) }
        return source.task
    }

    private var refreshSource = TaskCompletionSource().apply { setException(Exception()) }

    internal fun  refreshToken(user: FirebaseUserImpl, source: TaskCompletionSource, map: (user: FirebaseUserImpl) -> T?) {
        refreshSource = refreshSource
            .takeUnless { it.task.isComplete }
            ?: enqueueRefreshTokenCall(user)
        refreshSource.task.addOnSuccessListener { source.setResult(map(it)) }
        refreshSource.task.addOnFailureListener { source.setException(FirebaseException(it.toString(), it)) }
    }

    private fun enqueueRefreshTokenCall(user: FirebaseUserImpl): TaskCompletionSource {
        val source = TaskCompletionSource()
        val body = RequestBody.create(
            json,
            JsonObject(
                mapOf(
                    "refresh_token" to JsonPrimitive(user.refreshToken),
                    "grant_type" to JsonPrimitive("refresh_token")
                )
            ).toString()
        )
        val request = Request.Builder()
            .url("https://securetoken.googleapis.com/v1/token?key=" + app.options.apiKey)
            .post(body)
            .build()

        client.newCall(request).enqueue(object : Callback {

            override fun onFailure(call: Call, e: IOException) {
                source.setException(e)
            }

            @Throws(IOException::class)
            override fun onResponse(call: Call, response: Response) {
                val body = response.body()?.use { it.string() }
                if (!response.isSuccessful) {
                    signOutAndThrowInvalidUserException(body.orEmpty(), "token API returned an error: $body")
                } else {
                    jsonParser.parseToJsonElement(body!!).jsonObject.apply {
                        val user = FirebaseUserImpl(app, this, user.isAnonymous)
                        if (user.claims["aud"] != app.options.projectId) {
                            signOutAndThrowInvalidUserException(
                                user.claims.toString(),
                                "Project ID's do not match ${user.claims["aud"]} != ${app.options.projectId}"
                            )
                        } else {
                            [email protected] = user
                            source.setResult(user)
                        }
                    }
                }
            }

            private fun signOutAndThrowInvalidUserException(body: String, message: String) {
                signOut()
                source.setException(FirebaseAuthInvalidUserException(body, message))
            }
        })
        return source
    }

    override fun getUid(): String? {
        return user?.uid
    }

    override fun addIdTokenListener(listener: com.google.firebase.auth.internal.IdTokenListener) {
        internalIdTokenListeners.addIfAbsent(listener)
        GlobalScope.launch(Dispatchers.Main) {
            listener.onIdTokenChanged(InternalTokenResult(user?.idToken))
        }
    }

    override fun removeIdTokenListener(listener: com.google.firebase.auth.internal.IdTokenListener) {
        internalIdTokenListeners.remove(listener)
    }

    @Synchronized
    fun addAuthStateListener(listener: AuthStateListener) {
        authStateListeners.addIfAbsent(listener)
        GlobalScope.launch(Dispatchers.Main) {
            listener.onAuthStateChanged(this@FirebaseAuth)
        }
    }

    @Synchronized
    fun removeAuthStateListener(listener: AuthStateListener) {
        authStateListeners.remove(listener)
    }

    @FunctionalInterface
    interface AuthStateListener {
        fun onAuthStateChanged(auth: FirebaseAuth)
    }

    @FunctionalInterface
    interface IdTokenListener {
        fun onIdTokenChanged(auth: FirebaseAuth)
    }

    fun addIdTokenListener(listener: IdTokenListener) {
        idTokenListeners.addIfAbsent(listener)
        GlobalScope.launch(Dispatchers.Main) {
            listener.onIdTokenChanged(this@FirebaseAuth)
        }
    }

    fun removeIdTokenListener(listener: IdTokenListener) {
        idTokenListeners.remove(listener)
    }

    fun sendPasswordResetEmail(email: String, settings: ActionCodeSettings?): Task = TODO()
    fun createUserWithEmailAndPassword(email: String, password: String): Task = TODO()
    fun signInWithCredential(authCredential: AuthCredential): Task = TODO()
    fun checkActionCode(code: String): Task = TODO()
    fun confirmPasswordReset(code: String, newPassword: String): Task = TODO()
    fun fetchSignInMethodsForEmail(email: String): Task = TODO()
    fun sendSignInLinkToEmail(email: String, actionCodeSettings: ActionCodeSettings): Task = TODO()
    fun verifyPasswordResetCode(code: String): Task = TODO()
    fun updateCurrentUser(user: FirebaseUser): Task = TODO()
    fun applyActionCode(code: String): Task = TODO()
    val languageCode: String get() = TODO()
    fun isSignInWithEmailLink(link: String): Boolean = TODO()
    fun signInWithEmailLink(email: String, link: String): Task = TODO()

    fun setLanguageCode(value: String): Nothing = TODO()
    fun useEmulator(host: String, port: Int): Unit = TODO()
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy