Repository: honojs/middleware
Pull request #981: feat(oauth-providers): Add Twitch OAuth Provider
Author: login: Younis-Ahmed
Created: 2025-02-25T16:12:04Z | Merged: 2025-03-12T08:55:47Z

# Add Twitch OAuth Provider

## Which middleware is the feature for?
@hono/oauth-providers

## What is the feature you are proposing?

This PR introduces a Twitch OAuth provider, expanding the middleware's OAuth offerings. It includes a new middleware for Twitch authentication, a dedicated `AuthFlow` class, token refreshing/revocation/validation, and comprehensive type definitions. Detailed tests ensure correct behavior and error handling. resolves #983

## Key Changes

*   **Twitch OAuth Middleware [src/providers/twitch/twitchAuth.ts](https://github.com/honojs/middleware/blob/b46e20e02f8038e02e3f4ff445fef40202c131ac/packages/oauth-providers/src/providers/twitch/twitchAuth.ts):** Implements the core authentication flow, handling state management, redirects, and context variable setting (`token`, `refresh-token`, `user-twitch`, `granted-scopes`).
*   **AuthFlow Class [src/providers/twitch/authFlow.ts](https://github.com/honojs/middleware/blob/b46e20e02f8038e02e3f4ff445fef40202c131ac/packages/oauth-providers/src/providers/twitch/authFlow.ts):** Encapsulates token exchange and user data retrieval, with robust error handling.
*   **Token Operations [src/providers/twitch/refreshToken.ts](https://github.com/honojs/middleware/blob/b46e20e02f8038e02e3f4ff445fef40202c131ac/packages/oauth-providers/src/providers/twitch/refreshToken.ts), `src/providers/twitch/revokeToken.ts`):** Provides functions for refreshing and revoking tokens.
*   **Type Definitions [src/providers/twitch/types.ts](https://github.com/honojs/middleware/blob/b46e20e02f8038e02e3f4ff445fef40202c131ac/packages/oauth-providers/src/providers/twitch/types.ts):** Defines comprehensive types for Twitch API responses.
*   **Extensive Testing (`test/handlers.ts`, `test/index.test.ts`):** Includes unit tests covering redirection, valid code flow, error handling, refresh/revoke token, custom and built-in state scenarios, using a mock server.
*  **Validate Token  [src/providers/twitch/validateToken](https://github.com/honojs/middleware/blob/b46e20e02f8038e02e3f4ff445fef40202c131ac/packages/oauth-providers/src/providers/twitch/validateToken.ts)**:  That hit `/validate` endpoint to verify that the access token is still valid for reasons other than token expiring.

   > **Example: if your app provides a Log in with Twitch feature, the user signs in to the app as a Twitch user, which creates an OAuth session for them. If the user disconnects the integration while still signed in, the expectation is that your app should sign out the user and terminate the OAuth session. To discover that the user has disconnected the integration, the app needs to call the /validate endpoint to determine that the token is no longer valid and to sign out the user.**

## Checklist

*  [x] Create a `validateToken` that hit `/validate` endpoint to verify that the access token is still valid [for reasons other than token exirping](https://dev.twitch.tv/docs/authentication/validate-tokens/)
*   [x] Core Twitch OAuth flow implemented
*   [x] Implemented AuthFlow with error handling
*   [x] Added refresh/revoke token operations
*   [x] Comprehensive TypeScript types
*   [x] Extensive unit tests
*   [x] Updated module declarations
*   [x] Update Readme.md with examples
*   [x] Add tests
*   [x] Run tests
*   [x] `yarn changeset` with summary
*   [x] Follow contribution guide


Linked issue #983: feat(oauth-providers): Adding Twitch oauth provider
### Which middleware is the feature for?

@hono/oauth-providers

### What is the feature you are proposing?

## Twitch OAuth provider

Adding Twitch OAuth provider middleware, expanding the middleware's OAuth variety offerings. 

PR #981 (tests included)

### Why?

While using **honojs** with `@oauth-providers` for a stream related project, Though the package covers most major social media platforms to delegate the OAuth flow to, I found myself limited beyond the current options.

## Usage

The enhancement introduces a main middleware for the provider alongside helper functions for refreshing, revoking and validating tokens:

```ts
// Twitch middleware

hono.use('auth/twitch',
    twitchAuth({
        client_id: Bun.env.TWITCH_ID as string,
        client_secret: Bun.env.TWITCH_SECRET as string,
        redirect_uri: 'http://localhost:3000/auth/twitch',
        scope: ['user:read:email'],
    }),
    async (c) => {
        const user = c.get('user-twitch')
        const token = c.get('token')
        const refreshToken = c.get('refresh-token')
        const grantedScopes = c.get('granted-scopes')
        return c.json({
            user,
            token,
            refreshToken,
            grantedScopes,
        })
    }

)
``` 

```ts
// Using refresh token helper func
app.get('/refresh', async (c) => {
  const refreshTokenValue = c.get('refreshToken')

  try {
    const result = await refreshToken(
      Bun.env.TWITCH_ID as string,
      Bun.env.TWITCH_SECRET as string,
      refreshTokenValue
    )
    c.logger.log(result)
    return c.text(`Token refreshed`)
  } catch (error) {
    return c.text(`Error ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
  }
})
```

```ts
// Using revoke token func
app.get('/revoke', async (c) => {
  const token = c.req.query('token')
  
  if (!token) {
    return c.text('Token is required', 400)
  }

  try {
     const result = await revokeToken(
      Bun.env.TWITCH_ID as string,
      token
    )
    return c.text("✅")
  } catch (error) {
  retrun c.text("❌")
  }
```

```ts
// Using validate token func
app.get('/validate', async (c) => {
  const token = c.req.query('token')
  
  if (!token) {
    return c.text('Token is required', 400)
  }

  try {
    const validationResult = await validateToken(token)
    return c.text("✅")
  } catch (error) {
  retrun c.text("❌")
  }
```



Diff:
diff --git a/.changeset/slow-days-knock.md b/.changeset/slow-days-knock.md
new file mode 100644
index 000000000..418a56390
--- /dev/null
+++ b/.changeset/slow-days-knock.md
@@ -0,0 +1,17 @@
+---
+'@hono/oauth-providers': minor
+---
+
+These chages introduces a Twitch OAuth provider, expanding the middleware's OAuth offerings. It includes a new middleware for Twitch authentication, a dedicated `AuthFlow` class, token refreshing/revocation/validation, and comprehensive type definitions. Detailed tests ensure correct behavior and error handling.
+
+- **Twitch OAuth Middleware `src/providers/twitch/twitchAuth.ts`:** Implements the core authentication flow, handling state management, redirects, and context variable setting (`token`, `refresh-token`, `user-twitch`, `granted-scopes`).
+
+- **AuthFlow Class `src/providers/twitch/authFlow.ts`:** Encapsulates token exchange and user data retrieval, with robust error handling.
+
+- **Token Operations `src/providers/twitch/refreshToken.ts`:** Provides functions for refreshing and revoking tokens.
+
+- **Type Definitions `src/providers/twitch/types.ts:** Defines comprehensive types for Twitch API responses.
+
+- **Extensive Testing (`test/handlers.ts`, `test/index.test.ts`):** Includes unit tests covering redirection, valid code flow, error handling, refresh/revoke token, custom and built-in state scenarios, using a mock server.
+
+- **Validate Token `src/providers/twitch/validateToken`**: That hit `/validate` endpoint to verify that the access token is still valid for reasons other than token expiring.
diff --git a/packages/oauth-providers/README.md b/packages/oauth-providers/README.md
index 0376bd5f4..61dd147ae 100644
--- a/packages/oauth-providers/README.md
+++ b/packages/oauth-providers/README.md
@@ -918,6 +918,174 @@ app.post('/remove-user', async (c, next) => {
 })
 ```
 
+### Twitch
+
+```ts
+import { Hono } from 'hono'
+import { twitchAuth } from '@hono/oauth-providers/twitch'
+
+const app = new Hono()
+
+app.use(
+  '/twitch',
+  twitchAuth({
+    client_id: Bun.env.TWITCH_ID,
+    client_secret: Bun.env.TWITCH_SECRET,
+    scope: ['user:read:email', 'channel:read:subscriptions', 'bits:read'],
+    redirect_uri: 'http://localhost:3000/twitch',
+  })
+)
+
+export default app
+```
+
+#### Parameters
+
+- `client_id`:
+  - Type: `string`.
+  - `Required`.
+  - Your app client ID. You can find this value in the [Twitch Developer Portal](https://dev.twitch.tv/console/apps). <br />When developing **Cloudflare Workers**, there's no need to send this parameter. Just declare it in the `wrangler.toml` file as `TWITCH_ID=`.
+- `client_secret`:
+  - Type: `string`.
+  - `Required`.
+  - Your app client secret. You can find this value in the [Twitch Developer Portal](https://dev.twitch.tv/console/apps). <br />When developing **Cloudflare Workers**, there's no need to send this parameter. Just declare it in the `wrangler.toml` file as `TWITCH_SECRET=`.
+    > ⚠️ Do **not** share your **client secret** to ensure the security of your app.
+- `scope`:
+  - Type: `string[]`.
+  - `Required`.
+  - Set of **permissions** to request the user's authorization to access your app for retrieving user information and performing actions on their behalf.<br /> Review all the scopes Twitch offers for utilizing their API on the [Twitch API Reference](https://dev.twitch.tv/docs/authentication/scopes).
+- `redirect_uri`:
+  - Type: `string`.
+  - `Required`.
+  - The URI to which the user will be redirected after authentication.
+- `state`:
+  - Type: `string`.
+  - `Optional`.
+  - A unique string to protect against Cross-Site Request Forgery (CSRF) attacks. The state is passed back to your redirect URI after the user has authenticated. You should verify that the state matches the one you provided in the initial request.
+- `force_verify`:
+  - Type: `boolean`.
+  - `Optional`.
+  - Set this value to `true` if you want to force the user to verify their account. Defaults to `false`.
+
+#### Authentication Flow
+
+After the completion of the Twitch OAuth flow, essential data has been prepared for use in the subsequent steps that your app needs to take.
+
+`twitchAuth` method provides 4 set key data:
+
+- `token`:
+  - Access token to make requests to the Twitch API for retrieving user information and performing actions on their behalf.
+  - Type:
+    ```
+    {
+      token: string
+      expires_in: number
+    }
+    ```
+- `refresh-token`:
+  - You can refresh new tokens using this token. The duration of this token is not specified on the Twitch docs.
+  - Type:
+    ```
+    {
+      token: string
+      expires_in: number
+    }
+    ```
+- `granted-scopes`:
+  - Scopes for which the user has granted permissions.
+  - Type: `string[]`.
+- `user-twitch`:
+  - User basic info retrieved from Twitch
+  - Type:
+    ```
+    {
+      id: string
+      login: string
+      display_name: string
+      type: string
+      broadcaster_type: string
+      description: string
+      profile_image_url: string
+      offline_image_url: string
+      view_count: number
+      email: string
+      created_at: string
+    }
+    ```
+
+> [!NOTE]
+> To access this data, utilize the `c.get` method within the callback of the upcoming HTTP request handler.
+
+```ts
+app.get('/twitch', (c) => {
+  const token = c.get('token')
+  const refreshToken = c.get('refresh-token')
+  const grantedScopes = c.get('granted-scopes')
+  const user = c.get('user-twitch')
+
+  return c.json({
+    token,
+    refreshToken,
+    grantedScopes,
+    user,
+  })
+})
+```
+
+#### Refresh Token
+
+Once the user token expires you can refresh their token without the need to prompt the user again for access. In such scenario, you can utilize the `refreshToken` method, which accepts the `client_id`, `client_secret` and `refresh_token` as parameters.
+
+> [!NOTE]
+> The `refresh_token` can be used once. Once the token is refreshed Twitch gives you a new `refresh_token` along with the new token.
+
+```ts
+import { twitchAuth, refreshToken } from '@hono/oauth-providers/twitch'
+
+app.post('/twitch/refresh', async (c, next) => {
+  const newTokens = await refreshToken(CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN)
+
+  // newTokens = {
+  //   token_type: 'bearer',
+  //   access_token: 'new-access-token',
+  //   expires_in: 60000,
+  //   refresh_token: 'new-refresh-token',
+  //   scope: ['user:read:email', 'channel:read:subscriptions', 'bits:read']
+  // }
+  // ...
+})
+```
+
+#### Revoke Token
+
+In certain use cases, you may need to programmatically revoke a user's access token. In such scenarios, you can utilize the `revokeToken` method, the `client_id` and the `token` to be revoked as parameters.
+
+It returns a `boolean` to tell whether the token was revoked or not.
+
+```ts
+import { twitchAuth, revokeToken } from '@hono/oauth-providers/twitch'
+
+app.post('/remove-user', async (c, next) => {
+  const revoked = await revokeToken(CLIENT_ID, USER_TOKEN)
+
+  // revoked = true | false
+  // ...
+})
+```
+
+#### Validate Token
+
+You can validate a Twitch access token to verify it's still valid or to obtain information about the token, such as its expiration date, scopes, and the associated user.
+
+You can use `validateToken` method, which accepts the `token` to be validated as parameter and returns `TwitchValidateSuccess` if valid or throws `HTTPException` upon failure.
+
+
+> **IMPORTANT:** Twitch requires applications to validate OAuth tokens when they start and on an hourly basis thereafter. Failure to validate tokens may result in Twitch taking punitive action, such as revoking API keys or throttling performance. When a token becomes invalid, your app should terminate all sessions using that token immediately. [Read more](https://dev.twitch.tv/docs/authentication/validate-tokens)
+
+The validation endpoint helps your application detect when tokens become invalid for reasons other than expiration, such as when users disconnect your integration from their Twitch account. When a token becomes invalid, your app should terminate all sessions using that token.
+
+> For security and compliance, make sure to implement regular token validation in your application. If a token becomes invalid, promptly sign out the user and terminate their OAuth session.
+
 ## Advance Usage
 
 ### Customize `redirect_uri`
diff --git a/packages/oauth-providers/package.json b/packages/oauth-providers/package.json
index 3807995be..e1b05da07 100644
--- a/packages/oauth-providers/package.json
+++ b/packages/oauth-providers/package.json
@@ -83,6 +83,16 @@
         "types": "./dist/providers/discord/index.d.ts",
         "default": "./dist/providers/discord/index.js"
       }
+    },
+    "./twitch" : {
+      "import": {
+        "types": "./dist/providers/twitch/index.d.mts",
+        "default": "./dist/providers/twitch/index.mjs"
+      },
+      "require": {
+        "types": "./dist/providers/twitch/index.d.ts",
+        "default": "./dist/providers/twitch/index.js"
+      }
     }
   },
   "typesVersions": {
@@ -104,6 +114,9 @@
       ],
       "discord": [
         "./dist/providers/discord/index.d.ts"
+      ],
+      "twitch": [
+        "./dist/providers/twitch/index.d.ts"
       ]
     }
   },
diff --git a/packages/oauth-providers/src/providers/twitch/authFlow.ts b/packages/oauth-providers/src/providers/twitch/authFlow.ts
new file mode 100644
index 000000000..70d0317ba
--- /dev/null
+++ b/packages/oauth-providers/src/providers/twitch/authFlow.ts
@@ -0,0 +1,131 @@
+import { HTTPException } from 'hono/http-exception'
+
+import type { Token } from '../../types'
+import { toQueryParams } from '../../utils/objectToQuery'
+import type {
+  TwitchErrorResponse,
+  TwitchUserResponse,
+  TwitchTokenResponse,
+  TwitchUser,
+  Scopes,
+} from './types'
+
+type TwitchAuthFlow = {
+  client_id: string
+  client_secret: string
+  redirect_uri: string
+  scope: Scopes[]
+  state: string
+  code: string | undefined
+  force_verify: boolean | undefined
+}
+
+export class AuthFlow {
+  client_id: string
+  client_secret: string
+  redirect_uri: string
+  scope: string
+  state: string
+  code: string | undefined
+  token: Token | undefined
+  refresh_token: Token | undefined
+  granted_scopes: string[] | undefined
+  user: Partial<TwitchUser> | undefined
+  force_verify: boolean | undefined
+
+  constructor({
+    client_id,
+    client_secret,
+    redirect_uri,
+    scope,
+    state,
+    code,
+    force_verify,
+  }: TwitchAuthFlow) {
+    this.client_id = client_id
+    this.client_secret = client_secret
+    this.redirect_uri = redirect_uri
+    this.scope = scope.join(' ')
+    this.state = state
+    this.code = code
+    this.refresh_token = undefined
+    this.force_verify = force_verify
+    this.granted_scopes = undefined
+    this.user = undefined
+  }
+
+  redirect() {
+    const parsedOptions = toQueryParams({
+      client_id: this.client_id,
+      force_verify: this.force_verify,
+      redirect_uri: this.redirect_uri,
+      response_type: 'code',
+      scope: this.scope,
+      state: this.state,
+    })
+    return `https://id.twitch.tv/oauth2/authorize?${parsedOptions}`
+  }
+
+  private async getTokenFromCode() {
+    const parsedOptions = toQueryParams({
+      client_id: this.client_id,
+      client_secret: this.client_secret,
+      code: this.code,
+      grant_type: 'authorization_code',
+      redirect_uri: this.redirect_uri,
+    })
+
+    const url = 'https://id.twitch.tv/oauth2/token'
+
+    const response = (await fetch(url, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded',
+      },
+      body: parsedOptions,
+    }).then((res) => res.json() as Promise<TwitchTokenResponse>))
+
+    if ('error' in response) {
+      throw new HTTPException(400, { message: response.error })
+    }
+
+    if ('access_token' in response) {
+      this.token = {
+        token: response.access_token,
+        expires_in: response.expires_in,
+      }
+    }
+
+    if ('refresh_token' in response) {
+      this.refresh_token = {
+        token: response.refresh_token,
+        expires_in: 0,
+      }
+    }
+
+    if ('scope' in response) {
+      this.granted_scopes = response.scope
+    }
+  }
+
+  async getUserData() {
+    await this.getTokenFromCode()
+    const response = (await fetch('https://api.twitch.tv/helix/users', {
+      headers: {
+        authorization: `Bearer ${this.token?.token}`,
+        'Client-ID': this.client_id,
+      },
+    }).then((res) => res.json())) as TwitchUserResponse | TwitchErrorResponse
+
+    if ('error' in response) {
+      throw new HTTPException(400, { message: JSON.stringify(response) })
+    }
+    if ('message' in response) {
+      throw new HTTPException(400, { message: JSON.stringify(response) })
+    }
+
+    if ('data' in response) {
+      this.user = response.data[0]
+    }
+  }
+}
diff --git a/packages/oauth-providers/src/providers/twitch/index.ts b/packages/oauth-providers/src/providers/twitch/index.ts
new file mode 100644
index 000000000..239d425f9
--- /dev/null
+++ b/packages/oauth-providers/src/providers/twitch/index.ts
@@ -0,0 +1,13 @@
+export { twitchAuth } from './twitchAuth'
+export { refreshToken } from './refreshToken'
+export { revokeToken } from './revokeToken'
+export { validateToken } from './validateToken'
+export * from './types'
+import type { OAuthVariables } from '../../types'
+import type { TwitchUser } from './types'
+
+declare module 'hono' {
+  interface ContextVariableMap extends OAuthVariables {
+    'user-twitch': Partial<TwitchUser> | undefined
+  }
+}
diff --git a/packages/oauth-providers/src/providers/twitch/refreshToken.ts b/packages/oauth-providers/src/providers/twitch/refreshToken.ts
new file mode 100644
index 000000000..2e24c0550
--- /dev/null
+++ b/packages/oauth-providers/src/providers/twitch/refreshToken.ts
@@ -0,0 +1,34 @@
+import { HTTPException } from 'hono/http-exception'
+import { toQueryParams } from '../../utils/objectToQuery'
+import type { TwitchRefreshResponse } from './types'
+
+export async function refreshToken(
+  client_id: string,
+  client_secret: string,
+  refresh_token: string
+): Promise<TwitchRefreshResponse> {
+  const params = toQueryParams({
+    grant_type: 'refresh_token',
+    refresh_token,
+    client_id,
+    client_secret,
+  })
+
+  const response = (await fetch('https://id.twitch.tv/oauth2/token', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/x-www-form-urlencoded',
+    },
+    body: params,
+  }).then((res) => res.json() as Promise<TwitchRefreshResponse>))
+
+  if ('error' in response) {
+    throw new HTTPException(400, { message: response.error })
+  }
+  
+  if ('message' in response) {
+    throw new HTTPException(400, { message: response.message as string })
+  }
+
+  return response
+}
diff --git a/packages/oauth-providers/src/providers/twitch/revokeToken.ts b/packages/oauth-providers/src/providers/twitch/revokeToken.ts
new file mode 100644
index 000000000..1619506c9
--- /dev/null
+++ b/packages/oauth-providers/src/providers/twitch/revokeToken.ts
@@ -0,0 +1,39 @@
+import { HTTPException } from 'hono/http-exception'
+import { toQueryParams } from '../../utils/objectToQuery'
+import type { TwitchRevokingResponse } from './types'
+
+export async function revokeToken(
+  client_id: string,
+  token: string
+): Promise<boolean> {
+  const params = toQueryParams({
+    client_id: client_id,
+    token,
+  })
+
+  const res = await fetch('https://id.twitch.tv/oauth2/revoke', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/x-www-form-urlencoded',
+    },
+    body: params,
+  })
+
+  // Check HTTP status code first
+  if (!res.ok) {
+    // Try to parse error response
+    try {
+      const errorResponse = await res.json() as TwitchRevokingResponse
+      if (errorResponse && typeof errorResponse === 'object' && 'message' in errorResponse) {
+        throw new HTTPException(400, { message: errorResponse.message })
+      }
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    } catch (e) {
+      // If parsing fails, throw a generic error with the status
+      throw new HTTPException(400, { message: `Token revocation failed with status: ${res.status}` })
+    }
+  }
+
+  // Success case - Twitch returns 200 with empty body on successful revocation
+  return true
+}
diff --git a/packages/oauth-providers/src/providers/twitch/twitchAuth.ts b/packages/oauth-providers/src/providers/twitch/twitchAuth.ts
new file mode 100644
index 000000000..b3cb29222
--- /dev/null
+++ b/packages/oauth-providers/src/providers/twitch/twitchAuth.ts
@@ -0,0 +1,61 @@
+import type { MiddlewareHandler } from 'hono'
+import { env } from 'hono/adapter'
+import { getCookie, setCookie } from 'hono/cookie'
+import { HTTPException } from 'hono/http-exception'
+
+import { getRandomState } from '../../utils/getRandomState'
+import { AuthFlow } from './authFlow'
+import type { Scopes } from './types'
+
+export function twitchAuth(options: {
+  scope: Scopes[]
+  client_id: string
+  client_secret: string
+  redirect_uri?: string
+  state?: string
+  force_verify?: boolean
+}): MiddlewareHandler {
+  return async (c, next) => {
+    // Generate encoded "keys" if not provided
+    const newState = options.state || getRandomState()
+    // Create new Auth instance
+    const auth = new AuthFlow({
+      client_id: options.client_id || (env(c).TWITCH_ID as string),
+      client_secret: options.client_secret || (env(c).TWITCH_SECRET as string),
+      redirect_uri: options.redirect_uri || c.req.url.split('?')[0],
+      scope: options.scope,
+      state: newState,
+      code: c.req.query('code'),
+      force_verify: options.force_verify || false,
+    })
+
+    // Redirect to login dialog
+    if (!auth.code) {
+      setCookie(c, 'state', newState, {
+        maxAge: 60 * 10,
+        httpOnly: true,
+        path: '/',
+      })
+      return c.redirect(auth.redirect())
+    }
+
+    // Avoid CSRF attack by checking state
+    if (c.req.url.includes('?')) {
+      const storedState = getCookie(c, 'state')
+      if (c.req.query('state') !== storedState) {
+        throw new HTTPException(401)
+      }
+    }
+
+    // Retrieve user data from twitch
+    await auth.getUserData()
+
+    // Set return info
+    c.set('token', auth.token)
+    c.set('refresh-token', auth.refresh_token)
+    c.set('user-twitch', auth.user)
+    c.set('granted-scopes', auth.granted_scopes)
+
+    await next()
+  }
+}
diff --git a/packages/oauth-providers/src/providers/twitch/types.ts b/packages/oauth-providers/src/providers/twitch/types.ts
new file mode 100644
index 000000000..23009d185
--- /dev/null
+++ b/packages/oauth-providers/src/providers/twitch/types.ts
@@ -0,0 +1,168 @@
+export type Scopes =
+  // Analytics
+  | 'analytics:read:extensions'
+  | 'analytics:read:games'
+  
+  // Bits
+  | 'bits:read'
+  
+  // Channel
+  | 'channel:bot'
+  | 'channel:manage:ads'
+  | 'channel:read:ads'
+  | 'channel:manage:broadcast'
+  | 'channel:read:charity'
+  | 'channel:edit:commercial'
+  | 'channel:read:editors'
+  | 'channel:manage:extensions'
+  | 'channel:read:goals'
+  | 'channel:read:guest_star'
+  | 'channel:manage:guest_star'
+  | 'channel:read:hype_train'
+  | 'channel:manage:moderators'
+  | 'channel:read:polls'
+  | 'channel:manage:polls'
+  | 'channel:read:predictions'
+  | 'channel:manage:predictions'
+  | 'channel:manage:raids'
+  | 'channel:read:redemptions'
+  | 'channel:manage:redemptions'
+  | 'channel:manage:schedule'
+  | 'channel:read:stream_key'
+  | 'channel:read:subscriptions'
+  | 'channel:manage:videos'
+  | 'channel:read:vips'
+  | 'channel:manage:vips'
+  | 'channel:moderate'
+
+  // Clips
+  | 'clips:edit'
+
+  // User
+  | 'user:bot'
+  | 'user:edit'
+  | 'user:edit:broadcast'
+  | 'user:read:blocked_users'
+  | 'user:manage:blocked_users'
+  | 'user:read:broadcast'
+  | 'user:read:chat'
+  | 'user:manage:chat_color'
+  | 'user:read:email'
+  | 'user:read:emotes'
+  | 'user:read:follows'
+  | 'user:read:moderated_channels'
+  | 'user:read:subscriptions'
+  | 'user:read:whispers'
+  | 'user:manage:whispers'
+  | 'user:write:chat'
+
+  // Moderation
+  | 'moderation:read'
+  | 'moderator:manage:announcements'
+  | 'moderator:manage:automod'
+  | 'moderator:read:automod_settings'
+  | 'moderator:manage:automod_settings'
+  | 'moderator:read:banned_users'
+  | 'moderator:manage:banned_users'
+  | 'moderator:read:blocked_terms'
+  | 'moderator:read:chat_messages'
+  | 'moderator:manage:blocked_terms'
+  | 'moderator:manage:chat_messages'
+  | 'moderator:read:chat_settings'
+  | 'moderator:manage:chat_settings'
+  | 'moderator:read:chatters'
+  | 'moderator:read:followers'
+  | 'moderator:read:guest_star'
+  | 'moderator:manage:guest_star'
+  | 'moderator:read:moderators'
+  | 'moderator:read:shield_mode'
+  | 'moderator:manage:shield_mode'
+  | 'moderator:read:shoutouts'
+  | 'moderator:manage:shoutouts'
+  | 'moderator:read:suspicious_users'
+  | 'moderator:read:unban_requests'
+  | 'moderator:manage:unban_requests'
+  | 'moderator:read:vips'
+  | 'moderator:read:warnings'
+  | 'moderator:manage:warnings'
+    
+  // IRC Chat Scopes
+  | 'chat:edit'
+  | 'chat:read'
+  
+  // PubSub-specific Chat Scopes
+  | 'whispers:read'
+
+// Error responses types from Twitch API
+export type TwitchErrorResponse = {
+  error?: string
+  error_description?: string
+  state?: string
+  message?: string
+  status?: number
+}
+
+export type TwitchValidateError = Required<Pick<TwitchErrorResponse, 'status' | 'message'>>
+
+export type TwitchRevokingError = Required<Pick<TwitchErrorResponse, 'status' | 'message'>>
+
+export type TwitchRefreshError = Required<Pick<TwitchErrorResponse, 'status' | 'message' | 'error'>>
+
+export type TwitchTokenError = Required<Pick<TwitchErrorResponse, 'status' | 'message' | 'error'>>
+
+
+// Success responses types from Twitch API
+export interface TwitchValidateSuccess {
+  client_id: string
+  login: string
+  scopes: Scopes[]
+  user_id: string
+  expires_in: number
+}
+
+export interface TwitchRevokingSuccess {
+  status?: number
+}
+
+export interface TwitchRefreshSuccess {
+  access_token: string
+  expires_in: number
+  refresh_token: string
+  scope: Scopes[]
+  token_type: string
+}
+
+export interface TwitchTokenSuccess {
+  access_token: string
+  expires_in: number
+  refresh_token: string
+  scope: Scopes[]
+  token_type: string
+}
+
+// Combined response types
+export type TwitchRevokingResponse = TwitchRevokingSuccess | TwitchRevokingError
+
+export type TwitchRefreshResponse = TwitchRefreshSuccess | TwitchRefreshError
+
+export type TwitchTokenResponse = TwitchTokenSuccess | TwitchTokenError
+
+export type TwitchValidateResponse = TwitchValidateSuccess | TwitchValidateError
+
+export interface TwitchUserResponse {
+  data: [{
+    id: string
+    login: string
+    display_name: string
+    type: string
+    broadcaster_type: string
+    description: string
+    profile_image_url: string
+    offline_image_url: string
+    view_count: number
+    email: string
+    created_at: string
+  }]
+}
+
+export type TwitchUser = TwitchUserResponse['data'][0]
diff --git a/packages/oauth-providers/src/providers/twitch/validateToken.ts b/packages/oauth-providers/src/providers/twitch/validateToken.ts
new file mode 100644
index 000000000..8453628be
--- /dev/null
+++ b/packages/oauth-providers/src/providers/twitch/validateToken.ts
@@ -0,0 +1,20 @@
+import { HTTPException } from 'hono/http-exception'
+import type { TwitchValidateResponse } from './types'
+
+export async function validateToken(
+  token: string
+): Promise<TwitchValidateResponse> {
+
+  const response = await fetch('https://id.twitch.tv/oauth2/validate', {
+    method: 'GET',
+    headers: {
+        authorization: `Bearer ${token}`,
+    },
+  }).then((res) => res.json() as Promise<TwitchValidateResponse>)
+
+  if ('status' in response) {
+    throw new HTTPException(400, { message: response.message })
+  }
+
+  return response
+}
diff --git a/packages/oauth-providers/test/handlers.ts b/packages/oauth-providers/test/handlers.ts
index ae17f1133..bcde2b981 100644
--- a/packages/oauth-providers/test/handlers.ts
+++ b/packages/oauth-providers/test/handlers.ts
@@ -10,6 +10,7 @@ import type {
 import type { GitHubErrorResponse, GitHubTokenResponse } from '../src/providers/github'
 import type { GoogleErrorResponse, GoogleTokenResponse, GoogleUser } from '../src/providers/google'
 import type { LinkedInErrorResponse, LinkedInTokenResponse } from '../src/providers/linkedin'
+import type { TwitchErrorResponse, TwitchTokenResponse, TwitchTokenSuccess } from '../src/providers/twitch'
 import type { XErrorResponse, XRevokeResponse, XTokenResponse } from '../src/providers/x'
 
 export const handlers = [
@@ -146,6 +147,55 @@ export const handlers = [
     }
   ),
   http.get('https://discord.com/api/oauth2/@me', () => HttpResponse.json(discordUser)),
+  // Twitch
+  http.post(
+    'https://id.twitch.tv/oauth2/token',
+    async ({ request }): Promise<StrictResponse<Partial<TwitchTokenResponse> | TwitchErrorResponse>> => {
+      const params = new URLSearchParams(await request.text())
+      const code = params.get('code')
+      const grant_type = params.get('grant_type')
+      if (grant_type === 'refresh_token') {
+        const refresh_token = params.get('refresh_token')
+        if (refresh_token === 'wrong-refresh-token') {
+          return HttpResponse.json(twitchRefreshTokenError)
+        }
+        return HttpResponse.json(twitchRefreshToken)
+      }
+      if (code === dummyCode) {
+        return HttpResponse.json(twitchToken)
+      }
+      return HttpResponse.json(twitchCodeError)
+    }
+  ),
+  http.get('https://api.twitch.tv/helix/users', () => HttpResponse.json(twitchUser)),
+  http.post(
+    'https://id.twitch.tv/oauth2/revoke',
+    async ({ request }): Promise<StrictResponse<{ status: number; message?: string } | null>> => {
+      const params = new URLSearchParams(await request.text())
+      const token = params.get('token')
+      if (token === 'wrong-token') {
+        return HttpResponse.json<{ status: number; message?: string }>(twitchRevokeTokenError, { status: 400 })
+      }
+      return HttpResponse.json<null>(null, { status: 200 }) // Return 200 with empty body
+    }
+  ),
+  // Twitch validate token handler
+  http.get(
+    'https://id.twitch.tv/oauth2/validate',
+    async ({ request }): Promise<StrictResponse<typeof twitchValidateSuccess | typeof twitchValidateError>> => {
+      const authHeader = request.headers.get('authorization')
+      if (!authHeader || !authHeader.startsWith('Bearer ')) {
+        return HttpResponse.json(twitchValidateError, { status: 401 })
+      }
+      
+      const token = authHeader.split(' ')[1]
+      if (token === 'twitchr4nd0m4cc3sst0k3n') {
+        return HttpResponse.json(twitchValidateSuccess)
+      }
+      
+      return HttpResponse.json(twitchValidateError, { status: 401 })
+    }
+  ),
 ]
 
 export const dummyCode = '4/0AfJohXl9tS46EmTA6u9x3pJQiyCNyahx4DLJaeJelzJ0E5KkT4qJmCtjq9n3FxBvO40ofg'
