Skip to content

Commit

Permalink
feat: add Client.getAuthorizedScopes()
Browse files Browse the repository at this point in the history
This method allows you to get the currently authorized oAuth scopes.
Note that it will return `undefined` in the following cases:
  - You are using anonymous auth
  - You have not yet called any methods that required oAuth access

The second case is only an issue if you are creating a brand new Client
object from a previously obtained refresh token. Client.fromAuthCode
will set the scopes, but just creating a new Client will not.

Closes thislooksfun#43
  • Loading branch information
thislooksfun committed Mar 5, 2022
1 parent 612586e commit 1cdcac2
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 44 deletions.
12 changes: 12 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,16 @@ export class Client {
}
return undefined;
}

/**
* Get the set of authorized scopes for the current session.
*
* @returns The scopes, or `undefined` if no oauth session exists.
*/
getAuthorizedScopes(): Maybe<string[]> {
if (this.gateway instanceof OauthGateway) {
return this.gateway.getScopes();
}
return undefined;
}
}
90 changes: 52 additions & 38 deletions src/gateway/__tests__/unit/oauth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ function fcToken(withRefresh?: boolean): fc.Arbitrary<Token> {
const access = fc.string();
const expiration = fc.integer().map(v => v + Date.now());
const refresh = fc.string({ minLength: 1 });
const scopes = fc.array(fc.string());

if (withRefresh === true) {
return fc.record({ access, expiration, refresh });
return fc.record({ access, expiration, refresh, scopes });
} else if (withRefresh === false) {
return fc.record({ access, expiration });
return fc.record({ access, expiration, scopes });
} else {
// No preference given, randomize whether or not there is a refresh token.
return fc.record(
{ access, expiration, refresh },
{ requiredKeys: ["access", "expiration"] }
{ access, expiration, refresh, scopes },
{ requiredKeys: ["access", "expiration", "scopes"] }
);
}
}
Expand Down Expand Up @@ -93,24 +94,28 @@ describe("OauthGateway", () => {

it("uses the stored refresh token, if it has one", async () => {
await fc.assert(
fc.asyncProperty(fcToken(true), async token => {
gateway.setToken(token);

const body = {
/* eslint-disable @typescript-eslint/naming-convention */
api_type: "json",
grant_type: "refresh_token",
refresh_token: token.refresh,
/* eslint-enable @typescript-eslint/naming-convention */
};
const n = nock("https://www.reddit.com", commonNockOptions)
.post("/api/v1/access_token.json?raw_json=1&api_type=json", body)
.reply(200, { bim: "bom" });

await gateway.updateAccessToken();
fc.asyncProperty(
fcToken(true),
fcTokenResponse(),
async (token, tokenResponse) => {
gateway.setToken(token);

n.done();
})
const body = {
/* eslint-disable @typescript-eslint/naming-convention */
api_type: "json",
grant_type: "refresh_token",
refresh_token: token.refresh,
/* eslint-enable @typescript-eslint/naming-convention */
};
const n = nock("https://www.reddit.com", commonNockOptions)
.post("/api/v1/access_token.json?raw_json=1&api_type=json", body)
.reply(200, tokenResponse);

await gateway.updateAccessToken();

n.done();
}
)
);
});

Expand All @@ -119,7 +124,8 @@ describe("OauthGateway", () => {
fc.asyncProperty(
fcToken(false),
fcClientAuth(),
async (token, auth) => {
fcTokenResponse(),
async (token, auth, tokenResponse) => {
gateway.setToken(token);
gateway.setInitialAuth(auth);

Expand All @@ -136,7 +142,7 @@ describe("OauthGateway", () => {
};
const n = nock("https://www.reddit.com", commonNockOptions)
.post("/api/v1/access_token.json?raw_json=1&api_type=json", body)
.reply(200, { bim: "bom" });
.reply(200, tokenResponse);

await gateway.updateAccessToken();

Expand All @@ -148,21 +154,25 @@ describe("OauthGateway", () => {

it("falls on client credentials auth if it has no stored refresh token and no initial auth", async () => {
await fc.assert(
fc.asyncProperty(fcToken(false), async token => {
gateway.setToken(token);
// eslint-disable-next-line unicorn/no-useless-undefined
gateway.setInitialAuth(undefined);
fc.asyncProperty(
fcToken(false),
fcTokenResponse(),
async (token, tokenResponse) => {
gateway.setToken(token);
// eslint-disable-next-line unicorn/no-useless-undefined
gateway.setInitialAuth(undefined);

// eslint-disable-next-line @typescript-eslint/naming-convention
const body = { api_type: "json", grant_type: "client_credentials" };
const n = nock("https://www.reddit.com", commonNockOptions)
.post("/api/v1/access_token.json?raw_json=1&api_type=json", body)
.reply(200, { bim: "bom" });
// eslint-disable-next-line @typescript-eslint/naming-convention
const body = { api_type: "json", grant_type: "client_credentials" };
const n = nock("https://www.reddit.com", commonNockOptions)
.post("/api/v1/access_token.json?raw_json=1&api_type=json", body)
.reply(200, tokenResponse);

await gateway.updateAccessToken();
await gateway.updateAccessToken();

n.done();
})
n.done();
}
)
);
});

Expand Down Expand Up @@ -200,13 +210,13 @@ describe("OauthGateway", () => {
["not authenticated", undefined],
[
"access has expired",
{ access: "accessTkn", expiration: Date.now() - 9000 },
{ access: "accessTkn", expiration: Date.now() - 9000, scopes: [] },
],
])("When %s", (_condition, token) => {
beforeEach(() => {
gateway.setToken(token);
updateAccessTokenSpy.mockImplementation(async () => {
gateway.setToken({ access: "accessTkn", expiration: 0 });
gateway.setToken({ access: "accessTkn", expiration: 0, scopes: [] });
});
});

Expand Down Expand Up @@ -259,7 +269,11 @@ describe("OauthGateway", () => {
};

beforeEach(() => {
gateway.setToken({ access: "accessTkn", expiration: Date.now() + 9000 });
gateway.setToken({
access: "accessTkn",
expiration: Date.now() + 9000,
scopes: [],
});
});

describe(".get()", () => {
Expand Down
20 changes: 14 additions & 6 deletions src/gateway/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface Token {
access: string;
expiration: number;
refresh?: string;
scopes: string[];
}

export interface TokenResponse {
Expand Down Expand Up @@ -104,6 +105,11 @@ export class OauthGateway extends Gateway {
return this.token?.refresh;
}

/** @internal */
public getScopes(): Maybe<string[]> {
return this.token?.scopes;
}

protected async auth(): Promise<BearerAuth> {
await this.ensureTokenValid();
if (!this.token) throw new Error("Something has gone horribly wrong.");
Expand Down Expand Up @@ -154,15 +160,17 @@ export class OauthGateway extends Gateway {
debug("Updating token with grant %o", grant);
const credGate = new CredsGateway(this.creds, this.userAgent);
const raw: Data = await credGate.post("api/v1/access_token", grant);
const tkns: TokenResponse = fromRedditData(raw);
const response: TokenResponse = fromRedditData(raw);
this.token = {
access: tkns.accessToken,
expiration: Date.now() + tkns.expiresIn * 1000,
refresh: tkns.refreshToken,
access: response.accessToken,
expiration: Date.now() + response.expiresIn * 1000,
refresh: response.refreshToken,
scopes: response.scope.split(" "),
};
debug(
"Token updated successfully, new token expires at %d",
this.token.expiration
"Token updated successfully, new token expires at %d and has scopes ['%s']",
this.token.expiration,
this.token.scopes.join("', '")
);
}
}

0 comments on commit 1cdcac2

Please sign in to comment.