├── .dev.vars.example ├── .gitignore ├── 1.png ├── 2.png ├── 3.png ├── BACKLOG.md ├── README.md ├── docs.md ├── package.json ├── simplerauth-x.ts ├── template.ts └── wrangler.jsonc /.dev.vars.example: -------------------------------------------------------------------------------- X_CLIENT_ID= X_CLIENT_SECRET= X_REDIRECT_URI = "http://localhost:8787/callback" CALLBACK_REDIRECT_URI = "/dashboard" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- node_modules .dev.vars .wrangler package-lock.json -------------------------------------------------------------------------------- /1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janwilmake/x-oauth-middleware/cef6f2f4e378fd77d357943296048fe001be7432/1.png -------------------------------------------------------------------------------- /2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janwilmake/x-oauth-middleware/cef6f2f4e378fd77d357943296048fe001be7432/2.png -------------------------------------------------------------------------------- /3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janwilmake/x-oauth-middleware/cef6f2f4e378fd77d357943296048fe001be7432/3.png -------------------------------------------------------------------------------- /BACKLOG.md: -------------------------------------------------------------------------------- # IDEA: Sponsorware [Tweet](https://x.com/janwilmake/status/1883363691136946295) use this as easy app onboarding where the we request the user to pay via `X subscriptions` requires 1.6M organic impressions and 2000 verified followers https://docs.x.com/x-api/enterprise-gnip-2.0/fundamentals/account-activity#managing-subscribed-users https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#sponsorshipevent You can then create an offering for your audience, they just need to authenticate with a single button to use your product. [Sponsorware](https://calebporzio.com/sponsorware)! -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- [![](https://badge.forgithub.com/janwilmake/x-oauth-middleware)](https://uithub.com/janwilmake/x-oauth-middleware?lines=false) This is a template made from first principles, for secure login via X OAuth. Use this boilerplate fore easy creation of apps that require X login. If you like to combine this with DORM for sharded databases, see [x-dorm-template](https://github.com/janwilmake/x-dorm-template) To use this: - make a client at https://developer.x.com - make sure to provide the right "User authentication settings", specifically the callback URLs should include https://your-worker.com/callback - Ensure to get the OAuth client/secret, as highlighted below - gather all vars in both `.dev.vars` and `wrangler.toml`, and in your deployed secrets ![](1.png) ![](2.png) You can add as many callbacks as you want (for all your X oauthed workers)! ![](3.png) > [!WARNING] > X Free Plan has low ratelimits you should be aware of. Also there have been issues from my end when using this from cloudflare workers: https://x.com/janwilmake/status/1940654226138374256 -------------------------------------------------------------------------------- /docs.md: -------------------------------------------------------------------------------- OAuth 2.0 OAuth 2.0 Authorization Code Flow with PKCE ​ OAuth 2.0 Authorization Code Flow with PKCE ​ Introduction OAuth 2.0 is an industry-standard authorization protocol that allows for greater control over an application’s scope, and authorization flows across multiple devices. OAuth 2.0 allows you to pick specific fine-grained scopes which give you specific permissions on behalf of a user. To enable OAuth 2.0 in your App, you must enable it in your’s App’s authentication settings found in the App settings section of the developer portal. ​ How long will my credentials stay valid? By default, the access token you create through the Authorization Code Flow with PKCE will only stay valid for two hours unless you’ve used the offline.access scope. ​ Refresh tokens Refresh tokens allow an application to obtain a new access token without prompting the user via the refresh token flow. If the scope offline.access is applied an OAuth 2.0 refresh token will be issued. With this refresh token, you obtain an access token. If this scope is not passed, we will not generate a refresh token. An example of the request you would make to use a refresh token to obtain a new access token is as follows: Copy Ask AI POST 'https://api.x.com/2/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'refresh_token=bWRWa3gzdnk3WHRGU1o0bmRRcTJ5VUxWX1lZTDdJSUtmaWcxbTVxdEFXcW5tOjE2MjIxNDc3NDM5MTQ6MToxOnJ0OjE' \ --data-urlencode 'grant_type=refresh_token' \ --data-urlencode 'client_id=rG9n6402A3dbUJKzXTNX4oWHJ ​ App settings You can select your App’s authentication settings to be OAuth 1.0a or OAuth 2.0. You can also enable an App to access both OAuth 1.0a and OAuth 2.0. OAuth 2.0 can be used with the X API v2 only. If you have selected OAuth 2.0 you will be able to see a Client ID in your App’s Keys and Tokens section. ​ Confidential Clients Confidential clients can hold credentials in a secure way without exposing them to unauthorized parties and securely authenticate with the authorization server they keep your client secret safe. Public clients as they’re usually running in a browser or on a mobile device and are unable to use your client secrets. If you select a type of App that is a confidential client, you will be provided with a client secret. If you selected a type of client that is a confidential client in the developer portal, you will also be able to see a Client Secret. Your options are Native App, Single page App, Web App, Automated App, or bot. Native App and Single page Apps are public clients and Web App and Automated App or bots are confidential clients. You don’t need client id for confidential clients with a valid Authorization Header. You still are required to include Client Id in the body for the requests with a public client. ​ Scopes Scopes allow you to set granular access for your App so that your App only has the permissions that it needs. To learn more about what scopes map to what endpoints, view our authentication mapping guide. Scope Description tweet.read All the Tweets you can view, including Tweets from protected accounts. tweet.write Tweet and Retweet for you. tweet.moderate.write Hide and unhide replies to your Tweets. users.email Email from an authenticated user. users.read Any account you can view, including protected accounts. follows.read People who follow you and people who you follow. follows.write Follow and unfollow people for you. offline.access Stay connected to your account until you revoke access. space.read All the Spaces you can view. mute.read Accounts you’ve muted. mute.write Mute and unmute accounts for you. like.read Tweets you’ve liked and likes you can view. like.write Like and un-like Tweets for you. list.read Lists, list members, and list followers of lists you’ve created or are a member of, including private lists. list.write Create and manage Lists for you. block.read Accounts you’ve blocked. block.write Block and unblock accounts for you. bookmark.read Get Bookmarked Tweets from an authenticated user. bookmark.write Bookmark and remove Bookmarks from Tweets. media.write Upload media. ​ Rate limits For the most part, the rate limits are the same as they are authenticating with OAuth 1.0a, with the exception of Tweets lookup and Users lookup. We are increasing the per-App limit from 300 to 900 requests per 15 minutes while using OAuth 2.0 for Tweet lookup and user lookup. To learn more be sure to check out our documentation on rate limits. ​ Grant types We only provide authorization code with PKCE and refresh token as the supported grant types for this initial launch. We may provide more grant types in the future. ​ OAuth 2.0 Flow OAuth 2.0 uses a similar flow to what we are currently using for OAuth 1.0a. You can check out a diagram and detailed explanation in our documentation on this subject. ​ Glossary Term Description Grant types The OAuth framework specifies several grant types for different use cases and a framework for creating new grant types. Examples include authorization code, client credentials, device code, and refresh token. Confidential client Clients are applications that can securely authenticate with the authorization server, for example, keeping their registered client secret safe. Public client Clients cannot use registered client secrets, such as applications running in a browser or mobile device. Authorization code flow Used by both confidential and public clients to exchange an authorization code for an access token. PKCE An extension to the authorization code flow to prevent several attacks and to be able to perform the OAuth exchange from public clients securely. Client ID Can be found in the keys and tokens section of the developer portal under the header “Client ID.” If you don’t see this, please get in touch with our team directly. The Client ID will be needed to generate the authorize URL. Redirect URI Your callback URL. You will need to have exact match validation. Authorization code This allows an application to hit APIs on behalf of users. Known as the auth_code. The auth_code has a time limit of 30 seconds once the App owner receives an approved auth_code from the user. You will have to exchange it with an access token within 30 seconds, or the auth_code will expire. Access token Access tokens are the token that applications use to make API requests on behalf of a user. Refresh token Allows an application to obtain a new access token without prompting the user via the refresh token flow. Client Secret If you have selected an App type that is a confidential client you will be provided with a “Client Secret” under “Client ID” in your App’s keys and tokens section. ​ Parameters To construct an OAuth 2.0 authorize URL, you will need to ensure you have the following parameters in the authorization URL. Parameter Description response_type You will need to specify that this is a code with the word “code”. client_id Can be found in the developer portal under the header “Client ID”. redirect_uri Your callback URL. This value must correspond to one of the Callback URLs defined in your App’s settings. For OAuth 2.0, you will need to have exact match validation for your callback URL. state A random string you provide to verify against CSRF attacks. The length of this string can be up to 500 characters. code_challenge A PKCE parameter, a random secret for each request you make. code_challenge_method Specifies the method you are using to make a request (S256 OR plain). ​ Authorize URL With OAuth 2.0, you create an authorize URL, which you can use to allow a user to authenticate via an authentication flow, similar to “Sign In” with X. An example of the URL you are creating is as follows: Copy Ask AI https://x.com/i/oauth2/authorize?response_type=code&client_id=M1M5R3BMVy13QmpScXkzTUt5OE46MTpjaQ&redirect_uri=https://www.example.com&scope=tweet.read%20users.read%20account.follows.read%20account.follows.write&state=state&code_challenge=challenge&code_challenge_method=plain You will need to have the proper encoding for this URL to work, be sure to check out our documentation on the percent encoding. OAuth 2.0 How to connect to endpoints using OAuth 2.0 Authorization Code Flow with PKCE ​ How to connect to endpoints using OAuth 2.0 Authorization Code Flow with PKCE ​ How to connect to the endpoints To authenticate your users, your App will need to implement an authorization flow. This authorization flow lets you direct your users to an authorization dialog on X. From there, the primary X experience will show the authorization dialog and handle the authorization on behalf of your App. Your users will be able to authorize your App or decline permission. After the user makes their choice, X will redirect the user to your App, where you can exchange the authorization code for an access token (if the user authorized your App), or handle a rejection (if the user did not authorize your App). ​ Working with confidential clients If you are working with confidential clients, you will need to use a basic authentication scheme for generating an authorization header with base64 encoding while making requests to the token endpoints. The userid and password are separated by a single colon (”:”) character within a base64 encoded string in the credentials. An example would look like this: -header 'Authorization: Basic V1ROclFTMTRiVWhwTWw4M2FVNWFkVGQyTldNNk1UcGphUTotUm9LeDN4NThKQThTbTlKSXQyZm1BanEzcTVHWC1icVozdmpKeFNlR3NkbUd0WEViUA==' If the user agent wishes to send the Client ID “Aladdin” and password “open sesame,” it would use the following header field: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== To create the basic authorization header you will need to base64 encoding on your Client ID and Client Secret which can be obtained from your App’s “Keys and Tokens” page inside of the developer portal. ​ Steps to connect using OAuth 2.0 Step 1: Construct an Authorize URL Your App will need to build an authorize URL to X, indicating the scopes your App needs to authorize. For example, if your App needs to lookup Tweets, users and to manage follows, it should request the following scopes: tweet.read%20users.read%20follows.read%20follows.write The URL will also contain the code_challenge and state parameters, in addition to the other required parameters. In production you should use a random string for the code_challenge. Step 2: GET oauth2/authorize Have the user authenticate and send the application an authorization code. If you have enabled OAuth 2.0 for your App you can find your Client ID inside your App’s “Keys and Tokens” page. An example URL to redirect the user to would look like this: Copy Ask AI https://x.com/i/oauth2/authorize?response_type=code&client_id=M1M5R3BMVy13QmpScXkzTUt5OE46MTpjaQ&redirect_uri=https://www.example.com&scope=tweet.read%20users.read%20follows.read%20follows.write&state=state&code_challenge=challenge&code_challenge_method=plain An example URL with offline_access would look like this: Copy Ask AI https://x.com/i/oauth2/authorize?response_type=code&client_id=M1M5R3BMVy13QmpScXkzTUt5OE46MTpjaQ&redirect_uri=https://www.example.com&scope=tweet.read%20users.read%20follows.read%20offline.access&state=state&code_challenge=challenge&code_challenge_method=plain Upon successful authentication, the redirect_uri you would receive a request containing the auth_code parameter. Your application should verify the state parameter. An example request from client’s redirect would be: Copy Ask AI https://www.example.com/?state=state&code=VGNibzFWSWREZm01bjN1N3dicWlNUG1oa2xRRVNNdmVHelJGY2hPWGxNd2dxOjE2MjIxNjA4MjU4MjU6MToxOmFjOjE Step 3: POST oauth2/token - Access Token At this point, you can use the authorization code to create an access token and refresh token (only if offline.access scope is requested). You can make a POST request to the following endpoint: Copy Ask AI https://api.x.com/2/oauth2/token You will need to pass in the Content-Type of application/x-www-form-urlencoded via a header. Additionally, you should have in your request: code, grant_type, client_id and redirect_uri, and the code_verifier. Here is an example token request for a public client: Copy Ask AI curl --location --request POST 'https://api.x.com/2/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'code=VGNibzFWSWREZm01bjN1N3dicWlNUG1oa2xRRVNNdmVHelJGY2hPWGxNd2dxOjE2MjIxNjA4MjU4MjU6MToxOmFjOjE' \ --data-urlencode 'grant_type=authorization_code' \ --data-urlencode 'client_id=rG9n6402A3dbUJKzXTNX4oWHJ' \ --data-urlencode 'redirect_uri=https://www.example.com' \ --data-urlencode 'code_verifier=challenge' Here is an example using a confidential client: Copy Ask AI curl --location --request POST 'https://api.x.com/2/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --header 'Authorization: Basic V1ROclFTMTRiVWhwTWw4M2FVNWFkVGQyTldNNk1UcGphUTotUm9LeDN4NThKQThTbTlKSXQyZm1BanEzcTVHWC1icVozdmpKeFNlR3NkbUd0WEViUA=='\ --data-urlencode 'code=VGNibzFWSWREZm01bjN1N3dicWlNUG1oa2xRRVNNdmVHelJGY2hPWGxNd2dxOjE2MjIxNjA4MjU4MjU6MToxOmFjOjE' \ --data-urlencode 'grant_type=authorization_code' \ --data-urlencode 'redirect_uri=https://www.example.com' \ --data-urlencode 'code_verifier=challenge' Step 4: Connect to the APIs You are now ready to connect to the endpoints using OAuth 2.0. To do so, you will request the API as you would using Bearer Token authentication. Instead of passing your Bearer Token, you’ll want to use the access token you generated in the last step. As a response, you should see the appropriate payload corresponding to the endpoint you are requesting. This request is the same for both public and confidential clients. An example of the request you would make would look as follows: Copy Ask AI curl --location --request GET 'https://api.x.com/2/tweets?ids=1261326399320715264,1278347468690915330' \ --header 'Authorization: Bearer Q0Mzb0VhZ0V5dmNXSTEyNER2MFNfVW50RzdXdTN6STFxQlVkTGhTc1lCdlBiOjE2MjIxNDc3NDM5MTQ6MToxOmF0OjE' Step 5: POST oauth2/token - refresh token A refresh token allows an application to obtain a new access token without prompting the user. You can create a refresh token by making a POST request to the following endpoint: https://api.x.com/2/oauth2/token You will need to add in the Content-Type of application/x-www-form-urlencoded via a header. In addition, you will also need to pass in your refresh_token, set your grant_type to be a refresh_token, and define your client_id. This request will work for public clients: Copy Ask AI POST 'https://api.x.com/2/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'refresh_token=bWRWa3gzdnk3WHRGU1o0bmRRcTJ5VUxWX1lZTDdJSUtmaWcxbTVxdEFXcW5tOjE2MjIxNDc3NDM5MTQ6MToxOnJ0OjE' \ --data-urlencode 'grant_type=refresh_token' \ --data-urlencode 'client_id=rG9n6402A3dbUJKzXTNX4oWHJ' Here is an example of one for confidential clients: Copy Ask AI POST 'https://api.x.com/2/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --header 'Authorization: Basic V1ROclFTMTRiVWhwTWw4M2FVNWFkVGQyTldNNk1UcGphUTotUm9LeDN4NThKQThTbTlKSXQyZm1BanEzcTVHWC1icVozdmpKeFNlR3NkbUd0WEViUA=='\ --data-urlencode 'refresh_token=bWRWa3gzdnk3WHRGU1o0bmRRcTJ5VUxWX1lZTDdJSUtmaWcxbTVxdEFXcW5tOjE2MjIxNDc3NDM5MTQ6MToxOnJ0OjE'\ --data-urlencode 'grant_type=refresh_token' Step 6: POST oauth2/revoke - Revoke Token A revoke token invalidates an access token or refresh token. This is used to enable a “log out” feature in clients, allowing you to clean up any security credentials associated with the authorization flow that may no longer be necessary. The revoke token is for an App to revoke a token and not a user. You can create a revoke token request by making a POST request to the following URL if the App wants to programmatically revoke the access given to it: Copy Ask AI https://api.x.com/2/oauth2/revoke You will need to pass in the Content-Type of application/x-www-form-urlencoded via a header, your token, and your client_id. In some cases, a user may wish to revoke access given to an App, they can revoke access by visiting the connected Apps page. Copy Ask AI curl --location --request POST 'https://api.x.com/2/oauth2/revoke' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'token=Q0Mzb0VhZ0V5dmNXSTEyNER2MFNfVW50RzdXdTN6STFxQlVkTGhTc1lCdlBiOjE2MjIxNDc3NDM5MTQ6MToxOmF0OjE' \ --data-urlencode 'client_id=rG9n6402A3dbUJKzXTNX4oWHJ' This request will work for confidential clients: Copy Ask AI curl --location --request POST 'https://api.x.com/2/oauth2/revoke' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --header 'Authorization: Basic V1ROclFTMTRiVWhwTWw4M2FVNWFkVGQyTldNNk1UcGphUTotUm9LeDN4NThKQThTbTlKSXQyZm1BanEzcTVHWC1icVozdmpKeFNlR3NkbUd0WEViUA=='\ --data-urlencode 'token=Q0Mzb0VhZ0V5dmNXSTEyNER2MFNfVW50RzdXdTN6STFxQlVkTGhTc1lCdlBiOjE2MjIxNDc3NDM5MTQ6MToxOmF0OjE' -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- { "name": "simplerauth-x", "version": "0.0.4", "main": "simplerauth-x.ts", "files": [ "simplerauth-x.ts" ], "devDependencies": { "@cloudflare/workers-types": "^4.20250124.3" } } -------------------------------------------------------------------------------- /simplerauth-x.ts: -------------------------------------------------------------------------------- /* ======X LOGIN SCRIPT======== This is the most simple version of x oauth. To use it, ensure to create a x oauth client, then set .dev.vars and wrangler.toml alike with the Env variables required And navigate to /login from the homepage, with optional parameters ?scope=a,b,c In localhost this won't work due to your hardcoded redirect url; It's better to simply set your localstorage manually. */ export interface Env { X_CLIENT_ID: string; X_CLIENT_SECRET: string; X_REDIRECT_URI: string; CALLBACK_REDIRECT_URI: string; } export const html = (strings: TemplateStringsArray, ...values: any[]) => { return strings.reduce( (result, str, i) => result + str + (values[i] || ""), "", ); }; async function generateRandomString(length: number): Promise { const randomBytes = new Uint8Array(length); crypto.getRandomValues(randomBytes); return Array.from(randomBytes, (byte) => byte.toString(16).padStart(2, "0"), ).join(""); } async function generateCodeChallenge(codeVerifier: string): Promise { const encoder = new TextEncoder(); const data = encoder.encode(codeVerifier); const digest = await crypto.subtle.digest("SHA-256", data); //@ts-ignore const base64 = btoa(String.fromCharCode(...new Uint8Array(digest))); return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } export const middleware = async (request: Request, env: Env) => { const url = new URL(request.url); const isLocalhost = url.hostname === "localhost"; const securePart = isLocalhost ? "" : "Secure; "; if ( !env.X_CLIENT_ID || !env.X_CLIENT_SECRET || !env.X_REDIRECT_URI || !env.CALLBACK_REDIRECT_URI ) { return new Response( `Please ensure you have all environment variables set up: X_CLIENT_ID=YOUR_ID X_CLIENT_SECRET=YOUR_SECRET X_REDIRECT_URI = "https://yoursite.com/callback" CALLBACK_REDIRECT_URI = "/dashboard"`, { status: 500 }, ); } if (url.pathname === "/logout") { const url = new URL(request.url); const redirectTo = url.searchParams.get("redirect_to") || "/"; return new Response(null, { status: 302, headers: { Location: redirectTo, "Set-Cookie": `x_access_token=; HttpOnly; ${securePart}SameSite=Lax; Max-Age=0; Path=/`, }, }); } // Login page route if (url.pathname === "/login") { const scope = url.searchParams.get("scope"); const state = await generateRandomString(16); const codeVerifier = await generateRandomString(43); const codeChallenge = await generateCodeChallenge(codeVerifier); const headers = new Headers({ Location: `https://x.com/i/oauth2/authorize?response_type=code&client_id=${ env.X_CLIENT_ID }&redirect_uri=${encodeURIComponent( env.X_REDIRECT_URI, )}&scope=${encodeURIComponent( scope || "users.read follows.read tweet.read offline.access", )}&state=${state}&code_challenge=${codeChallenge}&code_challenge_method=S256`, }); headers.append( "Set-Cookie", `x_oauth_state=${state}; HttpOnly; Path=/; ${securePart}SameSite=Lax; Max-Age=600`, ); headers.append( "Set-Cookie", `x_code_verifier=${codeVerifier}; HttpOnly; Path=/; ${securePart}SameSite=Lax; Max-Age=600`, ); return new Response("Redirecting", { status: 307, headers }); } // Twitter OAuth callback route if (url.pathname === "/callback") { const urlState = url.searchParams.get("state"); const code = url.searchParams.get("code"); const cookie = request.headers.get("Cookie") || ""; const cookies = cookie.split(";").map((c) => c.trim()); const stateCookie = cookies .find((c) => c.startsWith("x_oauth_state=")) ?.split("=")[1]; const codeVerifier = cookies .find((c) => c.startsWith("x_code_verifier=")) ?.split("=")[1]; // Validate state and code verifier if ( !urlState || !stateCookie || urlState !== stateCookie || !codeVerifier ) { return new Response( `Invalid state or missing code verifier ${JSON.stringify({ urlState, stateCookie, codeVerifier, })}`, { status: 400, }, ); } console.log({ stateCookie, urlState, codeVerifier }); try { // Exchange code for access token const tokenResponse = await fetch(`https://api.x.com/2/oauth2/token`, { method: "POST", headers: { // Host: "api.x.com", "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${btoa( `${env.X_CLIENT_ID}:${env.X_CLIENT_SECRET}`, )}`, }, body: new URLSearchParams({ code: code || "", client_id: env.X_CLIENT_ID, grant_type: "authorization_code", redirect_uri: env.X_REDIRECT_URI, code_verifier: codeVerifier, }), }); const responseText = await tokenResponse.text(); if (!tokenResponse.ok) { console.log("Response status:", tokenResponse.status); console.log( "Response headers:", Object.fromEntries(tokenResponse.headers.entries()), ); console.log("Response body:", responseText); throw new Error( `Twitter API responded with ${tokenResponse.status} - ${responseText}`, ); } const data: any = JSON.parse(responseText); const headers = new Headers({ Location: url.origin + (env.CALLBACK_REDIRECT_URI || "/"), }); const { access_token, refresh_token } = data; // NB: Here you can optionally retrieve the user or do other queries one-time. Beware of ratelimits on the free plan of X as they are very low. // For `/users/me` it's 25 per 24h per user so you shouldn't request this too often! // Another thing you can do here is set a KV to ensure we can verify the access_token. This way, you won't need to do it again. // try { // const res = await fetch( // "https://api.x.com/2/users/me?user.fields=profile_image_url", // { // headers: { Authorization: `Bearer ${access_token}` }, // }, // ); // const { // data, // }: { // data: { name: string; username: string; profile_image_url: string }; // } = await res.json(); // } catch { // console.log("Could not get the data"); // } // Set access token cookie and clear temporary cookies headers.append( "Set-Cookie", `x_access_token=${encodeURIComponent( access_token, )}; HttpOnly; Path=/; ${securePart}SameSite=Lax; Max-Age=34560000`, ); if (refresh_token) { headers.append( "Set-Cookie", `x_refresh_token=${encodeURIComponent( refresh_token, )}; HttpOnly; Path=/; ${securePart}SameSite=Lax; Max-Age=34560000`, ); } headers.append("Set-Cookie", `x_oauth_state=; Max-Age=0`); headers.append("Set-Cookie", `x_code_verifier=; Max-Age=0`); return new Response("Redirecting", { status: 307, headers }); } catch (error) { return new Response( html` Login Failed