@@ -433,3 +483,67 @@ export const discordRefreshToken = {
 export const discordRefreshTokenError = {
   error: 'Invalid Refresh Token.',
 }
+
+export const twitchToken: TwitchTokenSuccess = {
+  access_token: 'twitchr4nd0m4cc3sst0k3n',
+  expires_in: 14400,
+  refresh_token: 'twitchr4nd0mr3fr3sht0k3n',
+  scope: ['user:read:email', 'channel:read:subscriptions', 'bits:read'],
+  token_type: 'bearer',
+}
+
+export const twitchRefreshToken: TwitchTokenResponse = {
+  access_token: 'twitchn3w4cc3sst0k3n',
+  expires_in: 14400,
+  refresh_token: 'twitchn3wr3fr3sht0k3n',
+  scope: ['user:read:email', 'channel:read:subscriptions', 'bits:read'],
+  token_type: 'bearer',
+}
+
+export const twitchUser = {
+  data: [
+    {
+      id: '12345678',
+      login: 'younis',
+      display_name: 'younis.name',
+      type: '',
+      broadcaster_type: 'partner',
+      description: 'Supporting third-party developers building Twitch integrations',
+      profile_image_url: 'https://static-cdn.jtvnw.net/jtv_user_pictures/example-profile-picture.png',
+      offline_image_url: 'https://static-cdn.jtvnw.net/jtv_user_pictures/example-offline-image.png',
+      view_count: 5980557,
+      email: 'example@twitch.tv',
+      created_at: '2025-02-14T20:32:28Z',
+    },
+  ],
+}
+
+export const twitchCodeError = {
+  error: 'access_denied',
+  error_description: 'Invalid authorization code',
+  state: 'c3ab8aa609ea11e793ae92361f002671',
+}
+
+export const twitchRefreshTokenError = {
+  error: 'Bad Request',
+  status: 400,
+  message: 'Invalid refresh token',
+}
+
+export const twitchRevokeTokenError = {
+  status: 400,
+  message: 'Token revocation failed with status: 400',
+}
+
+export const twitchValidateSuccess = {
+  client_id: 'wbmytr93xzw8zbg0p1izqyzzc5mbiz',
+  login: 'younis',
+  scopes: ['user:read:email', 'channel:read:subscriptions', 'bits:read'],
+  user_id: '12345678',
+  expires_in: 14400
+}
+
+export const twitchValidateError = {
+  status: 401,
+  message: 'invalid access token'
+}
diff --git a/packages/oauth-providers/test/index.test.ts b/packages/oauth-providers/test/index.test.ts
index d8f58dbec..88da93913 100644
--- a/packages/oauth-providers/test/index.test.ts
+++ b/packages/oauth-providers/test/index.test.ts
@@ -14,6 +14,13 @@ import { googleAuth } from '../src/providers/google'
 import type { GoogleUser } from '../src/providers/google'
 import { linkedinAuth } from '../src/providers/linkedin'
 import type { LinkedInUser } from '../src/providers/linkedin'
+import type { TwitchUser } from '../src/providers/twitch'
+import { 
+  twitchAuth, 
+  refreshToken as twitchRefresh, 
+  revokeToken as twitchRevoke,
+  validateToken as twitchValidate 
+} from '../src/providers/twitch'
 import type { XUser } from '../src/providers/x'
 import { refreshToken, revokeToken, xAuth } from '../src/providers/x'
 import type { Token } from '../src/types'
@@ -42,6 +49,14 @@ import {
   xRevokeTokenError,
   xToken,
   xUser,
+  twitchCodeError,
+  twitchRefreshToken,
+  twitchRefreshTokenError,
+  twitchRevokeTokenError,
+  twitchToken,
+  twitchUser,
+  twitchValidateSuccess,
+  twitchValidateError,
 } from './handlers'
 
 const server = setupServer(...handlers)
@@ -340,6 +355,76 @@ describe('OAuth Middleware', () => {
     return c.json(response)
   })
 
+  // Twitch
+  app.use(
+    '/twitch',
+    twitchAuth({
+      client_id,
+      client_secret,
+      scope: ['user:read:email', 'channel:read:subscriptions', 'bits:read'],
+      redirect_uri: 'http://localhost:3000/twitch', // Redirect URI
+    })
+  )
+  app.use('/twitch-custom-redirect', (c, next) =>
+    twitchAuth({
+      client_id,
+      client_secret,
+      scope: ['user:read:email'],
+      redirect_uri: 'http://localhost:3000/twitch',
+    })(c, next)
+  )
+  app.use('/twitch-force-verify', (c, next) =>
+    twitchAuth({
+      client_id,
+      client_secret,
+      scope: ['user:read:email'],
+      redirect_uri: 'http://localhost:3000/twitch',
+      force_verify: true,
+    })(c, next)
+  )
+  app.use('/twitch-custom-state', (c, next) =>
+    twitchAuth({
+      client_id,
+      client_secret,
+      scope: ['user:read:email'],
+      redirect_uri: 'http://localhost:3000/twitch',
+      state: 'test-state',
+    })(c, next)
+  )
+  app.get('/twitch', (c) => {
+    const token = c.get('token')
+    const refreshToken = c.get('refresh-token')
+    const user = c.get('user-twitch')
+    const grantedScopes = c.get('granted-scopes')
+
+    return c.json({
+      token,
+      refreshToken,
+      grantedScopes,
+      user,
+    })
+  })
+  app.get('/twitch/refresh', async (c) => {
+    const response = await twitchRefresh(
+      client_id,
+      client_secret,
+      'twitchr4nd0mr3fr3sht0k3n'
+    )
+    return c.json(response)
+  })
+  app.get('/twitch/refresh/error', async (c) => {
+    const response = await twitchRefresh(client_id, client_secret, 'wrong-refresh-token')
+    return c.json(response)
+  })
+  app.get('/twitch/revoke', async (c) => {
+    const response = await twitchRevoke(client_id, 'twitchr4nd0m4cc3sst0k3n')
+    return c.json(response)
+  })
+  app.get('/twitch/revoke/error', async (c) => {
+    const response = await twitchRevoke(client_id, 'wrong-token')
+    return c.json(response)
+  })
+
   beforeAll(() => {
     server.listen()
   })
@@ -771,4 +856,125 @@ describe('OAuth Middleware', () => {
       })
     })
   })
