@elysiumjs/themis (英语)
认证 极乐世界 应用:密码,指使,会话,卫士,CSRF,TOTP,OAuth/OpenID Connection和密码. 它运行于邦,节点.js和Cloudflare工人.
安装
bun add @elysiumjs/themis
# Sessions in Redis or Workers KV
bun add @elysiumjs/themis-redis # or @elysiumjs/themis-workers
设置
import { Application, Middleware } from '@elysiumjs/core';
import { Redis } from '@elysiumjs/redis';
import {
apiKeyGuard,
CsrfMiddleware,
jwtGuard,
sessionGuard,
SessionMiddleware,
ThemisModule
} from '@elysiumjs/themis';
import { RedisSessionStore } from '@elysiumjs/themis-redis';
@Application.register({
modules: [
ThemisModule.with({
secret: env.APP_SECRET,
session: {
store: new RedisSessionStore({ connection: () => Redis.connection() }),
ttl: 60 * 60 * 24 * 7
},
guards: {
session: sessionGuard({ user: (id) => users.find(id) }),
jwt: jwtGuard({ secret: env.JWT_SECRET, user: (claims) => users.find(claims.sub) }),
apiKey: apiKeyGuard({ find: (hash) => keys.ownerOf(hash) })
}
}),
AppModule
]
})
@Middleware.register(SessionMiddleware, CsrfMiddleware)
class App extends Application {}
secret 签名会话 cookie, 加密 cookie 会话并绑定 CSRF 令牌 。 在环境上保密。 32 字节以下的秘密在启动时被拒绝(WeakSecretError),更短的JWT和已签名的URL秘密也一样.
认证
import type { AuthUser } from '@elysiumjs/themis';
import { Http } from '@elysiumjs/core';
import { Auth } from '@elysiumjs/themis';
declare module '@elysiumjs/themis' {
interface AuthUser {
id: string;
}
}
@Http.controller('/me')
@Auth()
class MeController {
@Http.get()
public me(@Auth.user() user: AuthUser) {
return { id: user.id };
}
@Http.get('/tokens')
@Auth({ guards: ['jwt'] })
public tokens() {
/* ... */
}
}
@Auth()按顺序尝试配置的守护者@Auth({ guards })只有这些名字 守护是执行Guard,或者一个警卫班,这个班次通过服务集装箱建造一次,以便其建造者得到注射服务。 登记AuthMiddleware这样@Middleware.register(AuthMiddleware.guards(['jwt']))与@Auth({ guards: ['jwt'] })。 。 。 .- 一个要求没有警卫接受一个401问题的答案.
@Auth({ optional: true })让它通过,并Auth.check()告诉用户是否被签名 . - 用户可通过
@Auth.user()在一个处理器参数上,Auth.currentUser()财务报告和审定财务报表ctx.currentUser这样cerberus就可以读它与getSubject: (ctx) => ctx.currentUser。通过增加AuthUser接口。 后面,我们走@Auth({ optional: true }),键入参数AuthUser | undefined。 。 。 . Auth.login(id)在会话上签名一个用户(并重新生成)Auth.logout()把他们签出来.
会议
import { Session } from '@elysiumjs/themis';
const session = Session.current();
session.set('cart', items);
session.flash('notice', 'Saved');
session.regenerate();
- 这些商店是
MemorySessionStore, (中文(简体) ),RedisSessionStore联 合 国@elysiumjs/themis-redis财务报告和审定财务报表KvSessionStore联 合 国@elysiumjs/themis-workers(a) 工人KV装订)。 没有商店,会话就活在过程的记忆中,他们就会在外部测试中警告. store: 'cookie'将整个会话保存在 AES- GCM 加密饼干中 。 它必须保持在4000个字符以下(SessionTooLargeError),而所复制的饼干在过期前仍然有效,所以在商店中保留可撤销的会话.- 会话饼干已经签了,
HttpOnly, (中文(简体) ),SameSite=Lax财务报告和审定财务报表Secure默认. - 处理器运行后会话被保存,当请求以客户端出错(4xx)而结束时也会被保存,例如签名后出现抛出的问题.
中国证监会
CsrfMiddleware 以会话中保存的同步令牌保护 cookie 会话 。 不安全的方法需要 x-csrf-token 页眉或 _csrf 要匹配的字段 Csrf.token()或者他们回答一个403个问题.
密码
import { Password } from '@elysiumjs/themis';
const hash = await Password.hash(input);
if ((await Password.verify(input, hash)) && Password.needsRehash(hash)) {
await users.updateHash(id, await Password.hash(input));
}
散热器为rg2id(BunPasswordHasher) Bun和PBKDF2-SHA256号机上安装的Pbkdf2PasswordHasher在其他地方,有60万次重复(在Cloudflare Workers上为100,000次,在此处为PBKDF2上限). ScryptPasswordHasher从 @elysiumjs/themis/scrypt, 用途 node:crypto选择一个 Password.use(hasher)Hashes描述它们的格式,所以 verify 读取所有三个.
键
import { ApiKeys, Jwt, SignedUrl } from '@elysiumjs/themis';
const token = await Jwt.sign({ role: 'admin' }, { secret, subject: user.id, expiresIn: 3600 });
const claims = await Jwt.verify(token, { secret });
const { key, hash } = await ApiKeys.generate({ prefix: 'sk' }); // store the hash, show the key once
await ApiKeys.verify(key, hash);
const link = await SignedUrl.sign('https://app.example.com/download/1', { secret, expiresIn: 600 });
await SignedUrl.verify(link, { secret });
Jwt 带有密钥(HS256)或不对称密钥及其算法的符号。 无效或过期的符是401个问题,无效或过期的链接是403个问题.
Jwt.verify 未过期即拒签; 通过 requiredClaims (a) 更改必须携带的凭证([] 接受一个没有 exp) (中文(简体) ). currentDate 对照另一时间检查过期日期 issuedAt 从另一个时间开始.
使用时读取的秘密
每一个秘密 ThemisModule 秘密,JWT秘密和已签名的 URL 秘密)可以是函数,同步或async,每次需要该秘密时都会被调用. 从一个与请求相联的商店读取,比如Cloudflare Workers上绑有的秘密,一个旋转的秘密会被取出而不重启. 每个读数都检查32字节的最小值.
ThemisModule.with({ secret: () => secrets.get('app-secret') });
在旋转时,用当前键,然后用上一个键进行校验:
const claims = await Jwt.verify(token, {
keys: [
{ secret: () => secrets.get('jwt-current') },
{ secret: () => secrets.get('jwt-previous') }
]
});
密钥按顺序进行,直到有人验证了签名;过期的令牌或错误的主张立即被拒绝.
二要素代码
import { BackupCodes, Totp } from '@elysiumjs/themis';
const secret = Totp.generateSecret();
const qr = Totp.uri({ secret, issuer: 'Acme', account: user.email });
const counter = await Totp.verify(code, secret, { window: 1 }); // null when wrong
const { codes, hashes } = await BackupCodes.generate(10);
const { valid, remaining } = await BackupCodes.consume(typed, hashes);
存储柜台 Totp.verify 返回并拒绝一个计数器没有更大的代码,这样一个代码就一次起作用.
普通的散列车让任何有表格测试副本的人猜测下线. 传递一个用 HMAC 来散开代码的密钥, 以及使用这些密钥 :
const { codes, hashes } = await BackupCodes.generate(8, {
secret: () => secrets.get('backup-codes')
});
const { valid, used } = await BackupCodes.consume(typed, hashes, {
secret: () => secrets.get('backup-codes')
});
used 是匹配的散列代码,以标记所使用的代码.
OAuth 和 OpenID 连接
import { OAuthClient } from '@elysiumjs/themis';
const google = new OAuthClient({
issuer: 'https://accounts.google.com',
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
redirectUri: 'https://api.example.com/auth/google/callback'
});
// GET /auth/google
const url = await google.authorize();
// GET /auth/google/callback
const { tokens, claims, userInfo } = await google.callback(request.url);
客户端使用PKCE的授权代码流. 状态,验证器和OpenID Connectnce保存在会话中. 一个没有发现的提供者需要 authorizationServer 和它的终点。 错误状态或交换失败的回调回答400个问题. 它的细节名称是 OAuth 错误代码, 提供者回答; 其它的失败都会被记录下来, 用固定的细节回答 .
密码
import { Passkeys } from '@elysiumjs/themis';
const passkeys = new Passkeys({
rpName: 'Acme',
rpID: 'example.com',
origin: 'https://example.com',
repository: new PasskeyTable()
});
const options = await passkeys.registrationOptions({ id: user.id, name: user.email });
const record = await passkeys.verifyRegistration(response);
const challenge = await passkeys.authenticationOptions();
const used = await passkeys.verifyAuthentication(assertion); // used.userId signs the user in
本届会议继续面临挑战。 应用程序通过执行存储证书 PasskeyRepository。无法验证400个问题的答案. updateCounter 获取上一个计数器: 答案 false 当存储的不再是它, 而输掉比赛的签入被拒绝.
一个在两个调用之间保持自身状态的应用程序,例如多步流,或者一个没有会话饼干的客户端,设置 challenges: 'caller' 并传递挑战回:
const passkeys = new Passkeys({ ...options, challenges: 'caller' });
const request = await passkeys.authenticationOptions({ id: user.id });
// keep request.challenge with the flow, then:
const used = await passkeys.verifyAuthentication(assertion, { challenge, userId: user.id });
residentKey: 'required' 使每一个新的密码都可以发现, 对于一个没有用户名的登录 timeout (以毫秒计)是客户端需要回答多久. 一个用户处理用户名而不是密码的断言被拒绝. 与一个 userId,另一个用户的密码像一个未知的一样被拒绝。 登记需要: userId 这是为.