Skip to content

Commit 8730831

Browse files
author
Itai Levi
authored
[FEAT] Active Org Support (#30)
* feat(client): add active org to access token map * feat(api): clarify status text for user not in org * fix(getAccessTokenForActiveOrg): return user not in org for 401 response, fix types * 2.0.17 * refactor: rename function
1 parent 0dca1ae commit 8730831

File tree

5 files changed

+102
-7
lines changed

5 files changed

+102
-7
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "git",
66
"url": "https://github.com/PropelAuth/javascript"
77
},
8-
"version": "2.0.16",
8+
"version": "2.0.17",
99
"keywords": [
1010
"auth",
1111
"user",

src/api.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,16 @@ export type LogoutResponse = {
5151
redirect_to: string
5252
}
5353

54-
export function fetchAuthenticationInfo(authUrl: string): Promise<AuthenticationInfo | null> {
55-
return fetch(`${authUrl}/api/v1/refresh_token`, {
54+
export function fetchAuthenticationInfo(authUrl: string, activeOrgId?: string): Promise<AuthenticationInfo | null> {
55+
const queryParams = new URLSearchParams()
56+
if (activeOrgId) {
57+
queryParams.append("active_org_id", activeOrgId)
58+
}
59+
let path = `${authUrl}/api/v1/refresh_token`
60+
if (queryParams.toString()) {
61+
path += `?${queryParams.toString()}`
62+
}
63+
return fetch(path, {
5664
method: "GET",
5765
credentials: "include",
5866
headers: {

src/client.ts

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { AuthenticationInfo, fetchAuthenticationInfo, logout } from "./api"
2+
import { runWithRetriesOnAnyError } from "./fetch_retries"
23
import { currentTimeSeconds, getLocalStorageNumber, hasLocalStorage, hasWindow } from "./helpers"
3-
import {runWithRetriesOnAnyError} from "./fetch_retries";
44

55
const LOGGED_IN_AT_KEY = "__PROPEL_AUTH_LOGGED_IN_AT"
66
const LOGGED_OUT_AT_KEY = "__PROPEL_AUTH_LOGGED_OUT_AT"
77
const AUTH_TOKEN_REFRESH_BEFORE_EXPIRATION_SECONDS = 10 * 60
88
const DEBOUNCE_DURATION_FOR_REFOCUS_SECONDS = 60
9+
const ACTIVE_ORG_ACCESS_TOKEN_REFRESH_EXPIRATION_SECONDS = 60 * 5
910

1011
const encodeBase64 = (str: string) => {
1112
const encode = window ? window.btoa : btoa
@@ -38,6 +39,20 @@ export interface RedirectToSetupSAMLPageOptions {
3839
redirectBackToUrl?: string
3940
}
4041

42+
export type AccessTokenForActiveOrg =
43+
| {
44+
error: undefined
45+
accessToken: string
46+
}
47+
| {
48+
error: "user_not_in_org"
49+
accessToken: never
50+
}
51+
| {
52+
error: "unexpected_error"
53+
accessToken: never
54+
}
55+
4156
export interface IAuthClient {
4257
/**
4358
* If the user is logged in, this method returns an access token, the time (in seconds) that the token will expire,
@@ -88,6 +103,11 @@ export interface IAuthClient {
88103
*/
89104
getSetupSAMLPageUrl(orgId: string): string
90105

106+
/**
107+
* Gets an access token for a specific organization, known as an Active Org.
108+
*/
109+
getAccessTokenForOrg(orgId: string): Promise<AccessTokenForActiveOrg>
110+
91111
/**
92112
* Redirects the user to the signup page.
93113
*/
@@ -160,6 +180,13 @@ export interface IAuthOptions {
160180
enableBackgroundTokenRefresh?: boolean
161181
}
162182

183+
interface AccessTokenActiveOrgMap {
184+
[orgId: string]: {
185+
accessToken: string
186+
fetchedAt: number
187+
}
188+
}
189+
163190
interface ClientState {
164191
initialLoadFinished: boolean
165192
authenticationInfo: AuthenticationInfo | null
@@ -169,6 +196,7 @@ interface ClientState {
169196
lastLoggedOutAtMessage: number | null
170197
refreshInterval: number | null
171198
lastRefresh: number | null
199+
accessTokenActiveOrgMap: AccessTokenActiveOrgMap
172200
readonly authUrl: string
173201
}
174202

@@ -201,6 +229,7 @@ export function createClient(authOptions: IAuthOptions): IAuthClient {
201229
authUrl: authOptions.authUrl,
202230
refreshInterval: null,
203231
lastRefresh: null,
232+
accessTokenActiveOrgMap: {},
204233
}
205234

206235
// Helper functions
@@ -248,6 +277,13 @@ export function createClient(authOptions: IAuthOptions): IAuthClient {
248277
}
249278
}
250279

280+
/**
281+
* Invalidates all org's access tokens.
282+
*/
283+
function resetAccessTokenActiveOrgMap() {
284+
clientState.accessTokenActiveOrgMap = {}
285+
}
286+
251287
function setAuthenticationInfoAndUpdateDownstream(authenticationInfo: AuthenticationInfo | null) {
252288
const previousAccessToken = clientState.authenticationInfo?.accessToken
253289
clientState.authenticationInfo = authenticationInfo
@@ -265,14 +301,18 @@ export function createClient(authOptions: IAuthOptions): IAuthClient {
265301
notifyObserversOfAccessTokenChange(accessToken)
266302
}
267303

304+
resetAccessTokenActiveOrgMap()
305+
268306
clientState.lastRefresh = currentTimeSeconds()
269307
clientState.initialLoadFinished = true
270308
}
271309

272310
async function forceRefreshToken(returnCached: boolean): Promise<AuthenticationInfo | null> {
273311
try {
274312
// Happy case, we fetch auth info and save it
275-
const authenticationInfo = await runWithRetriesOnAnyError(() => fetchAuthenticationInfo(clientState.authUrl))
313+
const authenticationInfo = await runWithRetriesOnAnyError(() =>
314+
fetchAuthenticationInfo(clientState.authUrl)
315+
)
276316
setAuthenticationInfoAndUpdateDownstream(authenticationInfo)
277317
return authenticationInfo
278318
} catch (e) {
@@ -449,6 +489,52 @@ export function createClient(authOptions: IAuthOptions): IAuthClient {
449489
}
450490
},
451491

492+
async getAccessTokenForOrg(orgId: string): Promise<AccessTokenForActiveOrg> {
493+
// First, check if there is a valid access token for the org ID in the
494+
// valid time frame.
495+
const currentTimeSecs = currentTimeSeconds()
496+
497+
const activeOrgAccessToken = clientState.accessTokenActiveOrgMap[orgId]
498+
if (!!activeOrgAccessToken) {
499+
if (
500+
currentTimeSecs <
501+
activeOrgAccessToken.fetchedAt + ACTIVE_ORG_ACCESS_TOKEN_REFRESH_EXPIRATION_SECONDS
502+
) {
503+
return {
504+
accessToken: activeOrgAccessToken.accessToken,
505+
error: undefined,
506+
}
507+
}
508+
}
509+
// Fetch the access token for the org ID and update.
510+
try {
511+
const authenticationInfo = await runWithRetriesOnAnyError(() =>
512+
fetchAuthenticationInfo(clientState.authUrl, orgId)
513+
)
514+
if (!authenticationInfo) {
515+
// Only null if 401 unauthorized.
516+
return {
517+
error: "user_not_in_org",
518+
accessToken: null as never,
519+
}
520+
}
521+
const { accessToken } = authenticationInfo
522+
clientState.accessTokenActiveOrgMap[orgId] = {
523+
accessToken,
524+
fetchedAt: currentTimeSecs,
525+
}
526+
return {
527+
accessToken,
528+
error: undefined,
529+
}
530+
} catch (e) {
531+
return {
532+
error: "unexpected_error",
533+
accessToken: null as never,
534+
}
535+
}
536+
},
537+
452538
getSignupPageUrl(options?: RedirectToSignupOptions): string {
453539
return getSignupPageUrl(options)
454540
},

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export type { AccessHelper, AccessHelperWithOrg } from "./access_helper"
22
export type { AuthenticationInfo, User } from "./api"
33
export { createClient } from "./client"
44
export type {
5+
AccessTokenForActiveOrg,
56
IAuthClient,
67
IAuthOptions,
78
RedirectToAccountOptions,

0 commit comments

Comments
 (0)