+
+  describe('twitchAuth middleware', () => {
+      it('Should work with custom state', async () => {
+        const res = await app.request('/twitch-custom-state')
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(302)
+        const redirectLocation = res.headers.get('location')!
+        const redirectUrl = new URL(redirectLocation)
+        expect(redirectUrl.searchParams.get('state')).toBe('test-state')
+      })
+  })
+
+  describe('twitchAuth middleware', () => {
+    describe('middleware', () => {
+      it('Should redirect', async () => {
+        const res = await app.request('/twitch')
+
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(302)
+      })
+
+      it('Should redirect to custom redirect_uri', async () => {
+        const res = await app.request('/twitch-custom-redirect')
+        expect(res?.status).toBe(302)
+        const redirectLocation = res.headers.get('location')!
+        const redirectUrl = new URL(redirectLocation)
+        expect(redirectUrl.searchParams.get('redirect_uri')).toBe('http://localhost:3000/twitch')
+      })
+
+      it('Should include force_verify when enabled', async () => {
+        const res = await app.request('/twitch-force-verify')
+        expect(res?.status).toBe(302)
+        const redirectLocation = res.headers.get('location')!
+        const redirectUrl = new URL(redirectLocation)
+        expect(redirectUrl.searchParams.get('force_verify')).toBe('true')
+      })
+
+      it('Prevent CSRF attack', async () => {
+        const res = await app.request(`/twitch?code=${dummyCode}&state=malware-state`)
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(401)
+      })
+
+      it('Should throw error for invalid code', async () => {
+        const res = await app.request('/twitch?code=9348ffdsd-sdsdbad-code')
+
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(400)
+        expect(await res.text()).toBe(twitchCodeError.error)
+      })
+
+      it('Should work with received code', async () => {
+        const res = await app.request(`/twitch?code=${dummyCode}`)
+        const response = (await res.json()) as {
+          token: Token
+          refreshToken: Token
+          user: TwitchUser
+          grantedScopes: string[]
+        }
+
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(200)
+        expect(response.user).toEqual(twitchUser.data[0])
+        expect(response.grantedScopes).toEqual(twitchToken.scope)
+        expect(response.token).toEqual({
+          token: twitchToken.access_token,
+          expires_in: twitchToken.expires_in,
+        })
+        expect(response.refreshToken).toEqual({
+          token: twitchToken.refresh_token,
+          expires_in: 0,
+        })
+      })
+    })
+
+    describe('Refresh Token', () => {
+      it('Should refresh token', async () => {
+        const res = await app.request('/twitch/refresh')
+
+        expect(res).not.toBeNull()
+        expect(await res.json()).toEqual(twitchRefreshToken)
+      })
+
+      it('Should return error for refresh', async () => {
+        const res = await app.request('/twitch/refresh/error')
+
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(400)
+        expect(await res.text()).toBe(twitchRefreshTokenError.error)
+      })
+    })
+
+    describe('Revoke Token', () => {
+      it('Should revoke token', async () => {
+        const res = await app.request('/twitch/revoke')
+
+        expect(res).not.toBeNull()
+        expect(await res.json()).toEqual(true)
+      })
+
+      it('Should return error for revoke', async () => {
+        const res = await app.request('/twitch/revoke/error')
+
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(400)
+        expect(await res.text()).toBe(twitchRevokeTokenError.message)
+      })
+    })
+
+    describe('Validate Token', () => {
+      it('Should validate a valid token', async () => {
+        const res = await twitchValidate('twitchr4nd0m4cc3sst0k3n')
+        expect(res).toEqual(twitchValidateSuccess)
+      })
+    
+      it('Should throw error for invalid token', async () => {
+        const res = twitchValidate('invalid-token')
+        await expect(res).rejects.toThrow(twitchValidateError.message)
+      })
+    })
+  })
 })


