Last active
April 17, 2022 17:17
-
-
Save SagnikPradhan/4f1bb44b212cddf734935e33cbc412ef to your computer and use it in GitHub Desktop.
Express OAuth2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import config from "$/core/config"; | |
import OAuth2 from "./helper"; | |
import * as Discord from "discord-api-types/v10"; | |
import * as Github from "@octokit/types"; | |
export const discord = new OAuth2.Provider<Discord.RESTGetAPIUserResult>({ | |
credentials: config.oauth.discord!, | |
accountURL: Discord.RouteBases.api + Discord.Routes.user(), | |
authorizeURL: Discord.OAuth2Routes.authorizationURL, | |
tokenURL: Discord.OAuth2Routes.tokenURL, | |
scope: ["identify", "email"], | |
additionalProps: { prompt: "none" }, | |
account: ({ id, email, username, avatar }) => ({ | |
id, | |
username, | |
email: email || null, | |
avatar: avatar | |
? Discord.RouteBases.cdn + `/avatars/${id}/${avatar}.png` | |
: null, | |
}), | |
}); | |
export const github = new OAuth2.Provider< | |
Github.Endpoints["GET /user"]["response"]["data"] | |
>({ | |
credentials: config.oauth.github!, | |
accountURL: "https://api.github.com/user", | |
authorizeURL: "https://github.com/login/oauth/authorize", | |
tokenURL: "https://github.com/login/oauth/access_token", | |
scope: ["read:user", "user:email"], | |
additionalProps: { prompt: "none" }, | |
account: ({ id, email, name, avatar_url }) => ({ | |
id: id.toString(), | |
username: name, | |
email, | |
avatar: avatar_url, | |
}), | |
}); | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const oauth2 = new OAuth2({ baseURL: "" }); | |
oauth2.register("discord", discord).register("github", github); | |
router.use( | |
oauth2.authFlow({ | |
route: "/auth/:provider", | |
handler(account, _, response) { | |
response.json(account); | |
}, | |
}) | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import axios from "axios"; | |
import Express from "express"; | |
import { AuthorizationCode } from "simple-oauth2"; | |
export interface ProviderOptions<ProviderPayload, Account> { | |
/** OAuth2 Client Credentials */ | |
credentials: { id: string; secret: string }; | |
/** OAuth2 Provider token URL */ | |
tokenURL: string; | |
/** OAuth2 Provider authorize URL */ | |
authorizeURL: string; | |
/** OAuth2 Account fetch URL */ | |
accountURL: string; | |
/** OAuth2 Scopes */ | |
scope: string[]; | |
/** Additional props for authorize URL */ | |
additionalProps?: Record<string, string>; | |
/** Account mapper */ | |
account: (data: ProviderPayload) => Account; | |
} | |
export interface DefaultAccount {} | |
/** | |
* OAuth2 Provider | |
*/ | |
export class Provider< | |
ProviderPayload, | |
Account = DefaultAccount | |
> extends AuthorizationCode { | |
private readonly scope: string[]; | |
private readonly additionalProps: Record<string, string>; | |
private readonly accountURL: string; | |
private readonly account: (data: ProviderPayload) => Account; | |
/** | |
* Create new provider | |
* | |
* @param options Provider Options | |
*/ | |
constructor(options: ProviderOptions<ProviderPayload, Account>) { | |
const tokenURL = new URL(options.tokenURL); | |
const authorizeURL = new URL(options.authorizeURL); | |
super({ | |
client: options.credentials, | |
auth: { | |
tokenHost: tokenURL.host, | |
tokenPath: tokenURL.pathname, | |
authorizeHost: authorizeURL.host, | |
authorizePath: authorizeURL.pathname, | |
}, | |
}); | |
this.scope = options.scope; | |
this.additionalProps = options.additionalProps || {}; | |
this.accountURL = options.accountURL; | |
this.account = options.account; | |
} | |
/** | |
* Get redirect or authorize url | |
* | |
* @param callbackURL - Absolute URL of callback endpoint | |
* @param state - OAuth2 State | |
* @returns Authorize url | |
*/ | |
public getRedirectURL(callbackURL: string, state?: string) { | |
const url = new URL( | |
super.authorizeURL({ | |
state, | |
scope: this.scope, | |
redirect_uri: callbackURL, | |
}) | |
); | |
for (const key in this.additionalProps) | |
url.searchParams.set(key, this.additionalProps[key]); | |
return url.toString(); | |
} | |
/** | |
* Get account of user | |
* | |
* @param callbackURL - Absolute URL of callback endpoint | |
* @param code - OAuth2 Code | |
* @returns Account of user | |
*/ | |
public async getAccount(callbackURL: string, code: string) { | |
const token = await super.getToken({ | |
code: code, | |
redirect_uri: callbackURL, | |
}); | |
const response = await axios.get<ProviderPayload>(this.accountURL, { | |
headers: { Authorization: `Bearer ${token}` }, | |
}); | |
return this.account(response.data); | |
} | |
} | |
export interface OAuth2Options { | |
baseURL: string; | |
} | |
export interface AuthFlowOptions { | |
/** OAuth2 authorize redirect route */ | |
route: string; | |
/** | |
* OAuth2 callback route | |
* @defaultValue `route + "/callback"` | |
*/ | |
callbackRoute?: string; | |
/** Override provider */ | |
provider?: string; | |
/** | |
* Handler Function | |
* | |
* @param account Account returned from mapper | |
* @param request Express request | |
* @param response Express response | |
* @param next Express next function | |
*/ | |
handler: ( | |
account: DefaultAccount, | |
request: Express.Request, | |
response: Express.Response, | |
next: Express.NextFunction | |
) => void | Promise<void>; | |
} | |
/** | |
* OAuth2 | |
*/ | |
export default class OAuth2 { | |
public static readonly Provider = Provider; | |
private readonly baseURL: string; | |
private readonly providers: Map<string, Provider<any>>; | |
/** | |
* Create new OAuth2 Manager | |
* | |
* @param options - OAuth2 Options | |
* @param options.baseURL - Base URL for client server | |
*/ | |
constructor(options: OAuth2Options) { | |
this.baseURL = options.baseURL; | |
this.providers = new Map(); | |
} | |
/** | |
* Register a provider | |
* | |
* @param name - Name of provider | |
* @param provider - Provider instance | |
* @returns OAuth2 | |
*/ | |
public register(name: string, provider: Provider<any>) { | |
this.providers.set(name, provider); | |
return this; | |
} | |
/** | |
* Initialize an auth flow | |
* | |
* @param options - Auth flow options | |
* @returns Router | |
*/ | |
public authFlow(options: AuthFlowOptions) { | |
const router = Express.Router(); | |
const callbackRoute = options.callbackRoute || options.route + "/callback"; | |
router.get(options.route, (request, response) => { | |
const provider = this.getProvider(request, options.provider); | |
const callbackURL = this.getCallbackURL(callbackRoute, provider.name); | |
const redirectURL = provider.instance.getRedirectURL(callbackURL); | |
response.redirect(redirectURL); | |
}); | |
router.get(callbackRoute, async (request, response, next) => { | |
const provider = this.getProvider(request, options.provider); | |
const callbackURL = this.getCallbackURL(callbackRoute, provider.name); | |
const code = request.query.code; | |
if (typeof code !== "string") throw new Error("No OAuth2 code found"); | |
const account = await provider.instance.getAccount(callbackURL, code); | |
return options.handler(account, request, response, next); | |
}); | |
return router; | |
} | |
/** | |
* Get provider instance from request and user overide, throw otherwise | |
* | |
* @param request - Express request | |
* @param overrrideProvider - User override provider | |
* @returns Provider instance and name | |
*/ | |
private getProvider(request: Express.Request, overrrideProvider?: string) { | |
const name = overrrideProvider || request.params.provider; | |
const provider = this.providers.get(name); | |
if (provider) return { name, instance: provider }; | |
else throw new Error(`No provider instance found for ${name}`); | |
} | |
/** | |
* Get absolute URL to callback endpoint | |
* | |
* @param route - Callback route | |
* @param provider - Provider name | |
* @returns URL | |
*/ | |
private getCallbackURL(route: string, provider: string) { | |
return new URL( | |
route.replace(":provider", provider), | |
this.baseURL | |
).toString(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment