Skip to content
generated from Tweener/kmplate-lib

A Kotlin/Compose Multiplatform library for easy authentication on Android and iOS.

License

Notifications You must be signed in to change notification settings

Tweener/passage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

51 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Maven Central Version Kotlin Compose gradle-version License

Website X/Twitter


Passage logo Passage logo


Passage

Passage is a Kotlin Multiplatform library designed to simplify authentication flows across Android and iOS platforms. Built on Firebase Authentication, Passage abstracts common operations and provides composable APIs to manage authentication using popular providers like Google, Apple, and Email/Password.


Be sure to show your support by starring ⭐️ this repository, and feel free to contribute if you're interested!


🌟 Features

  • Firebase Authentication: Powered by Firebase for robust and secure authentication.
  • Gatekeeper (Provider) Support: Google, Apple, Email/Password.
  • Extensible Configuration: Customize authentication flows with platform-specific settings.
  • Email actions: Send email actions for password resets or verifying a user's email.

🎯 Concept

Passage uses Firebase Authentication as the backbone for secure and reliable user identity management. It abstracts the complexity of integrating with Firebase's SDKs on multiple platforms, providing a unified API for developers.

Passage abstracts the authentication flow into three main components:

  1. Passage: The entry point for managing authentication flows.
  2. Gatekeepers: Providers like Google, Apple, and Email/Password, which handle specific authentication mechanisms.
  3. Entrants: Users who have successfully authenticated and gained access.

πŸ› οΈ Installation

In your settings.gradle.kts file, add Maven Central to your repositories:

repositories {
    mavenCentral()
}

Then add Passage dependency to your module:

  • With version catalog, open libs.versions.toml:
[versions]
passage = "1.0.0" // Check latest version

[libraries]
passage = { group = "io.github.tweener", name = "passage", version.ref = "passage" }

Then in your module build.gradle.kts add:

dependencies {
    implementation(libs.passage)
}
  • Without version catalog, in your module build.gradle.kts add:
dependencies {
    val passage_version = "1.0.0" // Check latest version

    implementation("io.github.tweener:passage:$passage_version")
}

The latest version is: Maven Central Version


πŸ”§ Configuration

1. Create a Passage

Depending on your project configuration, you can create an instance of Passage in two different ways:

➑️ Kotlin Multplatform (without Compose)
  • πŸ€– Android

    Create an instance of PassageAndroid passing a Context:

val passage: Passage = PassageAndroid(context = context)
  • 🍎 iOS

    Create an instance of PassageIos:

val passage: Passage = PassageIos()
➑️ Compose Multplatform

Create an instance of Passage using rememberPassage():

val passage: Passage = rememberPassage()

2. Configure Passage

Provide a list of the desired gatekeepers (authentication providers) to configure:

val gatekeeperConfigurations = listOf(
    GoogleGatekeeperConfiguration(
        serverClientId = "your-google-server-client-id",
        android = GoogleGatekeeperAndroidConfiguration(
            filterByAuthorizedAccounts = false,
            autoSelectEnabled = true,
            maxRetries = 3
        )
    ),
    AppleGatekeeperConfiguration(),
    EmailPasswordGatekeeperConfiguration()
)

For example, if you only want to use the Google Gatekeeper, simply provide the GoogleGatekeeperConfiguration like this:

val gatekeeperConfigurations = listOf(
    GoogleGatekeeperConfiguration(
        serverClientId = "your-google-server-client-id",
        android = GoogleGatekeeperAndroidConfiguration(
            filterByAuthorizedAccounts = false,
            autoSelectEnabled = true,
            maxRetries = 3
        )
    ),
)

Important

Replace your-google-server-client-id with your actual Google serverClientId.

3. Initialize Passage

Initialize Passage in your common module entry point:

passage.initialize(gatekeepersConfiguration = gatekeepersConfiguration)

Note

If your app already uses Firebase, you can pass the existing Firebase instance to Passage to reuse it and prevent reinitializing Firebase unnecessarily:

passage.initialize(
    gatekeepersConfiguration = gatekeepersConfiguration,
    firebaseAuth = Firebase.Auth,
)

πŸ§‘β€πŸ’» Usage

1. Authenticate a user

Use the provider-specific methods to authenticate users.

Google Authentication

Passage#authenticateWithGoogle() authenticates a user via Google Sign-In. If the user does not already exist, a new account will be created automatically.

val result = passage.authenticateWithGoogle()
result.fold(
    onSuccess = { entrant -> Log.d("Passage", "Welcome, ${entrant.displayName}") },
    onFailure = { error -> Log.e("Passage", "Authentication failed", error) }
)

