@atproto/oauth-客户端: atproto flavoresed OAuth客户端
用于执行的核心库 原子弹 OAuth客户端.
关于浏览器的特定执行,见 @atproto/oauth-客户端浏览器。 。 。 . 关于节点的具体执行,见 @atproto/oauth-客户端-节点。 。 。 .
使用量
配置
import { OAuthClient, Key, Session } from '@atproto/oauth-client'
import { JoseKey } from '@atproto/jwk-jose' // NodeJS/Browser only
const client = new OAuthClient({
handleResolver: 'https://my-backend.example', // backend instances should use a DNS based resolver
responseMode: 'query', // or "fragment" (frontend only) or "form_post" (backend only)
// These must be the same metadata as the one exposed on the
// "client_id" endpoint (except when using a loopback client)
clientMetadata: {
client_id: 'https://my-app.example/atproto-oauth-client.json',
jwks_uri: 'https://my-app.example/jwks.json',
},
runtimeImplementation: {
// A runtime specific implementation of the crypto operations needed by the
// OAuth client. See "@atproto/oauth-client-browser" for a browser specific
// implementation. The following example is suitable for use in NodeJS.
createKey(algs: string[]): Promise<Key> {
// algs is an ordered array of preferred algorithms (e.g. ['RS256', 'ES256'])
// Note, in browser environments, it is better to use non extractable keys
// to prevent the private key from being stolen. This can be done using
// the WebcryptoKey class from the "@atproto/jwk-webcrypto" package. The
// inconvenient of these keys (which is also what makes them stronger) is
// that the only way to persist them across browser reloads is to save
// them in the indexed DB.
return JoseKey.generate(algs)
},
getRandomValues(length: number): Uint8Array | PromiseLike<Uint8Array> {
return crypto.getRandomValues(new Uint8Array(length))
},
digest(
bytes: Uint8Array,
algorithm: { name: string },
): Uint8Array | PromiseLike<Uint8Array> {
// sha256 is required. Unsupported algorithms should throw an error.
if (algorithm.name.startsWith('sha')) {
const subtleAlgo = `SHA-${algorithm.name.slice(3)}`
const buffer = await crypto.subtle.digest(subtleAlgo, bytes)
return new Uint8Array(buffer)
}
throw new TypeError(`Unsupported algorithm: ${algorithm.name}`)
},
requestLock: <T>(
name: string,
fn: () => T | PromiseLike<T>,
): Promise<T> => {
// This function is used to prevent concurrent refreshes of the same
// credentials. It is important to ensure that only one refresh is done at
// a time to prevent the sessions from being revoked.
// The following example shows a simple in-memory lock. In a real
// application, you should use a more robust solution (e.g. a system wide
// lock manager). Note that not providing a lock will result in an
// in-memory lock to be used (DO NOT copy-paste the following code).
declare const locks: Map<string, Promise<void>>
const current = locks.get(name) || Promise.resolve()
const next = current
.then(fn)
.catch(() => {})
.finally(() => {
if (locks.get(name) === next) locks.delete(name)
})
locks.set(name, next)
return next
},
},
stateStore: {
// A store for saving state data while the user is being redirected to the
// authorization server.
set(key: string, internalState: InternalStateData): Promise<void> {
throw new Error('Not implemented')
},
get(key: string): Promise<InternalStateData | undefined> {
throw new Error('Not implemented')
},
del(key: string): Promise<void> {
throw new Error('Not implemented')
},
},
sessionStore: {
// A store for saving session data.
set(sub: string, session: Session): Promise<void> {
throw new Error('Not implemented')
},
get(sub: string): Promise<Session | undefined> {
throw new Error('Not implemented')
},
del(sub: string): Promise<void> {
throw new Error('Not implemented')
},
},
keyset: [
// For backend clients only, a list of private keys to use for signing
// credentials. These keys MUST correspond to the public keys exposed on the
// "jwks_uri" of the client metadata. Note that the jwks JSON corresponding
// to the following keys can be obtained using the `client.jwks` getter.
await JoseKey.fromImportable(process.env.PRIVATE_KEY_1),
await JoseKey.fromImportable(process.env.PRIVATE_KEY_2),
await JoseKey.fromImportable(process.env.PRIVATE_KEY_3),
],
})
认证
const url = await client.authorize('foo.bsky.team', {
state: '434321',
prompt: 'consent',
scope: 'email',
ui_locales: 'fr',
})
访问用户 url。然后,一旦它被重定向到调回URI,就执行以下操作:
// Parse the query params from the callback URI
const params = new URLSearchParams('code=...&state=...')
// Process the callback using the OAuth client
const result = await client.callback(params)
// Verify the state (e.g. to link to an internal user)
result.state === '434321' // true
const oauthSession = result.session
登录过程导致 OAuthSession 可以用来制造
向资源服务器认证请求 。 此实例会自动
需要时刷新证书.
提出经认证的请求
这个 OAuthSession 签名后获得的实例可用于制作
向用户的PDS验证请求. 有两个主要的使用案例:
-
向Bluesky的AppView提出认证请求,以便获取 操作来自
app.bsky词典. -
向您的 AppView 提出认证请求,以便获取 操作您自己的词典中的数据.
向 Bluesky 的 AppView 提出认证请求
这个 @atproto/oauth-client 软件包提供 OAuthSession 类可以是
曾向Bluesky的AppView提出认证请求. 这可以做到
建造一个 Agent (从) @atproto/api使用
OAuthSession 举个例子.
import { Agent } from '@atproto/api'
const agent = new Agent(oauthSession)
// Make an authenticated request to the server. New credentials will be
// automatically fetched if needed (causing sessionStore.set() to be called).
await agent.post({
text: 'Hello, world!',
})
// revoke credentials on the server (causing sessionStore.del() to be called)
await agent.signOut()
向您的 AppView 提出认证请求
这个 OAuthSession 签名后获得的实例可用于即时处理
页:1 XrpcClient 类从 @atproto/xrpc 软件包.
import { Lexicons } from '@atproto/lexicon'
import { OAuthClient } from '@atproto/oauth-client' // or "@atproto/oauth-client-browser" or "@atproto/oauth-client-node"
import { XrpcClient } from '@atproto/xrpc'
// Define your lexicons
const myLexicon = new Lexicons([
{
lexicon: 1,
id: 'com.example.query',
defs: {
main: {
// ...
},
},
},
])
// Describe your app's oauth client
const oauthClient = new OAuthClient({
// ...
})
// Authenticate the user
const oauthSession = await oauthClient.restore('did:plc:123')
// Instantiate a client using the `oauthSession` as fetch handler object
const client = new XrpcClient(oauthSession, myLexicon)
// Make authenticated calls
const response = await client.call('com.example.query')
请注意, 用户的PDS 可能不知道您的词汇表, 也不知道该怎么处理
这些呼吁(PDS)只授权执行 com.atproto 词典. 单位
您需要有一个后端来处理这些
打电话 然后您可以指示您的 PDS 将这些电话转发到您的后端 .
const response = await client.call(
'com.example.query',
{
// Params
},
{
headers: {
// The PDS will proxy calls to the specified service in did:plc:xyz's did document.
// These calls will be authenticated using "service auth", a single use JWT Bearer token, signed with the logged-in user's private key.
'atproto-proxy': 'did:plc:xyz#serviceId',
},
},
)
你也可以即兴演奏 XrpcClient 有自定义的类 fetch 函数
将提供 atproto-proxy 所有通话的页眉 :
const boundClient = new XrpcClient((url, init) => {
const headers = new Headers(init?.headers)
// Add the atproto-proxy header if it is not already present
if (!headers.has('atproto-proxy')) {
headers.set('atproto-proxy', 'did:plc:xyz#serviceId')
}
return oauthSession.fetchHandler(url, { ...init, headers })
}, myLexicon)
// No need to specify the atproto-proxy header anymore
const response = await boundClient.call('com.example.query')
[注]
通过 PDS 代理每通呼叫不推荐执行 因为它会增加只读到您的词汇的空闲性. 这样做也会妨碍您的后端预想写作 在网络上。 实际上,信访会被发送到PDS,然后PDS 通过接力(a.k.a."firehose")在网络上传播. 这个会 引入写作时间和写作时间之间的延迟 由您的后端处理 .
为了避免这些问题,建议你执行你的建议 使用后端换前端模式的后端. 这个后端负责 并能够预测 在网络上写.
更多信息,请访问 原子弹 文献网站.
预付款使用情况
听会话更新并删除
这个 OAuthClient 当会话更新或删除时,会发出事件.
import {
Session,
TokenRefreshError,
TokenRevokedError,
} from '@atproto/oauth-client'
client.addEventListener('updated', (event: CustomEvent<Session>) => {
console.log('Refreshed tokens were saved in the store:', event.detail)
})
client.addEventListener(
'deleted',
(
event: CustomEvent<{
sub: string
cause: TokenRefreshError | TokenRevokedError | unknown
}>,
) => {
console.log('Session was deleted from the session store:', event.detail)
const { cause } = event.detail
if (cause instanceof TokenRefreshError) {
// - refresh_token unavailable or expired
// - oauth response error (`cause.cause instanceof OAuthResponseError`)
// - session data does not match expected values returned by the OAuth server
} else if (cause instanceof TokenRevokedError) {
// Session was revoked through:
// - agent.signOut()
// - client.revoke(sub)
} else {
// An unexpected error occurred, causing the session to be deleted
}
},
)
强制用户重新认证
const url = await client.authorize(handle, {
prompt: 'login',
state,
})
或者说
const url = await client.authorize(handle, {
state,
})
安静的标志... 单位
使用无声签名需要处理回调端点上的回调.
async function createLoginUrl(handle: string, state?: string): string {
return client.authorize(handle, {
state,
// Use "prompt=none" to attempt silent sign-in
prompt: 'none',
})
}
async function handleCallback(params: URLSearchParams) {
try {
return await client.callback(params)
} catch (err) {
// Silent sign-in failed, retry without prompt=none
if (
err instanceof OAuthCallbackError &&
['login_required', 'consent_required'].includes(err.params.get('error'))
) {
// Do *not* use prompt=none when retrying (to avoid infinite redirects)
const url = await client.authorize(handle, { state: err.state })
// Allow calling code to catch the error and redirect the user to the new URL
return new MyLoginRequiredError(url)
}
throw err
}
}