Reviews:
user: login: BarryThePenguin
body: Looks good to me 👍🏻 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T23:39:38Z
user: login: BarryThePenguin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T00:29:32Z
user: login: Younis-Ahmed
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T19:32:33Z
user: login: Younis-Ahmed
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T19:35:09Z
user: login: yusukebe
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T04:10:20Z
user: login: Younis-Ahmed
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:58:36Z
user: login: yusukebe
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T09:59:35Z
user: login: yusukebe
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T08:45:07Z
user: login: Younis-Ahmed
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:46:46Z
user: login: yusukebe
body: LGTM!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-12T08:55:07Z

Review comments:
diff_hunk: @@ -918,6 +918,227 @@ app.post('/remove-user', async (c, next) => {
 })
 ```
 
+### Twitch
+
+```ts
+import { Hono } from 'hono'
+import { twitchAuth } from '@hono/oauth-providers/twitch'
+
+const app = new Hono()
+
+app.use(
+  '/twitch',
+  twitchAuth({
+    client_id: Bun.env.TWITCH_ID,
+    client_secret: Bun.env.TWITCH_SECRET,
+    scope: ['user:read:email', 'channel:read:subscriptions', 'bits:read'],
+    redirect_uri: 'http://localhost:3000/twitch',
+  })
+)
+
+export default app
+```
+
+#### Parameters
+
+- `client_id`:
+  - Type: `string`.
+  - `Required`.
+  - Your app client ID. You can find this value in the [Twitch Developer Portal](https://dev.twitch.tv/console/apps). <br />When developing **Cloudflare Workers**, there's no need to send this parameter. Just declare it in the `wrangler.toml` file as `TWITCH_ID=`.
+- `client_secret`:
+  - Type: `string`.
+  - `Required`.
+  - Your app client secret. You can find this value in the [Twitch Developer Portal](https://dev.twitch.tv/console/apps). <br />When developing **Cloudflare Workers**, there's no need to send this parameter. Just declare it in the `wrangler.toml` file as `TWITCH_SECRET=`.
+    > ⚠️ Do **not** share your **client secret** to ensure the security of your app.
+- `scope`:
+  - Type: `string[]`.
+  - `Required`.
+  - Set of **permissions** to request the user's authorization to access your app for retrieving user information and performing actions on their behalf.<br /> Review all the scopes Twitch offers for utilizing their API on the [Twitch API Reference](https://dev.twitch.tv/docs/authentication/scopes).
+- `redirect_uri`:
+  - Type: `string`.
+  - `Required`.
+  - The URI to which the user will be redirected after authentication.
+- `state`:
+  - Type: `string`.
+  - `Optional`.
+  - A unique string to protect against Cross-Site Request Forgery (CSRF) attacks. The state is passed back to your redirect URI after the user has authenticated. You should verify that the state matches the one you provided in the initial request.
+- `force_verify`:
+  - Type: `boolean`.
+  - `Optional`.
+  - Set this value to `true` if you want to force the user to verify their account. Defaults to `false`.
+
+#### Authentication Flow
+
+After the completion of the Twitch OAuth flow, essential data has been prepared for use in the subsequent steps that your app needs to take.
+
+`twitchAuth` method provides 4 set key data:
+
+- `token`:
+  - Access token to make requests to the Twitch API for retrieving user information and performing actions on their behalf.
+  - Type:
+    ```
+    {
+      token: string
+      expires_in: number
+    }
+    ```
+- `refresh-token`:
+  - You can refresh new tokens using this token. The duration of this token is not specified on the Twitch docs.
+  - Type:
+    ```
+    {
+      token: string
+      expires_in: number
+    }
+    ```
+- `granted-scopes`:
+  - Scopes for which the user has granted permissions.
+  - Type: `string[]`.
+- `user-twitch`:
+  - User basic info retrieved from Twitch
+  - Type:
+    ```
+    {
+      id: string
+      login: string
+      display_name: string
+      type: string
+      broadcaster_type: string
+      description: string
+      profile_image_url: string
+      offline_image_url: string
+      view_count: number
+      email: string
+      created_at: string
+    }
+    ```
+
+> [!NOTE]
+> To access this data, utilize the `c.get` method within the callback of the upcoming HTTP request handler.
+
+```ts
+app.get('/twitch', (c) => {
+  const token = c.get('token')
+  const refreshToken = c.get('refresh-token')
+  const grantedScopes = c.get('granted-scopes')
+  const user = c.get('user-twitch')
+
+  return c.json({
+    token,
+    refreshToken,
+    grantedScopes,
+    user,
+  })
+})
+```
+
+#### Refresh Token
+
+Once the user token expires you can refresh their token without the need to prompt the user again for access. In such scenario, you can utilize the `refreshToken` method, which accepts the `client_id`, `client_secret` and `refresh_token` as parameters.
+
+> [!NOTE]
+> The `refresh_token` can be used once. Once the token is refreshed Twitch gives you a new `refresh_token` along with the new token.
+
+```ts
+import { twitchAuth, refreshToken } from '@hono/oauth-providers/twitch'
+
+app.post('/twitch/refresh', async (c, next) => {
+  const newTokens = await refreshToken(CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN)
+
+  // newTokens = {
+  //   token_type: 'bearer',
+  //   access_token: 'new-access-token',
+  //   expires_in: 60000,
+  //   refresh_token: 'new-refresh-token',
+  //   scope: ['user:read:email', 'channel:read:subscriptions', 'bits:read']
+  // }
+  // ...
+})
+```
+
+#### Revoke Token
+
+In certain use cases, you may need to programmatically revoke a user's access token. In such scenarios, you can utilize the `revokeToken` method, the `client_id` and the `token` to be revoked as parameters.
+
+It returns a `boolean` to tell whether the token was revoked or not.
+
+```ts
+import { twitchAuth, revokeToken } from '@hono/oauth-providers/twitch'
+
+app.post('/remove-user', async (c, next) => {
+  const revoked = await revokeToken(CLIENT_ID, USER_TOKEN)
+
+  // revoked = true | false
+  // ...
+})
+```
+
+#### Validate Token
+
+You can validate a Twitch access token to verify it's still valid or to obtain information about the token, such as its expiration date, scopes, and the associated user.
+
+> **IMPORTANT:** Twitch requires applications to validate OAuth tokens when they start and on an hourly basis thereafter. Failure to validate tokens may result in Twitch taking punitive action, such as revoking API keys or throttling performance. When a token becomes invalid, your app should terminate all sessions using that token immediately. [Read more](https://dev.twitch.tv/docs/authentication/validate-tokens)
+
+The validation endpoint helps your application detect when tokens become invalid for reasons other than expiration, such as when users disconnect your integration from their Twitch account. When a token becomes invalid, your app should terminate all sessions using that token.
+
+```ts
+import { twitchAuth, validateToken } from '@hono/oauth-providers/twitch'
+
+// Function to validate token - should be called on app startup and hourly thereafter
+async function performTokenValidation(token) {
+  try {
+    // Returns token information if valid
+    const tokenInfo = await validateToken(token)
+    
+    // tokenInfo = {
+    //   client_id: "wbmytr93xzw8zbg0p1izqyzzc5mbiz",
+    //   login: "twitchdev",
+    //   scopes: ["channel:read:subscriptions"],
+    //   user_id: "141981764",
+    //   expires_in: 5520838
+    // }
+    
+    c.logger.log("Token is valid. Next check scheduled in 1 hour")
+    return tokenInfo
+  } catch (error) {
+    // Token is invalid - should terminate user session
+    c.logger.error("Token validation failed:", error.message)
+    return null
+  }
+}
+
+// Example implementation of hourly validation
+app.get('/setup-token-validation', async (c) => {
+  const userToken = c.get('token') // Get the user's token from your auth system
+  
+  // Initial validation when app starts
+  const initialValidation = await performTokenValidation(userToken)
+  if (!initialValidation) {
+    // Token is invalid, terminate session
+    return c.json({ valid: false, message: "Invalid token. Please re-authenticate." })
+  }
+  
+  // Set up hourly validation as required by Twitch
+  setInterval(() => {
+    performTokenValidation(userToken)
+      .then(tokenInfo => {
+        if (!tokenInfo) {
+          // Token became invalid, application should terminate user session
+          // and stop making API calls with this token
+          c.logger.log("Token validation failed in scheduled check")
+        }
+      })
+  }, 60 * 60 * 1000) // Check every hour (60 minutes * 60 seconds * 1000 milliseconds)
+  
+  return c.json({ 
+    valid: true, 
+    message: "Token validation schedule established",
+    info: initialValidation
+  })
+})
+```
+> For security and compliance, make sure to implement regular token validation in your application. If a token becomes invalid, promptly sign out the user and terminate their OAuth session.
+
path: packages/oauth-providers/README.md
user: login: BarryThePenguin
body: I don't think these code examples are very useful, just the text description above should be enough.

Performing token validation will vary a lot based on the platform being used.

On Cloudflare Workers for example, using `setInterval()` over the course of hours will not work. You would need to use [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/) to validate all user tokens every hour

I'd keep the text description, but delete the code examples
created_at: 2025-03-04T23:05:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,134 @@
+import { HTTPException } from 'hono/http-exception'
+
+import type { Token } from '../../types'
+import { toQueryParams } from '../../utils/objectToQuery'
+import type {
+  TwitchErrorResponse,
+  TwitchUserResponse,
+  TwitchTokenResponse,
+  TwitchUser,
+  Scopes,
+} from './types'
+
+type TwitchAuthFlow = {
+  client_id: string
+  client_secret: string
+  redirect_uri: string
+  scope: Scopes[]
+  state: string
+  code: string | undefined
+  force_verify: boolean | undefined
+  // token: Token | undefined
path: packages/oauth-providers/src/providers/twitch/authFlow.ts
user: login: BarryThePenguin
body: ```suggestion
```
created_at: 2025-03-04T23:34:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,134 @@
+import { HTTPException } from 'hono/http-exception'
+
+import type { Token } from '../../types'
+import { toQueryParams } from '../../utils/objectToQuery'
+import type {
+  TwitchErrorResponse,
+  TwitchUserResponse,
+  TwitchTokenResponse,
+  TwitchUser,
+  Scopes,
+} from './types'
+
+type TwitchAuthFlow = {
+  client_id: string
+  client_secret: string
+  redirect_uri: string
+  scope: Scopes[]
+  state: string
+  code: string | undefined
+  force_verify: boolean | undefined
+  // token: Token | undefined
+}
+
+export class AuthFlow {
+  client_id: string
+  client_secret: string
+  redirect_uri: string
+  scope: string
+  state: string
+  code: string | undefined
+  token: Token | undefined
+  refresh_token: Token | undefined
+  granted_scopes: string[] | undefined
+  user: Partial<TwitchUser> | undefined
+  force_verify: boolean | undefined
+
+  constructor({
+    client_id,
+    client_secret,
+    redirect_uri,
+    scope,
+    state,
+    code,
+    force_verify,
+    // token,
path: packages/oauth-providers/src/providers/twitch/authFlow.ts
user: login: BarryThePenguin
body: ```suggestion
```
created_at: 2025-03-04T23:34:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,134 @@
+import { HTTPException } from 'hono/http-exception'
+
+import type { Token } from '../../types'
+import { toQueryParams } from '../../utils/objectToQuery'
+import type {
+  TwitchErrorResponse,
+  TwitchUserResponse,
+  TwitchTokenResponse,
+  TwitchUser,
+  Scopes,
+} from './types'
+
+type TwitchAuthFlow = {
+  client_id: string
+  client_secret: string
+  redirect_uri: string
+  scope: Scopes[]
+  state: string
+  code: string | undefined
+  force_verify: boolean | undefined
+  // token: Token | undefined
+}
+
+export class AuthFlow {
+  client_id: string
+  client_secret: string
+  redirect_uri: string
+  scope: string
+  state: string
+  code: string | undefined
+  token: Token | undefined
+  refresh_token: Token | undefined
+  granted_scopes: string[] | undefined
+  user: Partial<TwitchUser> | undefined
+  force_verify: boolean | undefined
+
+  constructor({
+    client_id,
+    client_secret,
+    redirect_uri,
+    scope,
+    state,
+    code,
+    force_verify,
+    // token,
+  }: TwitchAuthFlow) {
+    this.client_id = client_id
+    this.client_secret = client_secret
+    this.redirect_uri = redirect_uri
+    this.scope = scope.join(' ')
+    this.state = state
+    this.code = code
+    this.refresh_token = undefined
+    this.force_verify = force_verify
+    // this.token = token
path: packages/oauth-providers/src/providers/twitch/authFlow.ts
user: login: BarryThePenguin
body: ```suggestion
```
created_at: 2025-03-04T23:34:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -771,4 +856,137 @@ describe('OAuth Middleware', () => {
       })
     })
   })
+
+  describe('twitchAuth middleware', () => {
+    describe('middleware', () => {
+      it('Should work with custom state', async () => {
+        const res = await app.request('/twitch-custom-state')
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(302)
+        const redirectLocation = res.headers.get('location')!
+        const redirectUrl = new URL(redirectLocation)
+        expect(redirectUrl.searchParams.get('state')).toBe('test-state')
+      })
+    })
+  })
+
+  describe('twitchAuth middleware', () => {
+    describe('middleware', () => {
+      it('Should redirect', async () => {
+        const res = await app.request('/twitch')
+
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(302)
+      })
+
+      it('Should redirect to custom redirect_uri', async () => {
+        const res = await app.request('/twitch-custom-redirect')
+        expect(res?.status).toBe(302)
+        const redirectLocation = res.headers.get('location')!
+        const redirectUrl = new URL(redirectLocation)
+        expect(redirectUrl.searchParams.get('redirect_uri')).toBe('http://localhost:3000/twitch')
+      })
+
+      it('Should include force_verify when enabled', async () => {
+        const res = await app.request('/twitch-force-verify')
+        expect(res?.status).toBe(302)
+        const redirectLocation = res.headers.get('location')!
+        const redirectUrl = new URL(redirectLocation)
+        expect(redirectUrl.searchParams.get('force_verify')).toBe('true')
+      })
+
+      it('Prevent CSRF attack', async () => {
+        const res = await app.request(`/twitch?code=${dummyCode}&state=malware-state`)
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(401)
+      })
+
+      it('Should throw error for invalid code', async () => {
+        const res = await app.request('/twitch?code=9348ffdsd-sdsdbad-code')
+
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(400)
+        expect(await res.text()).toBe(twitchCodeError.error)
+      })
+
+      it('Should work with received code', async () => {
+        const res = await app.request(`/twitch?code=${dummyCode}`)
+        const response = (await res.json()) as {
+          token: Token
+          refreshToken: Token
+          user: TwitchUser
+          grantedScopes: string[]
+        }
+
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(200)
+        expect(response.user).toEqual(twitchUser.data[0])
+        expect(response.grantedScopes).toEqual(twitchToken.scope)
+        expect(response.token).toEqual({
+          token: twitchToken.access_token,
+          expires_in: twitchToken.expires_in,
+        })
+        expect(response.refreshToken).toEqual({
+          token: twitchToken.refresh_token,
+          expires_in: 0,
+        })
+      })
+    })
+
+    describe('Refresh Token', () => {
+      it('Should refresh token', async () => {
+        const res = await app.request('/twitch/refresh')
+
+        expect(res).not.toBeNull()
+        expect(await res.json()).toEqual(twitchRefreshToken)
+      })
+
+      it('Should return error for refresh', async () => {
+        const res = await app.request('/twitch/refresh/error')
+
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(400)
+        expect(await res.text()).toBe(twitchRefreshTokenError.error)
+      })
+    })
+
+    describe('Revoke Token', () => {
+      it('Should revoke token', async () => {
+        const res = await app.request('/twitch/revoke')
+
+        expect(res).not.toBeNull()
+        expect(await res.json()).toEqual(true)
+      })
+
+      it('Should return error for revoke', async () => {
+        const res = await app.request('/twitch/revoke/error')
+
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(400)
+        expect(await res.text()).toBe(twitchRevokeTokenError.message)
+      })
+    })
+
+    describe('Validate Token', () => {
+      it('Should validate a valid token', async () => {
+          const res = await twitchValidate('twitchr4nd0m4cc3sst0k3n')
+          expect(res).toEqual(twitchValidateSuccess)
+      })
+    
+      it('Should throw error for invalid token', async () => {
+          try {
+              await twitchValidate('invalid-token')
+              // If we reach here, the test should fail because an exception was expected
+              fail('Expected function to throw an exception')
+          } catch (error: unknown) {
+              // Type check before accessing properties
+              if (error instanceof Error) {
+                  expect(error.message).toBe(twitchValidateError.message)
+              } else {
+                  fail('Expected error to be an instance of Error')
+              }
+          }
path: packages/oauth-providers/test/index.test.ts
user: login: BarryThePenguin
body: ```suggestion
        await expect(twitchValidate('invalid-token')).rejects.toThrow(twitchValidateError.message)
