kmp-telegram-login
0.3.0indexedNative "Log in with Telegram" OAuth2+PKCE flow implementation offering app redirect plus web fallback, minimal API (configure/login/handle), and Telegram-signed OpenID Connect id_token.
Native "Log in with Telegram" OAuth2+PKCE flow implementation offering app redirect plus web fallback, minimal API (configure/login/handle), and Telegram-signed OpenID Connect id_token.
A Kotlin Multiplatform SDK for Telegram's native "Log in with Telegram" flow. Write the login logic once in Kotlin for Android and iOS — no per-platform native SDK, no Swift/Kotlin bridge.
Community library implementing the same OAuth2 + PKCE flow as Telegram's official native SDKs. Not affiliated with or endorsed by Telegram.
Telegram ships separate native SDKs for Android and iOS. In a KMP app you'd depend on both and shuttle results across the Kotlin/Swift boundary. This library puts the whole flow in commonMain; only a couple of tiny primitives are expect/actual.
// shared/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("app.univera.telegramlogin:telegram-login:0.3.0")
}
}
}
Register both your Android and iOS apps with your bot so Telegram can verify them and issue each a secure redirect domain. Open @BotFather → Bot Settings → Login Widget — this is also where you get your bot's client_id.
For each registered app Telegram auto-generates a per-app domain https://app{appid}-login.tg.dev (the {appid} differs per app — Android vs iOS get different ones). The domain requires no manual registration.
Telegram verifies your app's cryptographic signature. Provide:
app.univera.android)../gradlew signingReport). With Play App Signing enabled, use the Play app-signing key's SHA-256, not the upload key — App Link verification checks the certificate the installed (Play-delivered) app ships with.Redirect URI — App Link (recommended): https://app{androidAppId}-login.tg.dev/tglogin (only your verified app can intercept it). Custom-scheme fallback: yourapp://telegram-login.
Provide:
app.univera.ios).ABCDE12345).Redirect URI — Universal Link (recommended): https://app{iosAppId}-login.tg.dev (prevents callback hijacking). Custom-scheme fallback: yourapp://tglogin.
AndroidManifest.xmlDeclare the App Link on the Activity that handles the callback, and let the app query the tg scheme:
Info.plistapplinks:app{appid}-login.tg.devInfo.plist → LSApplicationQueriesSchemes: add tgDuring development iOS heavily caches the Universal-Links config. Append
?mode=developerto the Associated Domain (applinks:app{appid}-login.tg.dev?mode=developer) and toggle Settings → Developer → Associated Domains Development on a physical device. Remove it for release.
configure() is part of the common API, but the redirectUri is per-platform: BotFather issues a different app{appid}-login.tg.dev host for your Android app vs your iOS app, and Android's App Link uses the /tglogin path while iOS uses the bare host. Only clientId and are the same on both — so supply the redirect via :
// commonMain
import app.univera.telegramlogin.TelegramLogin
internal expect val telegramRedirectUri: String
fun initTelegramLogin() = TelegramLogin.configure(
clientId = "YOUR_BOT_CLIENT_ID", // your bot id — same on both platforms
redirectUri = telegramRedirectUri,
scopes = listOf("openid", "phone"),
// fallbackScheme = "yourapp", // optional: iOS < 17.4 web fallback
)
// androidMain — Android app's host, WITH the /tglogin path
internal actual val telegramRedirectUri = "https://app<androidAppId>-login.tg.dev/tglogin"
// iosMain — iOS app's host, NO path
internal actual val telegramRedirectUri = "https://app<iosAppId>-login.tg.dev"
Call initTelegramLogin() once at startup (e.g. Application.onCreate, or a shared init invoked from each platform's entry point).
login() opens Telegram and suspends until the redirect returns. Await it in a long-lived scope (e.g. a ViewModel):
when (val result = TelegramLogin.login(context)) { // context: TelegramAuthContext
is TelegramLoginResult.Success -> sendToBackend(result.idToken)
is TelegramLoginResult.Failure -> showError(result.error)
}
Build the TelegramAuthContext per platform. In Compose Multiplatform a tiny expect/actual helper keeps the call site common:
// commonMain
@Composable expect fun rememberTelegramAuthContext(): TelegramAuthContext
// androidMain — applicationContext is enough (the SDK uses FLAG_ACTIVITY_NEW_TASK)
@Composable actual =
remember { TelegramAuthContext(LocalContext.current.applicationContext) }
= remember { TelegramAuthContext() }
handle()The OS delivers the redirect to a platform entry point; pass it to the SDK to resume login():
Android (Activity.onNewIntent):
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
intent.data?.let { TelegramLogin.handle(it.toString()) }
}
iOS (SwiftUI):
ContentView().onOpenURL { url in
TelegramLogin.shared.handle(callbackUrl: url.absoluteString)
}
Success.idToken is a Telegram-signed OpenID Connect JWT. Always verify it server-side before trusting any claim:
https://oauth.telegram.org/.well-known/jwks.jsonhttps://oauth.telegram.orgSee Validating ID tokens.
When Telegram isn't installed, the SDK opens the hosted login:
sealed interface TelegramLoginResult {
( idToken: String)
( error: TelegramLoginError)
}
{
NotConfigured
TelegramNotInstalled
NoAuthorizationCode
Cancelled
Server(statusCode)
Network(reason)
Unexpected(detail)
}
Telegram shows the verified badge only when (1) the app is published in the store with the BotFather-registered package/bundle, and (2) login uses the *.tg.dev link (this SDK does). Debug / unpublished builds fall back to the unverified path — test the verified flow on a Play internal-testing track (iOS: TestFlight). This is decided by Telegram, not the SDK.
fallbackScheme.MIT.
id_token.ASWebAuthenticationSession (iOS).configure(), suspend login(), handle(). Typed results & errors.<activity android:name=".MainActivity" android:launchMode="singleTask" android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="app{appid}-login.tg.dev"
android:pathPrefix="/tglogin" />
</intent-filter>
</activity>
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="tg" />
</intent>
</queries>
scopesexpect/actual| Parameter | Description |
|---|
clientId | Required. Your bot's client id — same on both platforms. |
redirectUri | Required, per-platform. The app{appid}-login.tg.dev URL from @BotFather — host differs Android vs iOS; /tglogin on Android, bare host on iOS. |
scopes | Required. e.g. ["openid", "phone"]. |
fallbackScheme | Optional custom scheme for the iOS < 17.4 web fallback. |
handle().ASWebAuthenticationSession with the https callback — zero extra config.fallbackScheme to configure() (and register it), since ASWebAuthenticationSession can't intercept an https callback on older iOS. Without it, login() returns TelegramNotInstalled.Surfaced from shared tags and platforms — no rankings paid for.