Twitter Login Failed

${error instanceof Error ? error.message : "Unknown error"}

`, { status: 500, headers: { "Content-Type": "text/html", "Set-Cookie": `x_oauth_state=; Max-Age=0, x_code_verifier=; Max-Age=0`, }, }, ); } } }; -------------------------------------------------------------------------------- /template.ts: -------------------------------------------------------------------------------- import { Env, middleware } from "./simplerauth-x"; export default { fetch: async (request: Request, env: Env) => { const response = await middleware(request, env); if (response) return response; const url = new URL(request.url); const token = request.headers .get("Cookie") ?.split(";") .find((r) => r.includes("x_access_token")) ?.split("=")[1] || url.searchParams.get("apiKey"); if (url.pathname === "/dashboard") { try { const res = await fetch( "https://api.x.com/2/users/me?user.fields=profile_image_url", { headers: { Authorization: `Bearer ${token}` }, }, ); const { data: { name, username, profile_image_url }, } = await res.json(); return new Response( `Dashboard

X Dashboard

${name}

@${username}

Home | Logout
`, { headers: { "content-type": "text/html" } }, ); } catch { return new Response( `Error

Error

Home`, { status: 500, headers: { "content-type": "text/html" } }, ); } } return new Response( `X Login

X Login Demo

OAuth 2.0 for X/Twitter

${ token ? "Dashboard" : "Login with X" }

GitHub
`, { headers: { "content-type": "text/html" } }, ); }, }; -------------------------------------------------------------------------------- /wrangler.jsonc: -------------------------------------------------------------------------------- { "$schema": "https://unpkg.com/wrangler@latest/config-schema.json", "name": "x-oauth-middleware", "compatibility_date": "2025-06-05", "main": "template.ts", "routes": [{ "pattern": "x.simplerauth.com", "custom_domain": true }], "env": { "staging": { "routes": [] } } } --------------------------------------------------------------------------------