@atproto/oauth-client: atproto flavoured OAuth client
Core library for implementing ATPROTO OAuth clients.
For a browser specific implementation, see @atproto/oauth-client-browser.
For a node specific implementation, see
@atproto/oauth-client-node.
Usage
Configuration
import { OAuthClient } from '@atproto/oauth-client'
import { JoseKey } from '@atproto/jwk-jose'
const client = new OAuthClient({
handleResolver: 'https://my-backend.example',
responseMode: 'query',
clientMetadata: {
client_id: 'https://my-app.example/atproto-oauth-client.json',
jwks_uri: 'https://my-app.example/jwks.json',
},
runtimeImplementation: {
createKey(algs: string[]): Promise<Key> {
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> {
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 => {
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: {
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: {
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: [
await JoseKey.fromImportable(process.env.PRIVATE_KEY_1),
await JoseKey.fromImportable(process.env.PRIVATE_KEY_2),
await JoseKey.fromImportable(process.env.PRIVATE_KEY_3),
],
})
Authentication
const url = await client.authorize('foo.bsky.team', {
state: '434321',
prompt: 'consent',
scope: 'email',
ui_locales: 'fr',
})
Make user visit url
. Then, once it was redirected to the callback URI, perform the following:
const params = new URLSearchParams('code=...&state=...')
const result = await client.callback(params)
result.state === '434321'
const agent = result.agent
await agent.post({
text: 'Hello, world!',
})
if (agent instanceof AtpAgent) {
await agent.logout()
}
Advances use-cases
Listening for session updates and deletion
The OAuthClient
will emit events whenever a session is updated or deleted.
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) {
} else if (cause instanceof TokenRevokedError) {
} else {
}
},
)
Force user to re-authenticate
const url = await client.authorize(handle, {
prompt: 'login',
state,
})
or
const url = await client.authorize(handle, {
state,
max_age: 600,
})
Silent Sign-In
Using silent sign-in requires to handle retries on the callback endpoint.
async function createLoginUrl(handle: string, state?: string): string {
return client.authorize(handle, {
state,
prompt: 'none',
})
}
async function handleCallback(params: URLSearchParams) {
try {
return await client.callback(params)
} catch (err) {
if (
err instanceof OAuthCallbackError &&
['login_required', 'consent_required'].includes(err.params.get('error'))
) {
const url = await client.authorize(handle, { state: err.state })
return new MyLoginRequiredError(url)
}
throw err
}
}