Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "git",
"url": "https://github.com/PropelAuth/javascript"
},
"version": "2.0.23",
"version": "2.0.24",
"keywords": [
"auth",
"user",
Expand Down
133 changes: 93 additions & 40 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ export interface IAuthClient {
* Cleanup the auth client if you no longer need it.
*/
destroy(): void

/**
* Returns the auth options with all default values filled in.
*/
getAuthOptions(): IResolvedAuthOptions
}

export interface IAuthOptions {
Expand Down Expand Up @@ -195,12 +200,20 @@ export interface IAuthOptions {
/**
* If true, disables the token refresh on initial page load.
* Can help reduce duplicate token refresh requests.
*
*
* Default false
*/
skipInitialFetch?: boolean
}

export interface IResolvedAuthOptions {
authUrl: string
enableBackgroundTokenRefresh: boolean
minSecondsBeforeRefresh: number
disableRefreshOnFocus: boolean
skipInitialFetch: boolean
}

interface AccessTokenActiveOrgMap {
[orgId: string]: {
accessToken: string
Expand All @@ -218,6 +231,8 @@ interface ClientState {
refreshInterval: number | null
lastRefresh: number | null
accessTokenActiveOrgMap: AccessTokenActiveOrgMap
pendingAuthRequest: Promise<AuthenticationInfo | null> | null
pendingOrgAccessTokenRequests: Map<string, Promise<AccessTokenForActiveOrg>>
readonly authUrl: string
}

Expand Down Expand Up @@ -253,6 +268,8 @@ export function createClient(authOptions: IAuthOptions): IAuthClient {
refreshInterval: null,
lastRefresh: null,
accessTokenActiveOrgMap: {},
pendingAuthRequest: null,
pendingOrgAccessTokenRequests: new Map(),
}

// Helper functions
Expand Down Expand Up @@ -331,23 +348,35 @@ export function createClient(authOptions: IAuthOptions): IAuthClient {
}

async function forceRefreshToken(returnCached: boolean): Promise<AuthenticationInfo | null> {
try {
// Happy case, we fetch auth info and save it
const authenticationInfo = await runWithRetriesOnAnyError(() =>
fetchAuthenticationInfo(clientState.authUrl)
)
setAuthenticationInfoAndUpdateDownstream(authenticationInfo)
return authenticationInfo
} catch (e) {
// If there was an error, we sometimes still want to return the value we have cached
// (e.g. if we were prefetching), so in those cases we swallow the exception
if (returnCached) {
return clientState.authenticationInfo
} else {
setAuthenticationInfoAndUpdateDownstream(null)
throw e
}
// If there's already an in-flight request, return it to avoid duplicate fetches
if (clientState.pendingAuthRequest) {
return clientState.pendingAuthRequest
}

const request = (async () => {
try {
// Happy case, we fetch auth info and save it
const authenticationInfo = await runWithRetriesOnAnyError(() =>
fetchAuthenticationInfo(clientState.authUrl)
)
setAuthenticationInfoAndUpdateDownstream(authenticationInfo)
return authenticationInfo
} catch (e) {
// If there was an error, we sometimes still want to return the value we have cached
// (e.g. if we were prefetching), so in those cases we swallow the exception
if (returnCached) {
return clientState.authenticationInfo
} else {
setAuthenticationInfoAndUpdateDownstream(null)
throw e
}
} finally {
clientState.pendingAuthRequest = null
}
})()

clientState.pendingAuthRequest = request
return request
}

const getSignupPageUrl = (options?: RedirectToSignupOptions) => {
Expand Down Expand Up @@ -529,33 +558,47 @@ export function createClient(authOptions: IAuthOptions): IAuthClient {
}
}
}
// Fetch the access token for the org ID and update.
try {
const authenticationInfo = await runWithRetriesOnAnyError(() =>
fetchAuthenticationInfo(clientState.authUrl, orgId)
)
if (!authenticationInfo) {
// Only null if 401 unauthorized.

// Check for in-flight request for this org to avoid duplicate fetches
const pendingRequest = clientState.pendingOrgAccessTokenRequests.get(orgId)
if (pendingRequest) {
return pendingRequest
}

// Create new request and store it
const request = (async (): Promise<AccessTokenForActiveOrg> => {
try {
const authenticationInfo = await runWithRetriesOnAnyError(() =>
fetchAuthenticationInfo(clientState.authUrl, orgId)
)
if (!authenticationInfo) {
// Only null if 401 unauthorized.
return {
error: "user_not_in_org",
accessToken: null as never,
}
}
const { accessToken } = authenticationInfo
clientState.accessTokenActiveOrgMap[orgId] = {
accessToken,
fetchedAt: currentTimeSecs,
}
return {
accessToken,
error: undefined,
}
} catch (e) {
return {
error: "user_not_in_org",
error: "unexpected_error",
accessToken: null as never,
}
} finally {
clientState.pendingOrgAccessTokenRequests.delete(orgId)
}
const { accessToken } = authenticationInfo
clientState.accessTokenActiveOrgMap[orgId] = {
accessToken,
fetchedAt: currentTimeSecs,
}
return {
accessToken,
error: undefined,
}
} catch (e) {
return {
error: "unexpected_error",
accessToken: null as never,
}
}
})()

clientState.pendingOrgAccessTokenRequests.set(orgId, request)
return request
},

getSignupPageUrl(options?: RedirectToSignupOptions): string {
Expand Down Expand Up @@ -626,6 +669,16 @@ export function createClient(authOptions: IAuthOptions): IAuthClient {
clearInterval(clientState.refreshInterval)
}
},

getAuthOptions(): IResolvedAuthOptions {
return {
authUrl: clientState.authUrl,
enableBackgroundTokenRefresh: authOptions.enableBackgroundTokenRefresh!,
minSecondsBeforeRefresh: minSecondsBeforeRefresh,
disableRefreshOnFocus: authOptions.disableRefreshOnFocus ?? false,
skipInitialFetch: authOptions.skipInitialFetch ?? false,
}
},
}

const onStorageChange = async function () {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type {
AccessTokenForActiveOrg,
IAuthClient,
IAuthOptions,
IResolvedAuthOptions,
RedirectToAccountOptions,
RedirectToCreateOrgOptions,
RedirectToLoginOptions,
Expand Down
Loading