@solidus-network/auth-otp
Saf bir OTP (tek kullanımlık kod) giriş çekirdeği. 6 haneli kodlar üretir, saklar ve doğrular, ardından teslimatı ve kimlik çözümlemesini iki küçük arayüz aracılığıyla size geri devreder.
Sıfır çalışma zamanı bağımlılığı vardır ve tasarım gereği tek başına hiçbir işe yaramaz. Kendi göndericinizi ve kendi oturum düzenleyicinizi siz getirirsiniz.
npm install @solidus-network/auth-otpimport { startLogin, completeLogin, memoryOtpStore } from '@solidus-network/auth-otp'Kasıtlı olarak yapmadığı şeyler
Buna göre tasarım yapmadan önce bunu okuyun — bunlar sonraki bir yamada doldurulacak boşluklar değil, reddedişlerdir.
| It does not | Because |
|---|---|
| Herhangi bir şey göndermez. Hiçbir zaman Twilio, Vonage, Resend veya SES içe aktarımı yoktur. | Teslimat makbuzları, gönderici kimlikleri ve bölgesel yönlendirme sizin bağdaştırıcınıza aittir. OtpSender’ı siz uygularsınız. |
| Kimliği çözümlemez veya basmaz. | İlk kez görülen bir telefon numarası için bir did:solidus basmak, bir anahtar çifti üretmek ve gözetim altında tutmak anlamına gelir. Bu paket, bu kararı sizin adınıza vermeyi reddeder — resolvePrincipal’i siz uygularsınız. |
Bir oturum düzenlemez. completeLogin, { did, isNew }’de durur. | Kendi oturum token’ınızı, arka ucunuzun zaten sahip olduğu imzalayıcıyla imzalayın. |
0.1.0’da hiçbir SMS göndericisi gönderilmez.
channelparametresi'sms'’i kabul eder ve çekirdek kanaldan bağımsızdır — ancak bu pakette gerçek bir SMS bağdaştırıcısı yoktur. E-posta kanalı, bugün bağladığınız herhangi birOtpSenderüzerinden tamamen kullanılabilir. Üretim düzeyinde bir SMS bağdaştırıcısı bir sağlayıcı hesabı ve bir test cihazı gerektirir, dolayısıyla bu eksik bir içe aktarım değil, ayrı bir iş parçasıdır.
startLogin
Bir kod üretir, saklar ve göndericinize teslim eder. Hiçbir şey döndürmez — kod, hiçbir zaman bu fonksiyonun dokunduğu dönüş değerinin veya herhangi bir günlüğün parçası olmaz.
startLogin(
deps: {
store: OtpStore
sender: OtpSender
now: number
rng?: () => number
expiresInMs?: number
},
args: { channel: 'sms' | 'email'; to: string }
): Promise<void>Parametreler
| Name | Type | Default | Description |
|---|---|---|---|
deps.store | OtpStore | — | Hash’lenmiş kodun tutulduğu yer |
deps.sender | OtpSender | — | Teslimat bağdaştırıcınız |
deps.now | number | — | Enjekte edilmiş geçerli an — paket asla Date.now() çağırmaz |
deps.rng | () => number | Math.random | [0, 1) aralığında bir float döndürmelidir. Üretimde bir CSPRNG enjekte edin |
deps.expiresInMs | number | — | Kod ömrü |
args.channel | 'sms' | 'email' | — | Göndericinize olduğu gibi iletilir |
args.to | string | — | Telefon numarası veya e-posta — aynı zamanda depo anahtarı |
sender.send tarafından fırlatılan bir hata, bir ret olarak yayılır.
Örnek
import { startLogin, memoryOtpStore } from '@solidus-network/auth-otp'
import { webcrypto } from 'node:crypto'
const store = memoryOtpStore()
// Your delivery adapter — the only place a real code appears.
const sender = {
async send(channel: 'sms' | 'email', to: string, code: string) {
await resend.emails.send({
from: '[email protected]',
to,
subject: 'Your code',
text: `Your code is ${code}. It expires in 5 minutes.`,
})
},
}
// A CSPRNG, not Math.random.
const rng = () => webcrypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32
await startLogin(
{ store, sender, now: Date.now(), rng, expiresInMs: 5 * 60_000 },
{ channel: 'email', to: '[email protected]' },
)completeLogin
Kodu doğrular ve — yalnızca başarı durumunda — to’nun arkasındaki DID’i bulmak veya basmak
için resolvePrincipal’inizi çağırır.
completeLogin(
deps: {
store: OtpStore
resolvePrincipal: (to: string) => Promise<{ did: string; isNew: boolean }>
now: number
maxAttempts?: number
},
args: { to: string; code: string }
): Promise<{ did: string; isNew: boolean } | { error: 'expired' | 'wrong' | 'locked' | 'none' }>Sonucu error kontrolüyle ayırt edin — bir ok bayrağı yoktur.
Örnek
const result = await completeLogin(
{
store,
now: Date.now(),
maxAttempts: 5,
async resolvePrincipal(to) {
const existing = await db.users.findByEmail(to)
if (existing) return { did: existing.did, isNew: false }
const did = await mintDidWithYourOwnCustodyModel(to)
return { did, isNew: true }
},
},
{ to: '[email protected]', code: '481920' },
)
if ('error' in result) {
return reply.code(401).send({ error: result.error })
}
// auth-otp stops here. Signing the session is your job.
const token = await signSession({ sub: result.did })verifyOtp
completeLogin kullanmıyorsanız, daha düşük seviyeli kontrol.
verifyOtp(
store: OtpStore,
args: { to: string; code: string; now: number; maxAttempts: number }
): Promise<VerifyOtpResult>VerifyOtpResult, { ok: true } veya { ok: false; reason: 'expired' | 'wrong' | 'locked' | 'none' }’dir.
| Situation | Result | Record after |
|---|---|---|
to için kayıt yok | reason: 'none' | — |
exp’i geçmiş | reason: 'expired' | Tüketildi — süresi dolmuş bir kod yeniden denenemez |
| Doğru kod | ok: true | Tüketildi — tek kullanımlık |
Yanlış, hâlâ maxAttempts altında | reason: 'wrong' | Kalıcı olur, attempts + 1 |
Yanlış, maxAttempts’te veya üzerinde | reason: 'locked' | Tüketildi — doğru kodla bile başka yeniden deneme yok |
Son kullanma, startLogin’e ilettiğiniz aynı saat alanında (clock domain), enjekte edilmiş now’a
karşı değerlendirilir.
Depolama
interface OtpStore {
put(to: string, record: OtpRecord): Promise<void>
get(to: string): Promise<OtpRecord | undefined>
del(to: string): Promise<void>
}
interface OtpRecord {
codeHash: string // SHA-256 hex — never the plaintext code
exp: number // absolute expiry instant
attempts: number // failed verifications so far
}memoryOtpStore(), Map destekli bir referans uygulamasıdır. Süreçler arasında güvenli değildir
— çok örnekli bir dağıtım, aynı üç yöntemin arkasında Redis, Postgres veya eşdeğerine ihtiyaç duyar.
hashOtp hakkında — hash’lemenin size sağladığı ve sağlamadığı şey
Kodlar, tuzlanmamış (unsalted) bir SHA-256 özeti olarak saklanır. Bunun değeri konusunda kesin olmak gerekirse:
- Kodları bir DB tarayıcısında, bir destek talebi ekran görüntüsünde, depoyu alan bir günlük toplayıcıda veya bir yedeklemede düz görünürlükten uzak tutar — yaygın “düz metin olarak orada duruyordu” sızıntısı.
- Çevrimdışı kaba kuvvete karşı dayanıklı değildir. 6 haneli bir kodun yalnızca 1.000.000 olası değeri vardır; bir milyon hash’in tamamı bir saniyenin çok altında hesaplanır. Bir depo dökümüne sahip bir saldırgan, tüm canlı kodları kurtarır.
Çevrimiçi bir tahmin saldırısını gerçekte sınırlayan şey, hash değil, verifyOtp’nin maxAttempts
kilitlenmesidir.
Test Edilebilirlik
Zamana ve rastgeleliğe bağlı her girdi enjekte edilir — çekirdekte hiçbir yerde Date.now() veya
Math.random() yoktur. Sabit bir now ve deterministik bir rng iletin, tüm giriş akışı sahte
zamanlayıcılar olmadan tam olarak yeniden üretilebilir hâle gelir.
const codes = ['481920']
let i = 0
const rng = () => Number(codes[i++]) / 1_000_000
await startLogin({ store, sender, now: 1_700_000_000_000, rng }, { channel: 'email', to: '[email protected]' })Sonraki adımlar
- @solidus-network/auth — zaten bir DID’e sahip kullanıcılar için DID meydan okuma-yanıt
- @solidus-network/sdk — bir OTP girişinin bir DID bastığı kimlik bilgilerini düzenleyin ve doğrulayın