```
created_at: 2025-03-04T23:39:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -771,4 +856,137 @@ describe('OAuth Middleware', () => {
       })
     })
   })
+
+  describe('twitchAuth middleware', () => {
+    describe('middleware', () => {
+      it('Should work with custom state', async () => {
+        const res = await app.request('/twitch-custom-state')
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(302)
+        const redirectLocation = res.headers.get('location')!
+        const redirectUrl = new URL(redirectLocation)
+        expect(redirectUrl.searchParams.get('state')).toBe('test-state')
+      })
+    })
+  })
+
+  describe('twitchAuth middleware', () => {
+    describe('middleware', () => {
path: packages/oauth-providers/test/index.test.ts
user: login: BarryThePenguin
body: You've used the same `describe()` blocks twice here 👀 
created_at: 2025-03-05T00:29:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -771,4 +856,137 @@ describe('OAuth Middleware', () => {
       })
     })
   })
+
+  describe('twitchAuth middleware', () => {
+    describe('middleware', () => {
+      it('Should work with custom state', async () => {
+        const res = await app.request('/twitch-custom-state')
+        expect(res).not.toBeNull()
+        expect(res.status).toBe(302)
+        const redirectLocation = res.headers.get('location')!
+        const redirectUrl = new URL(redirectLocation)
+        expect(redirectUrl.searchParams.get('state')).toBe('test-state')
+      })
+    })
+  })
+
+  describe('twitchAuth middleware', () => {
+    describe('middleware', () => {
path: packages/oauth-providers/test/index.test.ts
user: login: Younis-Ahmed
body: yikes! Thank you 🦸‍♂️
created_at: 2025-03-05T19:32:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -918,6 +918,227 @@ app.post('/remove-user', async (c, next) => {
 })
 ```
 
