Skip to content

Commit

Permalink
add variable params to auth providers to request more scope sets, fix…
Browse files Browse the repository at this point in the history
… requestScopesForUser

fixes #475
  • Loading branch information
d-fischer committed Apr 28, 2023
1 parent 547e8af commit fef3545
Show file tree
Hide file tree
Showing 8 changed files with 70 additions and 37 deletions.
2 changes: 1 addition & 1 deletion packages/api/src/client/BaseApiClient.ts
Expand Up @@ -71,7 +71,7 @@ export class BaseApiClient extends EventEmitter {
* @param scopes The scopes to request.
*/
async requestScopesForUser(user: UserIdResolvable, scopes: string[]): Promise<void> {
await this._config.authProvider.getAccessTokenForUser(user, scopes);
await this._config.authProvider.getAccessTokenForUser(user, ...scopes.map(scope => [scope]));
}

/**
Expand Down
14 changes: 9 additions & 5 deletions packages/auth-ext/src/ExtensionAuthProvider.ts
Expand Up @@ -15,12 +15,16 @@ export class ExtensionAuthProvider implements AuthProvider {
return [];
}

async getAccessTokenForUser(user: UserIdResolvable, scopes?: string[]): Promise<AccessTokenWithUserId> {
if (scopes?.length) {
async getAccessTokenForUser(
user: UserIdResolvable,
...scopeSets: Array<string[] | undefined>
): Promise<AccessTokenWithUserId> {
if (scopeSets.length && scopeSets.some(set => set?.length)) {
throw new Error(
`Scope ${scopes.join(
', '
)} requested but direct extension calls do not support scopes. Please use an EBS instead.`
`Scopes ${scopeSets
.filter((val): val is string[] => Boolean(val))
.map(scopes => scopes.join('|'))
.join(', ')} requested but direct extension calls do not support scopes. Please use an EBS instead.`
);
}

Expand Down
11 changes: 6 additions & 5 deletions packages/auth/src/TokenFetcher.ts
Expand Up @@ -13,16 +13,17 @@ export class TokenFetcher<T extends AccessToken = AccessToken> {
this._executor = executor;
}

async fetch(scopes?: string[]): Promise<T> {
async fetch(...scopeSets: Array<string[] | undefined>): Promise<T> {
const filteredScopeSets = scopeSets.filter((val): val is string[] => Boolean(val));
if (this._newTokenPromise) {
if (!scopes?.length) {
if (!filteredScopeSets.length) {
return await this._newTokenPromise;
}

if (this._queueExecutor) {
this._queuedScopeSets.push(scopes);
this._queuedScopeSets.push(...filteredScopeSets);
} else {
this._queuedScopeSets = [scopes];
this._queuedScopeSets = [...filteredScopeSets];
}

if (!this._queuePromise) {
Expand Down Expand Up @@ -52,7 +53,7 @@ export class TokenFetcher<T extends AccessToken = AccessToken> {
return await this._queuePromise;
}

this._newTokenScopeSets = scopes?.length ? [scopes] : [];
this._newTokenScopeSets = [...filteredScopeSets];
const { promise, resolve, reject } = promiseWithResolvers<T>();
this._newTokenPromise = promise;
try {
Expand Down
9 changes: 6 additions & 3 deletions packages/auth/src/helpers.ts
Expand Up @@ -186,7 +186,7 @@ export async function getValidTokenFromProviderForIntent(
if (!provider.getAccessTokenForIntent) {
throw new InvalidTokenTypeError(
`This call requires an AuthProvider that supports intents.
Please provide an auth provider that does, such as \`RefreshingAuthProvider\`.`
Please use an auth provider that does, such as \`RefreshingAuthProvider\`.`
);
}
try {
Expand Down Expand Up @@ -291,7 +291,7 @@ export async function loadAndCompareTokenInfo(
token: string,
userId?: string,
loadedScopes?: string[],
requestedScopeSets?: string[][]
requestedScopeSets?: Array<string[] | undefined>
): Promise<[string[] | undefined, string]> {
if (requestedScopeSets?.length || !userId) {
const userInfo = await getTokenInfo(token, clientId);
Expand All @@ -301,7 +301,10 @@ export async function loadAndCompareTokenInfo(

const scopesToCompare = loadedScopes ?? userInfo.scopes;
if (requestedScopeSets) {
compareScopeSets(scopesToCompare, requestedScopeSets);
compareScopeSets(
scopesToCompare,
requestedScopeSets.filter((val): val is string[] => Boolean(val))
);
}

return [scopesToCompare, userInfo.userId];
Expand Down
24 changes: 20 additions & 4 deletions packages/auth/src/providers/AuthProvider.ts
Expand Up @@ -39,9 +39,17 @@ export interface AuthProvider {
* scopes are requested - the cached token should be valid for that -
* unless you know exactly what you're doing.
*
* @param scopes The requested scope(s).
* @param scopeSets The requested scopes.
*
* Each given parameter is an array of scopes, of which you should pick one to actually request.
*
* The recommendation is to pick the first one
* unless the provider already has one of the requested scopes for the given user.
*/
getAccessTokenForUser: (user: UserIdResolvable, scopes?: string[]) => Promise<AccessTokenWithUserId | null>;
getAccessTokenForUser: (
user: UserIdResolvable,
...scopeSets: Array<string[] | undefined>
) => Promise<AccessTokenWithUserId | null>;

/**
* Fetches a token for a user identified by the given intent defined by the provider.
Expand All @@ -56,9 +64,17 @@ export interface AuthProvider {
* as it accesses its connection credentials using an intent.
*
* @param intent The intent to fetch a token for.
* @param scopes The requested scopes.
* @param scopeSets The requested scopes.
*
* Each given parameter is an array of scopes, of which you should pick one to actually request.
*
* The recommendation is to pick the first one
* unless the provider already has one of the requested scopes for the resolved user.
*/
getAccessTokenForIntent?: (intent: string, scopes?: string[]) => Promise<AccessTokenWithUserId | null>;
getAccessTokenForIntent?: (
intent: string,
...scopeSets: Array<string[] | undefined>
) => Promise<AccessTokenWithUserId | null>;

/**
* Fetches an app token.
Expand Down
18 changes: 12 additions & 6 deletions packages/auth/src/providers/RefreshingAuthProvider.ts
Expand Up @@ -367,32 +367,38 @@ export class RefreshingAuthProvider extends EventEmitter implements AuthProvider
* Gets an access token for the given user.
*
* @param user The user to get an access token for.
* @param scopes The requested scopes.
* @param scopeSets The requested scopes.
*/
async getAccessTokenForUser(user: UserIdResolvable, scopes?: string[]): Promise<AccessTokenWithUserId | null> {
async getAccessTokenForUser(
user: UserIdResolvable,
...scopeSets: Array<string[] | undefined>
): Promise<AccessTokenWithUserId | null> {
const fetcher = this._userTokenFetchers.get(extractUserId(user));

if (!fetcher) {
return null;
}

return await fetcher.fetch(scopes);
return await fetcher.fetch(...scopeSets);
}

/**
* Fetches a token for a user identified by the given intent.
*
* @param intent The intent to fetch a token for.
* @param scopes The requested scopes.
* @param scopeSets The requested scopes.
*/
async getAccessTokenForIntent(intent: string, scopes?: string[]): Promise<AccessTokenWithUserId | null> {
async getAccessTokenForIntent(
intent: string,
...scopeSets: Array<string[] | undefined>
): Promise<AccessTokenWithUserId | null> {
if (!this._intentToUserId.has(intent)) {
return null;
}

const userId = this._intentToUserId.get(intent)!;

const newToken = await this.getAccessTokenForUser(userId, scopes);
const newToken = await this.getAccessTokenForUser(userId, ...scopeSets);

if (!newToken) {
throw new HellFreezesOverError(
Expand Down
26 changes: 16 additions & 10 deletions packages/auth/src/providers/StaticAuthProvider.ts
Expand Up @@ -59,11 +59,14 @@ export class StaticAuthProvider implements AuthProvider {
* If the current access token does not have the requested scopes, this method throws.
* This makes supplying an access token with the correct scopes from the beginning necessary.
*
* @param intent Ignored.
* @param scopes The requested scopes.
* @param user Ignored.
* @param scopeSets The requested scopes.
*/
async getAccessTokenForIntent(intent: string, scopes?: string[]): Promise<AccessTokenWithUserId> {
return await this._getAccessToken(scopes);
async getAccessTokenForUser(
user: UserIdResolvable,
...scopeSets: Array<string[] | undefined>
): Promise<AccessTokenWithUserId> {
return await this._getAccessToken(scopeSets);
}

/**
Expand All @@ -72,11 +75,14 @@ export class StaticAuthProvider implements AuthProvider {
* If the current access token does not have the requested scopes, this method throws.
* This makes supplying an access token with the correct scopes from the beginning necessary.
*
* @param user Ignored.
* @param scopes The requested scopes.
* @param intent Ignored.
* @param scopeSets The requested scopes.
*/
async getAccessTokenForUser(user: UserIdResolvable, scopes?: string[]): Promise<AccessTokenWithUserId> {
return await this._getAccessToken(scopes);
async getAccessTokenForIntent(
intent: string,
...scopeSets: Array<string[] | undefined>
): Promise<AccessTokenWithUserId> {
return await this._getAccessToken(scopeSets);
}

/**
Expand All @@ -93,13 +99,13 @@ export class StaticAuthProvider implements AuthProvider {
return this._scopes ?? [];
}

private async _getAccessToken(requestedScopes?: string[]): Promise<AccessTokenWithUserId> {
private async _getAccessToken(requestedScopeSets?: Array<string[] | undefined>): Promise<AccessTokenWithUserId> {
const [scopes, userId] = await loadAndCompareTokenInfo(
this._clientId,
this._accessToken.accessToken,
this._userId,
this._scopes,
requestedScopes ? [requestedScopes] : []
requestedScopeSets
);

this._scopes = scopes;
Expand Down
3 changes: 0 additions & 3 deletions tsconfig.json
Expand Up @@ -9,9 +9,6 @@
{
"path": "./packages/auth"
},
{
"path": "./packages/auth-electron"
},
{
"path": "./packages/auth-ext"
},
Expand Down

0 comments on commit fef3545

Please sign in to comment.