Skip to content

Commit

Permalink
feat: add OAuth flow
Browse files Browse the repository at this point in the history
  • Loading branch information
thislooksfun committed Apr 9, 2021
1 parent d86550d commit 7029a16
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 9 deletions.
52 changes: 50 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import type { OauthOpts } from "./helper/api/oauth";
import type { Query } from "./helper/api/core";
import type { Token } from "./helper/accessToken";
import { CommentControls, PostControls, SubredditControls } from "./controls";
import { updateAccessToken, tokenFromCode } from "./helper/accessToken";
import * as anon from "./helper/api/anon";
import * as oauth from "./helper/api/oauth";
import refreshToken from "./helper/accessToken";

/** Username and password based authentication */
export interface UsernameAuth {
Expand Down Expand Up @@ -152,6 +152,54 @@ export default class Client {
this.subreddits = new SubredditControls(this);
}

/**
* Get an OAuth login url.
*
* @param clientId The ID of the Reddit app.
* @param scopes The scopes to authorize with.
* @param redirectUri The uri to redirect to after authorization.
* @param state Some arbetrary state that will be passed back upon
* authorization. This is used as a CSRF token to prevent various attacks.
* @param temporary Whether the auth should be temporary (expires after 1hr),
* or permanent.
*
* @returns The URL to direct the user to for authorization.
*/
static getAuthUrl(
clientId: string,
scopes: string[],
redirectUri: string,
state: string = "snoots",
temporary: boolean = false
): string {
const q = new URLSearchParams();
q.append("client_id", clientId);
q.append("response_type", "code");
q.append("state", state);
q.append("redirect_uri", redirectUri);
q.append("duration", temporary ? "temporary" : "permanent");
q.append("scope", scopes.join(" "));

return `https://www.reddit.com/api/v1/authorize?${q}`;
}

/**
* Authorize this client from an OAuth code.
*
* @param code The OAuth code.
* @param redirectUri The redirect URI. This ***must*** be the same as the uri
* given to {@link OAuth.getAuthUrl}.
*
* @returns A promise that resolves when the authorization is complete.
*/
async authFromCode(code: string, redirectUri: string): Promise<void> {
const creds = this.creds;
if (!creds) throw "No creds";

this.token = await tokenFromCode(code, creds, this.userAgent, redirectUri);
this.auth = { refreshToken: this.token.refresh! };
}

/**
* (re)authorize this client.
*
Expand Down Expand Up @@ -238,7 +286,7 @@ export default class Client {
protected async updateAccessToken(): Promise<void> {
if (!this.creds) throw "No creds";

this.token = await refreshToken(
this.token = await updateAccessToken(
this.userAgent,
this.token,
this.creds,
Expand Down
33 changes: 26 additions & 7 deletions src/helper/accessToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,16 @@ export interface TokenResponse {
scope: string;
}

export default async function updateAccessToken(
function rawToToken(raw: Data): Token {
const tkns: TokenResponse = camelCaseKeys(raw);
return {
access: tkns.accessToken,
expiration: Date.now() + tkns.expiresIn * 1000,
refresh: tkns.refreshToken,
};
}

export async function updateAccessToken(
userAgent: string,
token: Token | null,
creds: Credentials,
Expand Down Expand Up @@ -53,10 +62,20 @@ export default async function updateAccessToken(
{}
);

const tkns: TokenResponse = camelCaseKeys(raw);
return {
access: tkns.accessToken,
expiration: Date.now() + tkns.expiresIn * 1000,
refresh: tkns.refreshToken,
};
return rawToToken(raw);
}

export async function tokenFromCode(
code: string,
creds: Credentials,
userAgent: string,
redirectUri: string
): Promise<Token> {
const raw: Data = await post(creds, userAgent, "api/v1/access_token", {
grant_type: "authorization_code",
code,
redirect_uri: redirectUri,
});

return rawToToken(raw);
}

0 comments on commit 7029a16

Please sign in to comment.