Passkeys

Telegram allows creating a passkey on your device to instantly log in to Telegram with a PIN or biometric data like Face ID and fingerprints — instead of an SMS code.

Introduction

Telegram passkey login implements the Web Authentication standard » (webauthn), make sure to familiarize yourself with webauthn by reading the standard », first.

A simpler overview of the standard is available on MDN ».

Put plainly, passkeys are composed of a public-private keypair: the private key is stored safely on the device (for example in a TEE enclave), the public key is sent to Telegram's servers.

When logging in, Telegram sends a challenge, which is signed using the local private key associated to the account: this signature is then verified by Telegram against the public key counterpart; if verification succeeds, the user is logged in.

Note that if the user configured a 2FA password, they will still have to enter it when logging in with a passkey ».

Passkeys can be used on all major browsers and all major platforms (and even outside of a browser).

Passkeys in unofficial Telegram apps

Note that official Telegram apps all use telegram.org as RP ID when generating and requesting passkeys.

This means that unofficial Telegram apps won't be able to use passkeys at all: this can be worked around by modifying the rp.id/rpId of PublicKeyCredentialCreationOptions and PublicKeyCredentialRequestOptions objects returned by the MTProto API, setting it to the app's own domain before passing them to the Web Authentication API.

This will create and look up unofficial passkeys in their own unofficial namespace within the password manager.

The only restriction will be that unofficial clients won't be able to use passkeys generated by official clients, and vice versa, official clients (and other unofficial clients not maintained by you) won't be able to use passkeys generated by your unofficial clients.

Other platforms require additional configuration to work:

  • Android requires the app to be configured with a valid Digital Asset Links file » linking the app to your custom domain in order to use passkeys linked to your domain.
  • iOS requires the app to be configured with a valid Apple App Site Association file » linking the app to your custom domain in order to use passkeys linked to your domain.

Creating a passkey

account.passkeyRegistrationOptions#e16b5ce1 options:DataJSON = account.PasskeyRegistrationOptions;

inputPasskeyResponseRegister#3e63935c client_data:DataJSON attestation_data:bytes = InputPasskeyResponse;
inputPasskeyCredentialPublicKey#3c27b78f id:string raw_id:string response:InputPasskeyResponse = InputPasskeyCredential;

passkey#98613ebf flags:# id:string name:string date:int software_emoji_id:flags.0?long last_usage_date:flags.1?int = Passkey;

---functions---

account.initPasskeyRegistration#429547e8 = account.PasskeyRegistrationOptions;

account.registerPasskey#55b41fd6 credential:InputPasskeyCredential = Passkey;

To create a passkey linked to the currently logged in account, start by invoking account.initPasskeyRegistration.

This method will return a JSON object in account.passkeyRegistrationOptions.options, containing a single key:

Parse the JSON payload, base64url-decoding binary fields for example using PublicKeyCredential.parseCreationOptionsFromJSON.

Then, pass the PublicKeyCredentialCreationOptions » to navigator.credentials.create inside browsers (or equivalent APIs on other platforms): on success, this will generate a new passkey, returning a PublicKeyCredential object.

Some platforms (like Android) may instead return the canonical JSON representation of PublicKeyCredential, which has the same overall structure, with the difference that fields marked as binary below are base64url-encoded and should in some cases be base64url-decoded before being used inside TL constructors.

For simplicity, the documentation will always refer to the JSON representation of the object, which in browsers can be obtained by simply invoking PublicKeyCredential.toJSON.

Take the PublicKeyCredential and transform it into an inputPasskeyCredentialPublicKey as follows:

  1. Extract the AuthenticatorAttestationResponse object contained in PublicKeyCredential.response.
    Use it to generate an inputPasskeyResponseRegister with the following fields:

  2. Then, generate an inputPasskeyCredentialPublicKey with the following fields:

Finally, pass the generated inputPasskeyCredentialPublicKey to account.registerPasskey.

On success, the method will associate the passkey to the current account and it will return a passkey constructor, containing human-readable information about the added passkey.

From this moment, the passkey can be used to safely log into the account without using a verification code », by using the private key safely and locally stored in the passkey to authenticate the user.

Note that if the user configured a 2FA password, they will still have to enter it even when logging in with a passkey.

List passkeys

passkey#98613ebf flags:# id:string name:string date:int software_emoji_id:flags.0?long last_usage_date:flags.1?int = Passkey;

