native factory impl

This commit is contained in:
Hailey
2024-04-12 00:35:30 -07:00
parent 0aab04f670
commit 73c98bc3af
@@ -0,0 +1,147 @@
import {Fetch} from '@atproto/fetch'
import {UniversalIdentityResolver} from '@atproto/identity-resolver'
import {
OAuthAuthorizeOptions,
OAuthClientFactory,
OAuthResponseMode,
OAuthResponseType,
Session,
} from '@atproto/oauth-client'
import {OAuthClientMetadata} from '@atproto/oauth-client-metadata'
import IsomorphicOAuthServerMetadataResolver from '@atproto/oauth-server-metadata-resolver'
import {CryptoSubtle} from './crypto-subtle'
import {DatabaseStore, RNOAuthDatabase} from './rn-oauth-database'
export type BrowserOauthClientFactoryOptions = {
responseMode?: OAuthResponseMode
responseType?: OAuthResponseType
clientMetadata: OAuthClientMetadata
fetch?: Fetch
crypto?: Crypto
}
const POPUP_KEY_PREFIX = '@@oauth-popup-callback:'
export class RNOAuthClientFactory extends OAuthClientFactory {
readonly sessionStore: DatabaseStore<Session>
constructor({
clientMetadata,
// "fragment" is safer as it is not sent to the server
responseMode = 'fragment',
responseType,
crypto = {subtle: CryptoSubtle},
fetch = globalThis.fetch,
}: BrowserOauthClientFactoryOptions) {
const database = new RNOAuthDatabase()
super({
clientMetadata,
responseMode,
responseType,
fetch,
cryptoImplementation: new CryptoSubtle(crypto),
sessionStore: database.getSessionStore(),
stateStore: database.getStateStore(),
metadataResolver: new IsomorphicOAuthServerMetadataResolver({
fetch,
cache: database.getMetadataCache(),
}),
identityResolver: UniversalIdentityResolver.from({
fetch,
didCache: database.getDidCache(),
handleCache: database.getHandleCache(),
}),
dpopNonceCache: database.getDpopNonceCache(),
})
this.sessionStore = database.getSessionStore()
}
async restoreAll() {
const sessionIds = await this.sessionStore.getKeys()
return Object.fromEntries(
await Promise.all(
sessionIds.map(
async sessionId =>
[sessionId, await this.restore(sessionId, false)] as const,
),
),
)
}
async init(sessionId?: string, forceRefresh = false) {
const signInResult = await this.signInCallback()
if (signInResult) {
return signInResult
} else if (sessionId) {
const client = await this.restore(sessionId, forceRefresh)
return {client}
} else {
// TODO: we could restore any session from the store ?
}
}
async signIn(input: string, options?: OAuthAuthorizeOptions) {
return await this.authorize(input, options)
}
async signInCallback() {
const redirectUri = new URL(this.clientMetadata.redirect_uris[0])
if (location.pathname !== redirectUri.pathname) return null
const params =
this.responseMode === 'query'
? new URLSearchParams(location.search)
: new URLSearchParams(location.hash.slice(1))
// Only if the query string contains oauth callback params
if (
!params.has('iss') ||
!params.has('state') ||
!(params.has('code') || params.has('error'))
) {
return null
}
// Replace the current history entry without the query string (this will
// prevent this 'if' branch to run again if the user refreshes the page)
history.replaceState(null, '', location.pathname)
return this.callback(params)
.then(async result => {
if (result.state?.startsWith(POPUP_KEY_PREFIX)) {
const stateKey = result.state.slice(POPUP_KEY_PREFIX.length)
await this.popupStore.set(stateKey, {
status: 'fulfilled',
value: result.client.sessionId,
})
window.close() // continued in signInPopup
throw new Error('Login complete, please close the popup window.')
}
return result
})
.catch(async err => {
// TODO: Throw a proper error from parent class to actually detect
// oauth authorization errors
const state = typeof (err as any)?.state
if (typeof state === 'string' && state?.startsWith(POPUP_KEY_PREFIX)) {
const stateKey = state.slice(POPUP_KEY_PREFIX.length)
await this.popupStore.set(stateKey, {
status: 'rejected',
reason: err,
})
window.close() // continued in signInPopup
throw new Error('Login complete, please close the popup window.')
}
throw err
})
}
}