Skip to content

Commit

Permalink
Remove V1.1 verify_credentials call after successful token retrieval (#…
Browse files Browse the repository at this point in the history
…26)

* Remove V1.1 verify_credentials call after successful token retrieval

* Update README to remove stuff related to verify_credentials
  • Loading branch information
na2hiro committed Jun 17, 2023
1 parent 80323f6 commit 44cb3df
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 179 deletions.
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Remix Auth plugin for Twitter OAuth 1.0a.

## Demo

Try out [live demo](https://remix-auth-twitter-example.na2hiro.workers.dev/) ([source code](https://github.com/na2hiro/remix-auth-twitter-example))
Try out ~~[live demo](https://remix-auth-twitter-example.na2hiro.workers.dev/)~~ Currently it doesn't work due to updates on Twitter APIs. You could try cloning the [source code](https://github.com/na2hiro/remix-auth-twitter-example)

## Installation

Expand Down Expand Up @@ -53,15 +53,11 @@ authenticator.use(
clientID,
clientSecret,
callbackURL: "https://my-app/login/callback",
// In order to get user's email address, you need to configure your app permission.
// See https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/get-account-verify_credentials.
includeEmail: true, // Optional parameter. Default: false.
alwaysReauthorize: false // otherwise, ask for permission every time
},
// Define what to do when the user is authenticated
async ({ accessToken, accessTokenSecret, profile }) => {
// profile contains all the info from `account/verify_credentials`
// https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/get-account-verify_credentials
// profile contains userId and screenName

// Return a user object to store in sessionStorage.
// You can also throw Error to reject the login
Expand Down
88 changes: 28 additions & 60 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,30 @@ import { v4 as uuid } from "uuid";
import hmacSHA1 from "crypto-js/hmac-sha1";
import Base64 from "crypto-js/enc-base64";
import { fixedEncodeURIComponent } from "./utils";
import type { TwitterProfile } from "./twitterInterface";

export type { TwitterProfile } from "./twitterInterface";

let debug = createDebug("TwitterStrategy");

const requestTokenURL = "https://api.twitter.com/oauth/request_token";
const authorizationURL = "https://api.twitter.com/oauth/authorize";
const authenticationURL = "https://api.twitter.com/oauth/authenticate";
const tokenURL = "https://api.twitter.com/oauth/access_token";
const verifyCredentialsURL =
"https://api.twitter.com/1.1/account/verify_credentials.json";

export interface TwitterStrategyOptions {
clientID: string;
clientSecret: string;
callbackURL: string;
includeEmail?: boolean;
alwaysReauthorize?: boolean;
}

export interface Profile {
userId: string;
screenName: string;
}

export interface TwitterStrategyVerifyParams {
accessToken: string;
accessTokenSecret: string;
profile: TwitterProfile;
profile: Profile;
context?: AppLoadContext;
}

Expand All @@ -60,7 +59,6 @@ export const TwitterStrategyDefaultName = "twitter";
* - `clientID` identifies client to service provider
* - `clientSecret` secret used to establish ownership of the client identifier
* - `callbackURL` URL to which the service provider will redirect the user after obtaining authorization
* - `includeEmail` Whether to return the user email (optional. default: false)
* - `alwaysReauthorize` If set to true, always as app permissions. This was v1 behavior.
* If false, just let them login if they've once accepted the permission. (optional. default: false)
*
Expand All @@ -70,7 +68,6 @@ export const TwitterStrategyDefaultName = "twitter";
* clientID: '123-456-789',
* clientSecret: 'shhh-its-a-secret',
* callbackURL: 'https://www.example.net/auth/example/callback',
* includeEmail: true
* },
* async ({ accessToken, accessTokenSecret, profile }) => {
* return await User.findOrCreate(profile.id, profile.email, ...);
Expand All @@ -86,7 +83,6 @@ export class TwitterStrategy<User> extends Strategy<
protected clientID: string;
protected clientSecret: string;
protected callbackURL: string;
protected includeEmail: boolean;
protected alwaysReauthorize: boolean;

constructor(
Expand All @@ -97,7 +93,6 @@ export class TwitterStrategy<User> extends Strategy<
this.clientID = options.clientID;
this.clientSecret = options.clientSecret;
this.callbackURL = options.callbackURL;
this.includeEmail = options.includeEmail || false;
this.alwaysReauthorize = options.alwaysReauthorize || false;
}

Expand All @@ -106,7 +101,7 @@ export class TwitterStrategy<User> extends Strategy<
sessionStorage: SessionStorage,
options: AuthenticateOptions
): Promise<User> {
debug("Request URL", request.url);
debug("Request URL", request.url.toString());
let url = new URL(request.url);
let session = await sessionStorage.getSession(
request.headers.get("Cookie")
Expand All @@ -121,11 +116,10 @@ export class TwitterStrategy<User> extends Strategy<
}

let callbackURL = this.getCallbackURL(url);
debug("Callback URL", callbackURL);
debug("Callback URL", callbackURL.toString());

// Before user navigates to login page: Redirect to login page
if (url.pathname !== callbackURL.pathname) {
debug("Requesting request token");
// Unlike OAuth2, we first hit the request token endpoint
const { requestToken, callbackConfirmed } = await this.fetchRequestToken(
callbackURL
Expand All @@ -152,6 +146,7 @@ export class TwitterStrategy<User> extends Strategy<

const denied = url.searchParams.get("denied");
if (denied) {
debug("Denied");
return await this.failure(
"Please authorize the app",
request,
Expand All @@ -177,16 +172,8 @@ export class TwitterStrategy<User> extends Strategy<
params.set("oauth_token", oauthToken);
params.set("oauth_verifier", oauthVerifier);

let { accessToken, accessTokenSecret } = await this.fetchAccessToken(
params
);

// Get the profile
let profile = await this.userProfile(
accessToken,
accessTokenSecret,
this.includeEmail
);
let { accessToken, accessTokenSecret, ...profile } =
await this.fetchAccessTokenAndProfile(params);

// Verify the user and return it, or redirect
try {
Expand Down Expand Up @@ -242,7 +229,9 @@ export class TwitterStrategy<User> extends Strategy<
);
const url = new URL(requestTokenURL);
url.search = new URLSearchParams(parameters).toString();
let response = await fetch(url.toString(), {
const urlString = url.toString();
debug("Fetching request token", urlString);
let response = await fetch(urlString, {
method: "GET",
});

Expand Down Expand Up @@ -319,12 +308,15 @@ export class TwitterStrategy<User> extends Strategy<
/**
* Step 3: Fetch access token to do anything
*/
private async fetchAccessToken(params: URLSearchParams): Promise<{
private async fetchAccessTokenAndProfile(params: URLSearchParams): Promise<{
accessToken: string;
accessTokenSecret: string;
userId: string;
screenName: string;
}> {
params.set("oauth_consumer_key", this.clientID);

debug("Fetch access token", tokenURL, params.toString());
let response = await fetch(tokenURL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
Expand All @@ -337,52 +329,28 @@ export class TwitterStrategy<User> extends Strategy<
throw new Response(body, { status: 401 });
}

return await this.getAccessToken(response.clone() as unknown as Response);
return await this.extractAccessTokenAndProfile(
response.clone() as unknown as Response
);
}

protected async getAccessToken(response: Response): Promise<{
protected async extractAccessTokenAndProfile(response: Response): Promise<{
accessToken: string;
accessTokenSecret: string;
userId: string;
screenName: string;
}> {
const text = await response.text();
const obj: { [key: string]: string } = {};
for (const pair of text.split("&")) {
const [key, value] = pair.split("=");
obj[key] = value;
}
const accessToken = obj.oauth_token as string;
const accessTokenSecret = obj.oauth_token_secret as string;
return {
accessToken,
accessTokenSecret,
accessToken: obj.oauth_token as string,
accessTokenSecret: obj.oauth_token_secret as string,
userId: obj.user_id as string,
screenName: obj.screen_name as string,
} as const;
}

/**
* Retrieve user profile from service provider.
*
* OAuth 2.0-based authentication strategies can override this function in
* order to load the user's profile from the service provider. This assists
* applications (and users of those applications) in the initial registration
* process by automatically submitting required information.
*/
protected async userProfile(
accessToken: string,
accessTokenSecret: string,
includeEmail: boolean
): Promise<TwitterProfile> {
const params = this.signRequest(
{
oauth_token: accessToken,
include_email: includeEmail ? "true" : "false",
},
"GET",
verifyCredentialsURL,
accessTokenSecret
);
const url = new URL(verifyCredentialsURL);
url.search = new URLSearchParams(params).toString();
const response = await fetch(url.toString());
return await response.json();
}
}
90 changes: 0 additions & 90 deletions src/twitterInterface.ts

This file was deleted.

Loading

0 comments on commit 44cb3df

Please sign in to comment.