Apple Authentication

Passage#authenticateWithApple() authenticates a user via Apple Sign-In. If the user does not already exist, a new account will be created automatically.

val result = passage.authenticateWithApple()
// Handle result similarly

Email/Password Authentication

a. Create a user:

Creating a user with email & password will automatically authenticate the user upon successful account creation.

val result = passage.createUserWithEmailAndPassword(PassageEmailAuthParams(email, password))
// Handle result similarly
b. Authenticate a user:
val result = passage.authenticateWithEmailAndPassword(PassageEmailAuthParams(email, password))
// Handle result similarly

2. Sign Out or Reauthenticate

passage.signOut()

passage.reauthenticateWithGoogle()
passage.reauthenticateWithApple()
passage.reauthenticateWithEmailAndPassword(params)

3. Email actions

You may need to send emails to the user for a password reset if the user forgot their password for instance, or for verifying the user's email address when creating an account.

Important

Passage uses Firebase Dynamic Links to send emails containing universal links. Follow the documentation to configure your app with Firebase Dynamic Links (you don't need to add Firebase Dynamic Links SDK to your app).

To handle universal links, additional configuration is required for each platform:

πŸ€– Android

In your activity configured to be open when a universal link is clicked:

class MainActivity : ComponentActivity() {

    private val passage = providePassage()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        handleUniversalLink(intent = intent)

        // ...
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)

        handleUniversalLink(intent = intent)

        // ...
    }

    private fun handleUniversalLink(intent: Intent) {
        intent.data?.let {
            passage.handleUrl(url = it.toString())
        }
    }
}
🍎 iOS

Create a class PassageHelper in your iosMain module:

class PassageHelper {

    private val passage = providePassage()

    fun handle(url: String): Boolean =
        passage.handleUrl(url = url)

}

Then, in your AppDelegate, add the following lines:

class AppDelegate : NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    // ...

    func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
           let url = userActivity.webpageURL {
            if (PassageHelper().handle(url: url.absoluteString)) {
                print("Handled by Passage")
            }

            return true
        }
        
        print("No valid URL in user activity.")
        return false
    }
}

Passage exposes a universalLinkToHandle StateFlow, which you can use to be notified when a new unviersal link has been clicked and validated by Passage:

val link = passage.universalLinkToHandle.collectAsStateWithLifecycle()

LaunchedEffect(link.value) {
    link.value?.let {
        println("Universal link handled for mode: ${it.mode} with continueUrl: ${it.continueUrl}")
        passage.onLinkHandled() // Important: call 'onLinkHandled()' to let Passage know the link has been handled and can update the authentication state
    }
}
a. Send an email for verifying a user's email

If you want to reinforce authentication, you can send the user an email to verify its email address:

val result = passage.sendEmailVerification(
    params = PassageEmailVerificationParams(
        url = "https://passagesample.page.link/action/email_verified",
        iosParams = PassageEmailVerificationIosParams(bundleId = "com.tweener.passage.sample"),
        androidParams = PassageEmailVerificationAndroidParams(
            packageName = "com.tweener.passage.sample",
            installIfNotAvailable = true,
            minimumVersion = "1.0",
        ),
        canHandleCodeInApp = true,
    )
)
result.fold(
    onSuccess = { entrant -> Log.d("Passage", "An email has been sent to the user to verify its email address.") },
    onFailure = { error -> Log.e("Passage", "Couldn't send the email", error) }
)
b. Send an email for a password reset

If you want to reinforce authentication, you can send the user an email to verify its email address:

val result = passage.sendPasswordResetEmail(
    params = PassageForgotPasswordParams(
        email = passage.getCurrentUser()!!.email,
        url = "https://passagesample.page.link/action/password_reset",
        iosParams = PassageForgotPasswordIosParams(bundleId = "com.tweener.passage.sample"),
        androidParams = PassageForgotPasswordAndroidParams(
            packageName = "com.tweener.passage.sample",
            installIfNotAvailable = true,
            minimumVersion = "1.0",
        ),
        canHandleCodeInApp = true,
    )
)
result.fold(
    onSuccess = { entrant -> Log.d("Passage", "An email has been sent to the user to reset its password.") },
    onFailure = { error -> Log.e("Passage", "Couldn't send the email", error) }
)

🀝 Contributing

We love your input and welcome any contributions! Please read our contribution guidelines before submitting a pull request.


πŸ™ Credits


πŸ“œ Licence

Passage is licensed under the Apache-2.0.