+### Twitch
+
+```ts
+import { Hono } from 'hono'
+import { twitchAuth } from '@hono/oauth-providers/twitch'
+
+const app = new Hono()
+
+app.use(
+  '/twitch',
+  twitchAuth({
+    client_id: Bun.env.TWITCH_ID,
+    client_secret: Bun.env.TWITCH_SECRET,
+    scope: ['user:read:email', 'channel:read:subscriptions', 'bits:read'],
+    redirect_uri: 'http://localhost:3000/twitch',
+  })
+)
+
+export default app
+```
+
+#### Parameters
+
+- `client_id`:
+  - Type: `string`.
+  - `Required`.
+  - Your app client ID. You can find this value in the [Twitch Developer Portal](https://dev.twitch.tv/console/apps). <br />When developing **Cloudflare Workers**, there's no need to send this parameter. Just declare it in the `wrangler.toml` file as `TWITCH_ID=`.
+- `client_secret`:
+  - Type: `string`.
+  - `Required`.
+  - Your app client secret. You can find this value in the [Twitch Developer Portal](https://dev.twitch.tv/console/apps). <br />When developing **Cloudflare Workers**, there's no need to send this parameter. Just declare it in the `wrangler.toml` file as `TWITCH_SECRET=`.
+    > ⚠️ Do **not** share your **client secret** to ensure the security of your app.
+- `scope`:
+  - Type: `string[]`.
+  - `Required`.
+  - Set of **permissions** to request the user's authorization to access your app for retrieving user information and performing actions on their behalf.<br /> Review all the scopes Twitch offers for utilizing their API on the [Twitch API Reference](https://dev.twitch.tv/docs/authentication/scopes).
+- `redirect_uri`:
+  - Type: `string`.
+  - `Required`.
+  - The URI to which the user will be redirected after authentication.
+- `state`:
+  - Type: `string`.
+  - `Optional`.
+  - A unique string to protect against Cross-Site Request Forgery (CSRF) attacks. The state is passed back to your redirect URI after the user has authenticated. You should verify that the state matches the one you provided in the initial request.
+- `force_verify`:
+  - Type: `boolean`.
+  - `Optional`.
+  - Set this value to `true` if you want to force the user to verify their account. Defaults to `false`.
+
+#### Authentication Flow
+
+After the completion of the Twitch OAuth flow, essential data has been prepared for use in the subsequent steps that your app needs to take.
+
+`twitchAuth` method provides 4 set key data:
+
+- `token`:
+  - Access token to make requests to the Twitch API for retrieving user information and performing actions on their behalf.
+  - Type:
+    ```
+    {
+      token: string
+      expires_in: number
+    }
+    ```
+- `refresh-token`:
+  - You can refresh new tokens using this token. The duration of this token is not specified on the Twitch docs.
+  - Type:
+    ```
+    {
+      token: string
+      expires_in: number
+    }
+    ```
+- `granted-scopes`:
+  - Scopes for which the user has granted permissions.
+  - Type: `string[]`.
+- `user-twitch`:
+  - User basic info retrieved from Twitch
+  - Type:
+    ```
+    {
+      id: string
+      login: string
+      display_name: string
+      type: string
+      broadcaster_type: string
+      description: string
+      profile_image_url: string
+      offline_image_url: string
+      view_count: number
+      email: string
+      created_at: string
+    }
+    ```
+
+> [!NOTE]
+> To access this data, utilize the `c.get` method within the callback of the upcoming HTTP request handler.
+
+```ts
+app.get('/twitch', (c) => {
+  const token = c.get('token')
+  const refreshToken = c.get('refresh-token')
+  const grantedScopes = c.get('granted-scopes')
+  const user = c.get('user-twitch')
+
+  return c.json({
+    token,
+    refreshToken,
+    grantedScopes,
+    user,
+  })
+})
+```
+
+#### Refresh Token
+
+Once the user token expires you can refresh their token without the need to prompt the user again for access. In such scenario, you can utilize the `refreshToken` method, which accepts the `client_id`, `client_secret` and `refresh_token` as parameters.
+
+> [!NOTE]
+> The `refresh_token` can be used once. Once the token is refreshed Twitch gives you a new `refresh_token` along with the new token.
+
+```ts
+import { twitchAuth, refreshToken } from '@hono/oauth-providers/twitch'
+
+app.post('/twitch/refresh', async (c, next) => {
+  const newTokens = await refreshToken(CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN)
+
+  // newTokens = {
+  //   token_type: 'bearer',
+  //   access_token: 'new-access-token',
+  //   expires_in: 60000,
+  //   refresh_token: 'new-refresh-token',
+  //   scope: ['user:read:email', 'channel:read:subscriptions', 'bits:read']
+  // }
+  // ...
+})
+```
+
+#### Revoke Token
+
+In certain use cases, you may need to programmatically revoke a user's access token. In such scenarios, you can utilize the `revokeToken` method, the `client_id` and the `token` to be revoked as parameters.
+
+It returns a `boolean` to tell whether the token was revoked or not.
+
+```ts
+import { twitchAuth, revokeToken } from '@hono/oauth-providers/twitch'
+
+app.post('/remove-user', async (c, next) => {
+  const revoked = await revokeToken(CLIENT_ID, USER_TOKEN)
+
+  // revoked = true | false
+  // ...
+})
+```
+
+#### Validate Token
+
+You can validate a Twitch access token to verify it's still valid or to obtain information about the token, such as its expiration date, scopes, and the associated user.
+
+> **IMPORTANT:** Twitch requires applications to validate OAuth tokens when they start and on an hourly basis thereafter. Failure to validate tokens may result in Twitch taking punitive action, such as revoking API keys or throttling performance. When a token becomes invalid, your app should terminate all sessions using that token immediately. [Read more](https://dev.twitch.tv/docs/authentication/validate-tokens)
+
+The validation endpoint helps your application detect when tokens become invalid for reasons other than expiration, such as when users disconnect your integration from their Twitch account. When a token becomes invalid, your app should terminate all sessions using that token.
+
+```ts
+import { twitchAuth, validateToken } from '@hono/oauth-providers/twitch'
+
+// Function to validate token - should be called on app startup and hourly thereafter
+async function performTokenValidation(token) {
+  try {
+    // Returns token information if valid
+    const tokenInfo = await validateToken(token)
+    
+    // tokenInfo = {
+    //   client_id: "wbmytr93xzw8zbg0p1izqyzzc5mbiz",
+    //   login: "twitchdev",
+    //   scopes: ["channel:read:subscriptions"],
+    //   user_id: "141981764",
+    //   expires_in: 5520838
+    // }
+    
+    c.logger.log("Token is valid. Next check scheduled in 1 hour")
+    return tokenInfo
+  } catch (error) {
+    // Token is invalid - should terminate user session
+    c.logger.error("Token validation failed:", error.message)
+    return null
+  }
+}
+
+// Example implementation of hourly validation
+app.get('/setup-token-validation', async (c) => {
+  const userToken = c.get('token') // Get the user's token from your auth system
+  
+  // Initial validation when app starts
+  const initialValidation = await performTokenValidation(userToken)
+  if (!initialValidation) {
+    // Token is invalid, terminate session
+    return c.json({ valid: false, message: "Invalid token. Please re-authenticate." })
+  }
+  
+  // Set up hourly validation as required by Twitch
+  setInterval(() => {
+    performTokenValidation(userToken)
+      .then(tokenInfo => {
+        if (!tokenInfo) {
+          // Token became invalid, application should terminate user session
+          // and stop making API calls with this token
+          c.logger.log("Token validation failed in scheduled check")
+        }
+      })
+  }, 60 * 60 * 1000) // Check every hour (60 minutes * 60 seconds * 1000 milliseconds)
+  
+  return c.json({ 
+    valid: true, 
+    message: "Token validation schedule established",
+    info: initialValidation
+  })
+})
+```
+> For security and compliance, make sure to implement regular token validation in your application. If a token becomes invalid, promptly sign out the user and terminate their OAuth session.
+
path: packages/oauth-providers/README.md
user: login: Younis-Ahmed
body: Noted, I'll just leave the text desc then. 👌
created_at: 2025-03-05T19:35:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,17 @@
+---
+'@hono/oauth-providers': patch
path: .changeset/slow-days-knock.md
user: login: yusukebe
body: You should replace `patch` with `minor` since this PR introduces new features.
created_at: 2025-03-07T04:10:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,17 @@
+---
+'@hono/oauth-providers': patch
path: .changeset/slow-days-knock.md
user: login: Younis-Ahmed
body: Done 👌
created_at: 2025-03-07T11:58:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,17 @@
+---
+'@hono/oauth-providers': patch
path: .changeset/slow-days-knock.md
user: login: yusukebe
body: Ty!
created_at: 2025-03-09T09:59:35Z
author_association: MEMBER
diff_hunk: @@ -918,6 +918,226 @@ app.post('/remove-user', async (c, next) => {
 })
 ```
 
+### Twitch
+
+```ts
+import { Hono } from 'hono'
+import { twitchAuth } from '@hono/oauth-providers/twitch'
+
+const app = new Hono()
+
+app.use(
+  '/twitch',
+  twitchAuth({
+    client_id: Bun.env.TWITCH_ID,
+    client_secret: Bun.env.TWITCH_SECRET,
+    scope: ['user:read:email', 'channel:read:subscriptions', 'bits:read'],
+    redirect_uri: 'http://localhost:3000/twitch',
+  })
+)
+
+export default app
+```
+
+#### Parameters
+
+- `client_id`:
+  - Type: `string`.
+  - `Required`.
+  - Your app client ID. You can find this value in the [Twitch Developer Portal](https://dev.twitch.tv/console/apps). <br />When developing **Cloudflare Workers**, there's no need to send this parameter. Just declare it in the `wrangler.toml` file as `TWITCH_ID=`.
+- `client_secret`:
+  - Type: `string`.
+  - `Required`.
+  - Your app client secret. You can find this value in the [Twitch Developer Portal](https://dev.twitch.tv/console/apps). <br />When developing **Cloudflare Workers**, there's no need to send this parameter. Just declare it in the `wrangler.toml` file as `TWITCH_SECRET=`.
+    > ⚠️ Do **not** share your **client secret** to ensure the security of your app.
+- `scope`:
+  - Type: `string[]`.
+  - `Required`.
+  - Set of **permissions** to request the user's authorization to access your app for retrieving user information and performing actions on their behalf.<br /> Review all the scopes Twitch offers for utilizing their API on the [Twitch API Reference](https://dev.twitch.tv/docs/authentication/scopes).
+- `redirect_uri`:
+  - Type: `string`.
+  - `Required`.
+  - The URI to which the user will be redirected after authentication.
+- `state`:
+  - Type: `string`.
+  - `Optional`.
+  - A unique string to protect against Cross-Site Request Forgery (CSRF) attacks. The state is passed back to your redirect URI after the user has authenticated. You should verify that the state matches the one you provided in the initial request.
+- `force_verify`:
+  - Type: `boolean`.
+  - `Optional`.
+  - Set this value to `true` if you want to force the user to verify their account. Defaults to `false`.
+
+#### Authentication Flow
+
+After the completion of the Twitch OAuth flow, essential data has been prepared for use in the subsequent steps that your app needs to take.
+
+`twitchAuth` method provides 4 set key data:
+
+- `token`:
+  - Access token to make requests to the Twitch API for retrieving user information and performing actions on their behalf.
+  - Type:
+    ```
+    {
+      token: string
+      expires_in: number
+    }
+    ```
+- `refresh-token`:
+  - You can refresh new tokens using this token. The duration of this token is not specified on the Twitch docs.
+  - Type:
+    ```
+    {
+      token: string
+      expires_in: number
+    }
+    ```
+- `granted-scopes`:
+  - Scopes for which the user has granted permissions.
+  - Type: `string[]`.
+- `user-twitch`:
+  - User basic info retrieved from Twitch
+  - Type:
+    ```
+    {
+      id: string
+      login: string
+      display_name: string
+      type: string
+      broadcaster_type: string
+      description: string
+      profile_image_url: string
+      offline_image_url: string
+      view_count: number
+      email: string
+      created_at: string
+    }
+    ```
+
+> [!NOTE]
+> To access this data, utilize the `c.get` method within the callback of the upcoming HTTP request handler.
+
+```ts
+app.get('/twitch', (c) => {
+  const token = c.get('token')
+  const refreshToken = c.get('refresh-token')
+  const grantedScopes = c.get('granted-scopes')
+  const user = c.get('user-twitch')
+
+  return c.json({
+    token,
+    refreshToken,
+    grantedScopes,
+    user,
+  })
+})
+```
+
+#### Refresh Token
+
+Once the user token expires you can refresh their token without the need to prompt the user again for access. In such scenario, you can utilize the `refreshToken` method, which accepts the `client_id`, `client_secret` and `refresh_token` as parameters.
+
+> [!NOTE]
+> The `refresh_token` can be used once. Once the token is refreshed Twitch gives you a new `refresh_token` along with the new token.
+
+```ts
+import { twitchAuth, refreshToken } from '@hono/oauth-providers/twitch'
+
+app.post('/twitch/refresh', async (c, next) => {
+  const newTokens = await refreshToken(CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN)
+
+  // newTokens = {
+  //   token_type: 'bearer',
+  //   access_token: 'new-access-token',
+  //   expires_in: 60000,
+  //   refresh_token: 'new-refresh-token',
+  //   scope: ['user:read:email', 'channel:read:subscriptions', 'bits:read']
+  // }
+  // ...
+})
+```
+
+#### Revoke Token
+
+In certain use cases, you may need to programmatically revoke a user's access token. In such scenarios, you can utilize the `revokeToken` method, the `client_id` and the `token` to be revoked as parameters.
+
+It returns a `boolean` to tell whether the token was revoked or not.
+
+```ts
+import { twitchAuth, revokeToken } from '@hono/oauth-providers/twitch'
+
+app.post('/remove-user', async (c, next) => {
+  const revoked = await revokeToken(CLIENT_ID, USER_TOKEN)
+
+  // revoked = true | false
+  // ...
+})
+```
+
+#### Validate Token
+
+You can validate a Twitch access token to verify it's still valid or to obtain information about the token, such as its expiration date, scopes, and the associated user.
+
+> **IMPORTANT:** Twitch requires applications to validate OAuth tokens when they start and on an hourly basis thereafter. Failure to validate tokens may result in Twitch taking punitive action, such as revoking API keys or throttling performance. When a token becomes invalid, your app should terminate all sessions using that token immediately. [Read more](https://dev.twitch.tv/docs/authentication/validate-tokens)
+
+The validation endpoint helps your application detect when tokens become invalid for reasons other than expiration, such as when users disconnect your integration from their Twitch account. When a token becomes invalid, your app should terminate all sessions using that token.
+
+import { twitchAuth, validateToken } from '@hono/oauth-providers/twitch'
+
path: packages/oauth-providers/README.md
user: login: yusukebe
body: Please add "```" to correct this code block.
created_at: 2025-03-11T08:45:05Z
author_association: MEMBER
diff_hunk: @@ -918,6 +918,226 @@ app.post('/remove-user', async (c, next) => {
 })
 ```
 
+### Twitch
+
+```ts
+import { Hono } from 'hono'
+import { twitchAuth } from '@hono/oauth-providers/twitch'
+
+const app = new Hono()
+
+app.use(
+  '/twitch',
+  twitchAuth({
+    client_id: Bun.env.TWITCH_ID,
+    client_secret: Bun.env.TWITCH_SECRET,
+    scope: ['user:read:email', 'channel:read:subscriptions', 'bits:read'],
+    redirect_uri: 'http://localhost:3000/twitch',
+  })
+)
+
+export default app
+```
+
+#### Parameters
+
+- `client_id`:
+  - Type: `string`.
+  - `Required`.
+  - Your app client ID. You can find this value in the [Twitch Developer Portal](https://dev.twitch.tv/console/apps). <br />When developing **Cloudflare Workers**, there's no need to send this parameter. Just declare it in the `wrangler.toml` file as `TWITCH_ID=`.
+- `client_secret`:
+  - Type: `string`.
+  - `Required`.
+  - Your app client secret. You can find this value in the [Twitch Developer Portal](https://dev.twitch.tv/console/apps). <br />When developing **Cloudflare Workers**, there's no need to send this parameter. Just declare it in the `wrangler.toml` file as `TWITCH_SECRET=`.
+    > ⚠️ Do **not** share your **client secret** to ensure the security of your app.
+- `scope`:
+  - Type: `string[]`.
+  - `Required`.
+  - Set of **permissions** to request the user's authorization to access your app for retrieving user information and performing actions on their behalf.<br /> Review all the scopes Twitch offers for utilizing their API on the [Twitch API Reference](https://dev.twitch.tv/docs/authentication/scopes).
+- `redirect_uri`:
+  - Type: `string`.
+  - `Required`.
+  - The URI to which the user will be redirected after authentication.
+- `state`:
+  - Type: `string`.
+  - `Optional`.
+  - A unique string to protect against Cross-Site Request Forgery (CSRF) attacks. The state is passed back to your redirect URI after the user has authenticated. You should verify that the state matches the one you provided in the initial request.
+- `force_verify`:
+  - Type: `boolean`.
+  - `Optional`.
+  - Set this value to `true` if you want to force the user to verify their account. Defaults to `false`.
+
+#### Authentication Flow
+
+After the completion of the Twitch OAuth flow, essential data has been prepared for use in the subsequent steps that your app needs to take.
+
+`twitchAuth` method provides 4 set key data:
+
+- `token`:
+  - Access token to make requests to the Twitch API for retrieving user information and performing actions on their behalf.
+  - Type:
+    ```
+    {
+      token: string
+      expires_in: number
+    }
+    ```
+- `refresh-token`:
+  - You can refresh new tokens using this token. The duration of this token is not specified on the Twitch docs.
+  - Type:
+    ```
+    {
+      token: string
+      expires_in: number
+    }
+    ```
+- `granted-scopes`:
+  - Scopes for which the user has granted permissions.
+  - Type: `string[]`.
+- `user-twitch`:
+  - User basic info retrieved from Twitch
+  - Type:
+    ```
+    {
+      id: string
+      login: string
+      display_name: string
+      type: string
+      broadcaster_type: string
+      description: string
+      profile_image_url: string
+      offline_image_url: string
+      view_count: number
+      email: string
+      created_at: string
+    }
+    ```
+
+> [!NOTE]
+> To access this data, utilize the `c.get` method within the callback of the upcoming HTTP request handler.
+
+```ts
+app.get('/twitch', (c) => {
+  const token = c.get('token')
+  const refreshToken = c.get('refresh-token')
+  const grantedScopes = c.get('granted-scopes')
+  const user = c.get('user-twitch')
+
+  return c.json({
+    token,
+    refreshToken,
+    grantedScopes,
+    user,
+  })
+})
+```
+
+#### Refresh Token
+
+Once the user token expires you can refresh their token without the need to prompt the user again for access. In such scenario, you can utilize the `refreshToken` method, which accepts the `client_id`, `client_secret` and `refresh_token` as parameters.
+
+> [!NOTE]
+> The `refresh_token` can be used once. Once the token is refreshed Twitch gives you a new `refresh_token` along with the new token.
+
+```ts
+import { twitchAuth, refreshToken } from '@hono/oauth-providers/twitch'
+
+app.post('/twitch/refresh', async (c, next) => {
+  const newTokens = await refreshToken(CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN)
+
+  // newTokens = {
+  //   token_type: 'bearer',
+  //   access_token: 'new-access-token',
+  //   expires_in: 60000,
+  //   refresh_token: 'new-refresh-token',
+  //   scope: ['user:read:email', 'channel:read:subscriptions', 'bits:read']
+  // }
+  // ...
+})
+```
+
+#### Revoke Token
+
+In certain use cases, you may need to programmatically revoke a user's access token. In such scenarios, you can utilize the `revokeToken` method, the `client_id` and the `token` to be revoked as parameters.
+
+It returns a `boolean` to tell whether the token was revoked or not.
+
+```ts
+import { twitchAuth, revokeToken } from '@hono/oauth-providers/twitch'
+
+app.post('/remove-user', async (c, next) => {
+  const revoked = await revokeToken(CLIENT_ID, USER_TOKEN)
+
+  // revoked = true | false
+  // ...
+})
+```
+
+#### Validate Token
+
+You can validate a Twitch access token to verify it's still valid or to obtain information about the token, such as its expiration date, scopes, and the associated user.
+
+> **IMPORTANT:** Twitch requires applications to validate OAuth tokens when they start and on an hourly basis thereafter. Failure to validate tokens may result in Twitch taking punitive action, such as revoking API keys or throttling performance. When a token becomes invalid, your app should terminate all sessions using that token immediately. [Read more](https://dev.twitch.tv/docs/authentication/validate-tokens)
+
+The validation endpoint helps your application detect when tokens become invalid for reasons other than expiration, such as when users disconnect your integration from their Twitch account. When a token becomes invalid, your app should terminate all sessions using that token.
+
+import { twitchAuth, validateToken } from '@hono/oauth-providers/twitch'
+
path: packages/oauth-providers/README.md
user: login: Younis-Ahmed
body: Done ✔

The validate Token instruction has been updated with simpler instruction due to various implementations based on platforms.
created_at: 2025-03-11T19:46:46Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-02-25T16:12:10Z
body: ###  🦋  Changeset detected

Latest commit: 17532e331da260a79d4b511e5afd5c38223436d9

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                  | Type  |
| --------------------- | ----- |
| @hono/oauth-providers | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/Younis-Ahmed/middleware/new/oauth-provider.twitch?filename=.changeset/slimy-pumpkins-sleep.md&value=---%0A%22%40hono%2Foauth-providers%22%3A%20patch%0A---%0A%0Afeat(oauth-providers)%3A%20Add%20Twitch%20OAuth%20Provider%0A)


author_association: NONE
user: login: yusukebe
created_at: 2025-02-28T08:08:26Z
body: @Younis-Ahmed thank you for the PR.

Hey @monoald, can you review this?
author_association: MEMBER
user: login: Younis-Ahmed
created_at: 2025-03-05T22:51:09Z
body: Hi @BarryThePenguin

Thank you for the review, appreciate it 
author_association: CONTRIBUTOR
user: login: yusukebe
created_at: 2025-03-12T08:55:44Z
body: Hey @Younis-Ahmed !

Looks good to me. Let's go with this. Thank you!


author_association: MEMBER