native factory impl
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user