Authentication
Learn how to implement "Sign in with GLIN" - a wallet-based authentication system similar to "Sign in with Ethereum" or OAuth.
π― What is "Sign in with GLIN"?β
"Sign in with GLIN" allows users to authenticate using their GLIN wallet instead of traditional username/password. This provides:
- π No passwords needed - Users authenticate with their existing wallet
- π€ User-owned identity - Users control their own data
- π Cryptographic security - Signatures prove identity without revealing private keys
- π Cross-platform - Works with browser extensions and mobile wallets
How It Worksβ
βββββββββββββββ ββββββββββββββββ βββββββββββ
β Your App ββββββββββΆβ GLIN Wallet ββββββββββΆβBackend β
β (Frontend) β β Extension β β Server β
βββββββββββββββ ββββββββββββββββ βββββββββββ
β β β
β 1. Request signature β β
βββββββββββββββββββββββββΆβ β
β β β
β 2. User approves β β
β 3. Return signature β β
ββββββββββββββββββββββββββ€ β
β β β
β 4. Send to backend for verification β
ββββββββββββββββββββββββββΌββββββββββββββββββββββββΆβ
β β β
β 5. Verify signature & create session β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β β
β 6. User authenticated β β
Frontend Implementationβ
Step 1: Request Signature from Walletβ
- TypeScript
- Rust
auth.ts
import { GlinAuth } from '@glin-ai/sdk';
async function signIn() {
const auth = new GlinAuth();
// Request signature from wallet
const result = await auth.authenticate('Your App Name');
console.log('Address:', result.address);
console.log('Signature:', result.signature);
console.log('Message:', result.message);
return result;
}
The authenticate()
method:
- Detects the GLIN wallet extension
- Requests the user's address
- Creates a unique message to sign
- Prompts the user to sign the message
- Returns the signature and address
auth.rs
use glin_client::{account_from_seed, sign_message};
use anyhow::Result;
pub fn sign_auth_message(seed: &str, app_name: &str) -> Result<(String, String, String)> {
// Create account from seed
let account = account_from_seed(seed)?;
// Create message to sign
let message = format!(
"Sign in to {}\n\nAddress: {}\nNonce: {}",
app_name,
account.public_key(),
generate_nonce()
);
// Sign the message
let signature = sign_message(&account, &message)?;
Ok((
account.public_key().to_string(),
hex::encode(signature),
message
))
}
fn generate_nonce() -> String {
use std::time::SystemTime;
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string()
}
Backend Focus
Rust SDK is typically used for backend services. For frontend auth, use TypeScript SDK with browser wallet integration.
Step 2: Send to Backendβ
- TypeScript
- Rust
auth.ts
async function authenticateUser() {
// Get signature from wallet
const auth = new GlinAuth();
const { address, signature, message } = await auth.authenticate('Your App');
// Send to backend
const response = await fetch('/api/auth/glin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, signature, message })
});
if (!response.ok) {
throw new Error('Authentication failed');
}
const data = await response.json();
console.log('Authenticated! Session ID:', data.sessionId);
return data;
}
auth_client.rs
use reqwest;
use serde::{Serialize, Deserialize};
use anyhow::Result;
#[derive(Serialize)]
struct AuthRequest {
address: String,
signature: String,
message: String,
}
#[derive(Deserialize)]
struct AuthResponse {
session_id: String,
address: String,
}
pub async fn authenticate(address: String, signature: String, message: String) -> Result<AuthResponse> {
let client = reqwest::Client::new();
let response = client
.post("https://api.yourapp.com/auth/glin")
.json(&AuthRequest { address, signature, message })
.send()
.await?;
let auth_response = response.json::<AuthResponse>().await?;
println!("Authenticated! Session: {}", auth_response.session_id);
Ok(auth_response)
}
Backend Verificationβ
Step 3: Verify Signatureβ
- TypeScript (Node.js)
- Rust (Axum)
api/auth/glin.ts
import { GlinAuth } from '@glin-ai/sdk';
export async function POST(req: Request) {
const { address, signature, message } = await req.json();
// Verify the signature
const isValid = GlinAuth.verifySignature(address, message, signature);
if (!isValid) {
return Response.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
// Signature is valid! Create session
const sessionId = await createSession(address);
return Response.json({
success: true,
sessionId,
address
});
}
async function createSession(address: string): Promise<string> {
// Store session in your database
// Return session ID
return 'session_' + Date.now();
}
routes/auth.rs
use axum::{Json, http::StatusCode};
use serde::{Deserialize, Serialize};
use glin_client::verify_signature;
#[derive(Deserialize)]
pub struct AuthRequest {
address: String,
signature: String,
message: String,
}
#[derive(Serialize)]
pub struct AuthResponse {
success: bool,
session_id: String,
address: String,
}
pub async fn glin_auth(
Json(payload): Json<AuthRequest>,
) -> Result<Json<AuthResponse>, StatusCode> {
// Verify signature
let signature_bytes = hex::decode(&payload.signature)
.map_err(|_| StatusCode::BAD_REQUEST)?;
let is_valid = verify_signature(
&payload.address,
&payload.message,
&signature_bytes,
).map_err(|_| StatusCode::UNAUTHORIZED)?;
if !is_valid {
return Err(StatusCode::UNAUTHORIZED);
}
// Create session
let session_id = create_session(&payload.address).await;
Ok(Json(AuthResponse {
success: true,
session_id,
address: payload.address,
}))
}
async fn create_session(address: &str) -> String {
// Store session in database
format!("session_{}", chrono::Utc::now().timestamp())
}
Complete Exampleβ
React + Next.js Exampleβ
app/auth/page.tsx
'use client';
import { useState } from 'react';
import { GlinAuth } from '@glin-ai/sdk';
export default function AuthPage() {
const [user, setUser] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function handleSignIn() {
setLoading(true);
try {
// 1. Get signature from wallet
const auth = new GlinAuth();
const { address, signature, message } = await auth.authenticate('My App');
// 2. Send to backend
const response = await fetch('/api/auth/glin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, signature, message }),
});
if (!response.ok) throw new Error('Auth failed');
const data = await response.json();
// 3. Save session and update UI
localStorage.setItem('sessionId', data.sessionId);
setUser(address);
} catch (error) {
console.error('Sign in failed:', error);
alert('Sign in failed. Please try again.');
} finally {
setLoading(false);
}
}
if (user) {
return (
<div>
<h1>Welcome!</h1>
<p>Address: {user}</p>
<button onClick={() => {
setUser(null);
localStorage.removeItem('sessionId');
}}>
Sign Out
</button>
</div>
);
}
return (
<div>
<h1>Sign In</h1>
<button onClick={handleSignIn} disabled={loading}>
{loading ? 'Signing in...' : 'Sign in with GLIN'}
</button>
</div>
);
}
API Route (Next.js App Router)β
app/api/auth/glin/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { GlinAuth } from '@glin-ai/sdk';
export async function POST(req: NextRequest) {
try {
const { address, signature, message } = await req.json();
// Verify signature
const isValid = GlinAuth.verifySignature(address, message, signature);
if (!isValid) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
// Create session (implement your session logic)
const sessionId = crypto.randomUUID();
// In production, store session in database:
// await db.sessions.create({ id: sessionId, address, expiresAt: ... });
return NextResponse.json({
success: true,
sessionId,
address
});
} catch (error) {
console.error('Auth error:', error);
return NextResponse.json(
{ error: 'Authentication failed' },
{ status: 500 }
);
}
}
Security Best Practicesβ
β Do'sβ
- Verify signatures on the backend - Never trust the frontend
- Use nonces - Prevent replay attacks with unique messages
- Set session expiry - Don't createζ°ΈδΉ sessions
- Store sessions securely - Use httpOnly cookies or secure storage
- Rate limit - Prevent brute force attacks
β Don'tsβ
- Don't store private keys - Users own their keys
- Don't trust the address - Always verify the signature
- Don't skip HTTPS - Always use secure connections
- Don't reuse messages - Each signature should be unique
- Don't expose session IDs - Keep them secure
Message Formatβ
The signed message should include:
const message = `
Sign in to ${appName}
Address: ${address}
Nonce: ${nonce}
Issued At: ${timestamp}
`;
Example:
Sign in to My DApp
Address: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
Nonce: abc123xyz789
Issued At: 2025-10-05T10:00:00Z
Error Handlingβ
- TypeScript
- Rust
try {
const auth = new GlinAuth();
const result = await auth.authenticate('My App');
} catch (error) {
if (error.message.includes('No wallet found')) {
// Wallet extension not installed
alert('Please install GLIN wallet extension');
} else if (error.message.includes('User rejected')) {
// User cancelled the signature
console.log('User cancelled sign in');
} else {
// Other error
console.error('Sign in failed:', error);
}
}
match sign_auth_message(seed, "My App") {
Ok((address, signature, message)) => {
println!("Signed successfully!");
}
Err(e) => {
eprintln!("Sign in failed: {}", e);
// Handle specific errors
if e.to_string().contains("Invalid seed") {
eprintln!("Invalid account seed");
}
}
}
Next Stepsβ
Troubleshootingβ
Wallet Not Detectedβ
Make sure the GLIN wallet extension is installed:
import { GlinAuth } from '@glin-ai/sdk';
const auth = new GlinAuth();
const hasWallet = await auth.hasWallet();
if (!hasWallet) {
// Show install instructions
window.open('https://wallet.glin.ai', '_blank');
}
Signature Verification Failsβ
Common issues:
- Message doesn't match exactly (including whitespace)
- Signature is corrupted or incorrectly encoded
- Using wrong address/public key format
Session Managementβ
For production apps, use established session libraries:
- Next.js:
next-auth
oriron-session
- Express:
express-session
- Rust:
tower-sessions
oraxum-sessions
Need help? Join our Discord or check out the complete example.