Skip to content

Commit b4501d7

Browse files
committed
OAuth2 public client support
1 parent 9e06796 commit b4501d7

File tree

1 file changed

+116
-63
lines changed

1 file changed

+116
-63
lines changed

src/OAuth2User.ts

+116-63
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,30 @@ export type OAuth2Scopes =
2626
| "bookmark.read"
2727
| "bookmark.write";
2828

29-
export type OAuth2UserOptions = {
29+
export interface OAuth2UserOptions {
3030
client_id: string;
31-
client_secret: string;
31+
client_secret?: string;
3232
callback: string;
3333
scopes: OAuth2Scopes[];
3434
request_options?: Partial<RequestOptions>;
35-
};
35+
}
36+
37+
export type GenerateAuthUrlOptions =
38+
| {
39+
state: string;
40+
code_challenge_method: "s256";
41+
}
42+
| {
43+
state: string;
44+
code_challenge: string;
45+
code_challenge_method?: "plain";
46+
};
47+
48+
export interface RevokeAccessTokenParams {
49+
token_type_hint: string;
50+
token: string;
51+
client_id: string;
52+
}
3653

3754
function sha256(buffer: string) {
3855
return crypto.createHash("sha256").update(buffer).digest();
@@ -46,42 +63,64 @@ function base64URLEncode(str: Buffer) {
4663
.replace(/=/g, "");
4764
}
4865

66+
interface RevokeAccessTokenResponse {
67+
revoked: boolean;
68+
}
69+
70+
/**
71+
* Twitter OAuth2 Authentication Client
72+
*
73+
* TypeScript Authentication Client for use with the Twitter API OAuth2
74+
*
75+
*/
4976
export class OAuth2User implements AuthClient {
5077
#access_token?: string;
5178
token_type?: string;
5279
expires_at?: Date;
5380
scope?: string;
5481
refresh_token?: string;
55-
#configuration: OAuth2UserOptions;
82+
#options: OAuth2UserOptions;
5683
#code_verifier?: string;
5784
#code_challenge?: string;
58-
constructor(configuration: OAuth2UserOptions) {
59-
this.#configuration = configuration;
85+
constructor(options: OAuth2UserOptions) {
86+
this.#options = options;
6087
}
6188

62-
async refreshAccessToken() {
89+
/**
90+
* Refresh the access token
91+
*/
92+
async refreshAccessToken(): Promise<void> {
6393
const refresh_token = this.refresh_token;
64-
const credentials = this.#configuration;
94+
const { client_id, client_secret, request_options } = this.#options;
95+
if (!client_id) {
96+
throw new Error("client_id is required");
97+
}
98+
if (!refresh_token) {
99+
throw new Error("refresh_token is required");
100+
}
65101
const data = await rest({
66-
...this.#configuration.request_options,
102+
...request_options,
67103
endpoint: `/2/oauth2/token`,
68104
params: {
105+
client_id,
69106
grant_type: "refresh_token",
70107
refresh_token,
71108
},
72109
method: "POST",
73110
headers: {
74-
...this.#configuration.request_options?.headers,
111+
...request_options?.headers,
75112
"Content-type": "application/x-www-form-urlencoded",
76-
Authorization: basicAuthHeader(
77-
credentials.client_id,
78-
credentials.client_secret
79-
),
113+
...(!!client_secret && {
114+
Authorization: basicAuthHeader(client_id, client_secret),
115+
}),
80116
},
81117
});
82118
this.updateToken(data);
83119
}
84120

121+
/**
122+
* Update token information
123+
*/
85124
updateToken(data: Record<string, any>) {
86125
this.refresh_token = data.refresh_token;
87126
this.#access_token = data.access_token;
@@ -90,7 +129,10 @@ export class OAuth2User implements AuthClient {
90129
this.scope = data.scope;
91130
}
92131

93-
isAccessTokenExpired() {
132+
/**
133+
* Check if an access token is expired
134+
*/
135+
isAccessTokenExpired(): boolean {
94136
const refresh_token = this.refresh_token;
95137
const expires_at = this.expires_at;
96138
return (
@@ -99,92 +141,103 @@ export class OAuth2User implements AuthClient {
99141
);
100142
}
101143

144+
/**
145+
* Request an access token
146+
*/
102147
async requestAccessToken(code?: string): Promise<void> {
103-
const credentials = this.#configuration;
104-
const code_verifier = this.#code_verifier || this.#code_challenge;
148+
const { client_id, client_secret, callback, request_options } =
149+
this.#options;
150+
const code_verifier = this.#code_verifier;
151+
if (!client_id) {
152+
throw new Error("client_id is required");
153+
}
154+
if (!callback) {
155+
throw new Error("callback is required");
156+
}
105157
const params = {
106158
code,
107159
grant_type: "authorization_code",
108-
code_verifier: code_verifier,
109-
client_id: credentials.client_id,
110-
redirect_uri: credentials.callback,
160+
code_verifier,
161+
client_id,
162+
redirect_uri: callback,
111163
};
112164
const data = await rest({
113-
...this.#configuration.request_options,
165+
...request_options,
114166
endpoint: `/2/oauth2/token`,
115-
params: params,
167+
params,
116168
method: "POST",
117169
headers: {
118-
...this.#configuration.request_options?.headers,
170+
...request_options?.headers,
119171
"Content-type": "application/x-www-form-urlencoded",
120-
Authorization: basicAuthHeader(
121-
credentials.client_id,
122-
credentials.client_secret
123-
),
172+
...(!!client_secret && {
173+
Authorization: basicAuthHeader(client_id, client_secret),
174+
}),
124175
},
125176
});
126177
this.updateToken(data);
127178
}
128179

129-
async revokeAccessToken(): Promise<any> {
130-
const credentials = this.#configuration;
180+
/**
181+
* Revoke an access token
182+
*/
183+
async revokeAccessToken(): Promise<RevokeAccessTokenResponse> {
184+
const { client_id, client_secret, request_options } = this.#options;
131185
const access_token = this.#access_token;
132186
const refresh_token = this.refresh_token;
133-
const configuration = this.#configuration;
134-
if (!access_token || !refresh_token)
135-
throw new Error("No access_token or refresh_token found");
136-
const useAccessToken = !!this.#access_token;
137-
const params = {
138-
token_type_hint: useAccessToken ? "access_token" : "refresh_token",
139-
token: useAccessToken ? access_token : refresh_token,
140-
client_id: configuration.client_id,
141-
};
187+
if (!client_id) {
188+
throw new Error("client_id is required");
189+
}
190+
let params: RevokeAccessTokenParams;
191+
if (!!access_token) {
192+
params = {
193+
token_type_hint: "access_token",
194+
token: access_token,
195+
client_id,
196+
};
197+
} else if (!!refresh_token) {
198+
params = {
199+
token_type_hint: "refresh_token",
200+
token: refresh_token,
201+
client_id,
202+
};
203+
} else {
204+
throw new Error("access_token or refresh_token required");
205+
}
142206
return rest({
143-
...this.#configuration.request_options,
207+
...request_options,
144208
endpoint: `/2/oauth2/revoke`,
145-
params: params,
209+
params,
146210
method: "POST",
147211
headers: {
148-
...this.#configuration.request_options?.headers,
212+
...request_options?.headers,
149213
"Content-Type": "application/x-www-form-urlencoded",
150-
Authorization: basicAuthHeader(
151-
credentials.client_id,
152-
credentials.client_secret
153-
),
214+
...(!!client_secret && {
215+
Authorization: basicAuthHeader(client_id, client_secret),
216+
}),
154217
},
155218
});
156219
}
157220

158-
generateAuthURL(
159-
options:
160-
| {
161-
state: string;
162-
code_challenge: string;
163-
code_challenge_method?: "plain";
164-
}
165-
| {
166-
state: string;
167-
code_challenge_method: "s256";
168-
}
169-
): string {
170-
const credentials = this.#configuration;
171-
if (!("callback" in credentials))
172-
throw new Error("You need to provide a callback and scopes");
221+
generateAuthURL(options: GenerateAuthUrlOptions): string {
222+
const { client_id, callback, scopes } = this.#options;
223+
if (!callback) throw new Error("callback required");
224+
if (!scopes) throw new Error("scopes required");
173225
if (options.code_challenge_method === "s256") {
174226
const code_verifier = base64URLEncode(crypto.randomBytes(32));
175227
this.#code_verifier = code_verifier;
176228
this.#code_challenge = base64URLEncode(sha256(code_verifier));
177229
} else {
178230
this.#code_challenge = options.code_challenge;
231+
this.#code_verifier = options.code_challenge;
179232
}
180233
const code_challenge = this.#code_challenge;
181234
const url = new URL("https://twitter.com/i/oauth2/authorize");
182235
url.search = buildQueryString({
183236
...options,
184-
client_id: credentials.client_id,
185-
scope: credentials.scopes.join(" "),
237+
client_id,
238+
scope: scopes.join(" "),
186239
response_type: "code",
187-
redirect_uri: credentials.callback,
240+
redirect_uri: callback,
188241
code_challenge_method: options.code_challenge_method || "plain",
189242
code_challenge,
190243
});

0 commit comments

Comments
 (0)