Skip to content

Commit

Permalink
feat: twitch oauth2 provider (#4)
Browse files Browse the repository at this point in the history
* feat: ✨ feat a provider for twitch oauth2

* fix: 🎨 define grant_type and storage setters for twitch

* docs: 📝 explain tokenLogin why and when

* chore: 🔥 set auto redirect to profile

Make easier to fetch profile info and auto redirectTo profile after successfuly logged in

* fix: 🐛 add missing twitch provider profile data

* fix: ⚰️ abort the auto profile idea

* refactor: remove dead code on utils

* docs: rollback readme changes to standard

* feat: ⚡ feat cookies to transport data through storage and plugin

* docs: fix duplicated documentation of cookie

* fix: remove required redundants and exclusive usage points

* fix: remove dead imports and code

* fix: rollback redirect params and dead imports

* docs: remove cookies declaration on readme

* fix: remove useless id on tokenHeaders

* fix: remove useless conditional usage

* styles: remove lb and missing semicolon

* feat: twitch oauth2 provider

---------

Co-authored-by: bogeychan <[email protected]>
  • Loading branch information
iagocalazans and bogeychan authored Oct 4, 2023
1 parent 975c8e7 commit 7f97fb1
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 8 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,12 @@ console.log('Listening on http://localhost:3000');
1. Generate a `client id` and `client secret` for an [OAuth app on Github](https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app)
2. Use `http://localhost:3000/login/github/authorized` as your `Authorization callback URL`
3. Create an `.env` file based on the previously generated client credentials:

```env
GITHUB_OAUTH_CLIENT_ID=client id
GITHUB_OAUTH_CLIENT_SECRET=client secret
```

4. [Bun](https://bun.sh/docs/cli/run#environment-variables) automatically loads environment variables from `.env` files

If you are unsure which URL should be used as `Authorization callback URL` call `ctx.profiles()` without an argument to get all URLs of all registered OAuth 2.0 Profiles:
Expand Down Expand Up @@ -170,4 +172,3 @@ const auth = oauth2({
## License

[MIT](LICENSE)

26 changes: 24 additions & 2 deletions examples/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import oauth2, {
github,
spotify,
reddit,
google
} from '../src/index';
google,
twitch,
validateToken
} from '../src';

import { randomBytes } from 'crypto';
import { Database } from 'bun:sqlite';
Expand All @@ -23,6 +25,8 @@ const states = new Set();

const app = new Elysia();

const twitchProvider = twitch();

const auth = oauth2({
profiles: {
azure: {
Expand All @@ -48,6 +52,10 @@ const auth = oauth2({
google: {
provider: google(),
scope: ['https://www.googleapis.com/auth/userinfo.profile']
},
twitch: {
provider: twitchProvider,
scope: ['user:read:follows']
}
},
state: {
Expand Down Expand Up @@ -170,6 +178,19 @@ app
return userPage(await user.json(), profiles.google.logout);
}

if (await ctx.authorized('twitch')) {
const tokenHeaders = await ctx.tokenHeaders('twitch');

if ((await validateToken(tokenHeaders)).status === 200) {
// https://dev.twitch.tv/docs/api/reference/#get-users
const user = await fetch('https://api.twitch.tv/helix/users', {
headers: { 'Client-Id': twitchProvider.clientId, ...tokenHeaders }
});

return userPage(await user.json(), profiles.twitch.logout);
}
}

const html = `<!DOCTYPE html>
<html lang="en">
<body>
Expand All @@ -187,3 +208,4 @@ app
.listen(3000);

console.log(`http://localhost:3000`);

6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type TOAuth2Request<Profile extends string> = {
*/
export type TOAuth2AccessToken = {
token_type: string;
scope: string;
scope: string | string[];
expires_in: number;
access_token: string;
created_at: number;
Expand Down Expand Up @@ -344,7 +344,7 @@ const oauth2 = <Profiles extends string>({
},

// authorize(...profiles: Profiles[]) {
// throw new Error('not implementd');
// throw new Error('not implemented');
// },

profiles<P extends Profiles = Profiles>(...profiles: P[]) {
Expand All @@ -365,7 +365,7 @@ const oauth2 = <Profiles extends string>({
return result;
},

async tokenHeaders(profile: Profiles) {
async tokenHeaders(profile) {
const token = await storage.get(ctx.request, profile);
return { Authorization: `Bearer ${token?.access_token}` };
}
Expand Down
1 change: 1 addition & 0 deletions src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './discord';
export * from './spotify';
export * from './reddit';
export * from './google';
export * from './twitch';
98 changes: 98 additions & 0 deletions src/providers/twitch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { TOAuth2Provider } from '..';
import { env } from '../utils';

/**
* @see https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#authorization-code-grant-flow
*/
type TTwitchParams = {
/**
* Set to true to force the user to re-authorize your app’s access to their resources. The default is false.
*/
force_verify?: boolean;
};

export type TTwitchToken = {
client_id: string;
login: string;
scopes: string[];
user_id: string;
expires_in: number;
};

export type TTwitchTokenValidationResult = {
status: 200 | 401 | number;
message?: string;
token?: TTwitchToken;
};

/**
* @example
*
* const twitchProvider = twitch();
*
* // ...
*
* const tokenHeaders = await ctx.tokenHeaders('twitch');
*
* if ((await validateToken(tokenHeaders)).status === 200) {
* const user = await fetch('https://api.twitch.tv/helix/users', {
* headers: { 'Client-Id': twitchProvider.clientId, ...tokenHeaders }
* });
* }
*
* @see https://dev.twitch.tv/docs/authentication/validate-tokens
*/
export async function validateToken(headers: {
Authorization: string;
}): Promise<TTwitchTokenValidationResult> {
const response = await fetch('https://id.twitch.tv/oauth2/validate', {
headers
});

if (!response.ok) {
throw response;
}

const isJson = response.headers
.get('Content-Type')
?.startsWith('application/json');

if (!isJson) {
throw response;
}

const json = await response.json();

if (response.status === 200) {
return {
status: 200,
token: json
};
}

return json;
}

export function twitch({ force_verify }: TTwitchParams = {}): TOAuth2Provider {
const authParams: TTwitchParams = {};

if (typeof force_verify === 'boolean') {
authParams.force_verify = force_verify;
}

return {
clientId: env('TWITCH_OAUTH_CLIENT_ID'),
clientSecret: env('TWITCH_OAUTH_CLIENT_SECRET'),

auth: {
url: 'https://id.twitch.tv/oauth2/authorize',
params: authParams
},

token: {
url: 'https://id.twitch.tv/oauth2/token',
params: {}
}
};
}

5 changes: 3 additions & 2 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { TOAuth2UrlParams, TOAuth2Scope, TOAuth2AccessToken } from '..';

export function env(name: string) {
export function env(name: string): string {
if (!(name in process.env)) {
throw new Error(
`.env variable '${name}' is required but could not be found`
);
}
return process.env[name];

return process.env[name]!;
}

export function buildUrl(
Expand Down

0 comments on commit 7f97fb1

Please sign in to comment.