account.passkeys#f8e0aa1c passkeys:Vector<Passkey> = account.Passkeys;

---functions---

account.getPasskeys#ea1f0c52 = account.Passkeys;

Use account.getPasskeys to list the passkeys that can be used to log into the current account.

Passkeys are represented by passkey constructors, containing info like the passkey name, its ID, a custom emoji » used as icon (usually coincides with the password manager's icon), its creation date and the date when it was last used.

Delete passkeys

---functions---

account.deletePasskey#f5b5563f id:string = Bool;

To delete a passkey associated to the current account, use account.deletePasskey, passing the passkey's ID (usually obtained using account.getPasskeys as described above »).

Logging in with a passkey

auth.passkeyLoginOptions#e2037789 options:DataJSON = auth.PasskeyLoginOptions;

inputPasskeyResponseLogin#c31fc14a client_data:DataJSON authenticator_data:bytes signature:bytes user_handle:string = InputPasskeyResponse;
inputPasskeyCredentialPublicKey#3c27b78f id:string raw_id:string response:InputPasskeyResponse = InputPasskeyCredential;

auth.authorization#2ea2c0d4 flags:# setup_password_required:flags.1?true otherwise_relogin_days:flags.1?int tmp_sessions:flags.0?int future_auth_token:flags.2?bytes user:User = auth.Authorization;

---functions---

auth.initPasskeyLogin#518ad0b7 api_id:int api_hash:string = auth.PasskeyLoginOptions;

auth.finishPasskeyLogin#9857ad07 flags:# credential:InputPasskeyCredential from_dc_id:flags.0?int from_auth_key_id:flags.0?long = auth.Authorization;

To login with a passkey, start by invoking auth.initPasskeyLogin.

Store the DC ID that was used to make the query into a initDcId variable, to be used later.

This method will return a JSON object in auth.passkeyLoginOptions.options, containing a single key:

Parse the JSON payload, base64url-decoding binary fields for example using PublicKeyCredential.parseRequestOptionsFromJSON.

Then, pass the PublicKeyCredentialRequestOptions » to navigator.credentials.get inside browsers (or equivalent APIs on other platforms).

This will prompt the user to choose a Telegram passkey (choosing the account to log into), and will return it as a PublicKeyCredential object.

Some platforms (like Android) may instead return the canonical JSON representation of PublicKeyCredential, which has the same overall structure, with the difference that fields marked as binary below are base64url-encoded and should in some cases be base64url-decoded before being used inside TL constructors.

Take the PublicKeyCredential and transform it into an inputPasskeyCredentialPublicKey using the following steps:

  1. Extract the AuthenticatorAssertionResponse object contained in PublicKeyCredential.response.
    Use it to generate an inputPasskeyResponseLogin with the following fields:

  2. Then, generate an inputPasskeyCredentialPublicKey with the following fields:

  3. Take the user_handle stored in the generated inputPasskeyResponseLogin (inputPasskeyCredentialPublicKey.response.user_handle) and parse it using the following printf/scanf format string: %d:%lld.

    The user handle is composed by the DC ID (a 32-bit integer) and the user ID (a 64-bit long), separated by a colon (:): store these values in separate variables (userDcId and userId) to be used later.

If the current client is already logged into an account with user ID equal to userId, ask the user to choose a different passkey (if any) and restart the passkey login process.

Otherwise, send a auth.finishPasskeyLogin query to DC userDcId, passing the following arguments:

  • credential - The inputPasskeyCredentialPublicKey generated above.
  • from_dc_id - If and only if userDcId != initDcId (i.e. userDcId is not equal to the DC ID that was used to invoke auth.initPasskeyLogin), populate this field with initDcId
  • from_auth_key_id - If and only if userDcId != initDcId (i.e. userDcId is not equal to the DC ID that was used to invoke auth.initPasskeyLogin), populate this field with the auth key ID of the connection to initDcId (if PFS is in use, use the permanent auth key ID).

auth.finishPasskeyLogin can throw the following RPC errors:

  • SESSION_PASSWORD_NEEDED - If the user configured a 2FA password, they will still have to enter it even when logging in with a passkey: simply proceed with the usual 2FA auth flow » to login.
  • PASSKEY_CREDENTIAL_NOT_FOUND - The specified passkey cannot be found on the server (for example, it could've been removed » by the user).

On success (except for the 2FA case), the user is logged in, and the method directly returns an auth.authorization constructor.