Login Widget
Login Widget allows you to embed a SafeW login button on your website, enabling users to authorize and sign in with their SafeW account in one click.
Prerequisites
- Create a Bot via BotFather and obtain a Bot Token
- Use the
/setdomaincommand to bind your website domain
TIP
It's recommended to set the Bot avatar to match your website logo to build user trust.
Embedding the Widget
Login Widget supports two modes: Callback mode and Redirect mode.
Callback Mode
Invokes a JavaScript callback function on the page after authorization. Ideal for single-page applications (SPA):
<script async src="https://safew.com/js/safew-widget.js"
data-safew-login="YOUR_BOT_USERNAME"
data-size="large"
data-onauth="onSafeWAuth(user)"
data-request-access="write">
</script>
<script>
function onSafeWAuth(user) {
console.log('User logged in:', user);
// Send user data to your server for verification
fetch('/auth/safew', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
});
}
</script>Redirect Mode
Redirects to a specified URL after authorization, with user data passed as query parameters:
<script async src="https://safew.com/js/safew-widget.js"
data-safew-login="YOUR_BOT_USERNAME"
data-size="large"
data-auth-url="https://example.com/auth/safew"
data-request-access="write">
</script>After successful authorization, the user will be redirected to:
https://example.com/auth/safew?id=12345&first_name=John&username=john_doe&photo_url=...&auth_date=1234567890&hash=abc123...Configuration Options
| Attribute | Type | Required | Description |
|---|---|---|---|
data-safew-login | String | Yes | Bot username (without @) |
data-size | String | No | Button size: large, medium, small. Default: large |
data-userpic | Boolean | No | Whether to show user avatar. Default: true |
data-radius | Integer | No | Button border radius (px) |
data-onauth | String | No | Callback function name for Callback mode |
data-auth-url | String | No | Redirect URL for Redirect mode |
data-request-access | String | No | Set to write to request permission to send messages to the user |
WARNING
data-onauth and data-auth-url are mutually exclusive. If neither is set, the Widget will display the button but won't handle the authorization result.
User Data Returned
After successful authorization, you will receive the following user data:
| Field | Type | Description |
|---|---|---|
id | Integer | Unique user ID |
first_name | String | User's first name |
last_name | String | User's last name (optional) |
username | String | Username (optional) |
photo_url | String | User avatar URL (optional) |
auth_date | Integer | Authorization time (Unix timestamp) |
hash | String | Data verification hash |
Receiving Data in Callback Mode
function onSafeWAuth(user) {
// user object contains all fields above
console.log(user.id); // 12345
console.log(user.first_name); // "John"
console.log(user.auth_date); // 1234567890
console.log(user.hash); // "abc123..."
}Receiving Data in Redirect Mode
// Parse user data from URL query parameters
const params = new URLSearchParams(window.location.search);
const user = {
id: params.get('id'),
first_name: params.get('first_name'),
last_name: params.get('last_name'),
username: params.get('username'),
photo_url: params.get('photo_url'),
auth_date: params.get('auth_date'),
hash: params.get('hash')
};Verifying Authorization Data
WARNING
You must verify authorization data on the server side. Never trust data from the client — attackers can forge arbitrary user information.
Verification Steps
- Extract the
hashfield from the received data - Sort the remaining fields alphabetically, join them as
key=valuepairs separated by newline\nto formdata_check_string - Compute
secret_keyby taking the SHA256 hash of the Bot Token - Compute HMAC-SHA256 of
data_check_stringusingsecret_key - Compare the result with the
hashfield
Difference from Mini App Data Validation
Login Widget uses SHA256(bot_token) as the HMAC key, while Mini App Data Validation uses HMAC-SHA256("WebAppData", bot_token) as the key. They are not interchangeable.
Python Example
import hashlib
import hmac
def validate_login_widget(data: dict, bot_token: str) -> bool:
"""Validate Login Widget authorization data"""
# Extract hash
received_hash = data.pop('hash', None)
if not received_hash:
return False
# Sort alphabetically and build data_check_string
data_check_pairs = []
for key in sorted(data.keys()):
data_check_pairs.append(f"{key}={data[key]}")
data_check_string = "\n".join(data_check_pairs)
# Compute secret_key (note: SHA256, not HMAC)
secret_key = hashlib.sha256(bot_token.encode()).digest()
# Compute and compare hash
computed_hash = hmac.new(
secret_key,
data_check_string.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed_hash, received_hash)Node.js Example
const crypto = require('crypto');
function validateLoginWidget(data, botToken) {
const hash = data.hash;
delete data.hash;
// Sort alphabetically and build data_check_string
const dataCheckString = Object.keys(data)
.sort()
.map(key => `${key}=${data[key]}`)
.join('\n');
// Compute secret_key (SHA256)
const secretKey = crypto
.createHash('sha256')
.update(botToken)
.digest();
// Compute and compare hash
const computedHash = crypto
.createHmac('sha256', secretKey)
.update(dataCheckString)
.digest('hex');
return computedHash === hash;
}Go Example
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"sort"
"strings"
)
func ValidateLoginWidget(data map[string]string, botToken string) bool {
receivedHash, ok := data["hash"]
if !ok {
return false
}
delete(data, "hash")
// Sort alphabetically
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
}
sort.Strings(keys)
pairs := make([]string, 0, len(keys))
for _, k := range keys {
pairs = append(pairs, fmt.Sprintf("%s=%s", k, data[k]))
}
dataCheckString := strings.Join(pairs, "\n")
// Compute secret_key (SHA256)
h := sha256.Sum256([]byte(botToken))
secretKey := h[:]
// Compute and compare hash
mac := hmac.New(sha256.New, secretKey)
mac.Write([]byte(dataCheckString))
computedHash := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(computedHash), []byte(receivedHash))
}Security Recommendations
- Server-side verification — Always verify
hashon the server. Never trust data from the client - Check auth_date — Reject expired authorization data (recommended validity: no more than 1 hour)
- Constant-time comparison — Use
hmac.compare_digest(Python) orcrypto.timingSafeEqual(Node.js) to prevent timing attacks - Protect Bot Token — Never expose the Bot Token in client-side code
- Use HTTPS — Ensure your website uses HTTPS to prevent data tampering in transit
// Complete server-side verification example
const crypto = require('crypto');
function verifyAndAuthenticate(data, botToken) {
// 1. Verify hash
const hashCopy = data.hash;
const dataCopy = { ...data };
delete dataCopy.hash;
const dataCheckString = Object.keys(dataCopy)
.sort()
.map(key => `${key}=${dataCopy[key]}`)
.join('\n');
const secretKey = crypto
.createHash('sha256')
.update(botToken)
.digest();
const computedHash = crypto
.createHmac('sha256', secretKey)
.update(dataCheckString)
.digest('hex');
// Constant-time comparison
if (!crypto.timingSafeEqual(
Buffer.from(computedHash, 'hex'),
Buffer.from(hashCopy, 'hex')
)) {
return { valid: false, error: 'Invalid hash' };
}
// 2. Check auth_date
const authDate = parseInt(data.auth_date);
const now = Math.floor(Date.now() / 1000);
if (now - authDate > 3600) {
return { valid: false, error: 'Data expired' };
}
return { valid: true, user: data };
}