Skip to content

Instantly share code, notes, and snippets.

@SagnikPradhan
Last active April 17, 2022 17:17
Show Gist options
  • Save SagnikPradhan/4f1bb44b212cddf734935e33cbc412ef to your computer and use it in GitHub Desktop.
Save SagnikPradhan/4f1bb44b212cddf734935e33cbc412ef to your computer and use it in GitHub Desktop.
Express OAuth2
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,
}),
});
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);
},
})
);
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