Import codebase from Cheeri-No

This commit is contained in:
RaviAnand Mohabir 2021-05-17 18:47:55 +02:00
parent 4ad637dd36
commit d7c4438f43
14 changed files with 729 additions and 0 deletions

201
src/SvelteAuth/auth.ts Normal file
View File

@ -0,0 +1,201 @@
import type { GetSession, RequestHandler } from "@sveltejs/kit";
import type { EndpointOutput, ServerRequest } from "@sveltejs/kit/types/endpoint";
import type { Headers } from "@sveltejs/kit/types/helper";
import cookie from "cookie";
import * as jsonwebtoken from "jsonwebtoken";
import type { JWT, Session } from "./interfaces";
import type { Provider } from "./providers";
interface AuthConfig {
providers?: Provider[];
callbacks?: AuthCallbacks;
jwtSecret?: string;
jwtExpiresIn?: string | number;
}
interface AuthCallbacks {
signIn?: () => boolean | Promise<boolean>;
jwt?: (token: JWT, profile?: any) => JWT | Promise<JWT>;
session?: (token: JWT, session: Session) => Session | Promise<Session>;
redirect?: (url: string) => string | Promise<string>;
}
export class Auth {
constructor(private readonly config?: AuthConfig) {}
getJwtSecret() {
if (this.config?.jwtSecret) {
return this.config?.jwtSecret;
}
if (this.config?.providers?.length) {
const provs = this.config?.providers?.map((provider) => provider.id).join("+");
return Buffer.from(provs).toString("base64");
}
return "svelte_auth_secret";
}
async getToken(headers: Headers) {
if (!headers.cookie) {
return null;
}
const cookies = cookie.parse(headers.cookie);
if (!cookies.svelteauthjwt) {
return null;
}
let token: JWT;
try {
token = (jsonwebtoken.verify(cookies.svelteauthjwt, this.getJwtSecret()) || {}) as JWT;
} catch {
return null;
}
if (this.config?.callbacks?.jwt) {
token = await this.config.callbacks.jwt(token);
}
return token;
}
getBaseUrl(host: string) {
return `http://${host}`;
}
setToken(headers: Headers, newToken: JWT | any) {
const originalToken = this.getToken(headers);
return {
...(originalToken ?? {}),
...newToken,
};
}
signToken(token: JWT) {
const opts = !token.exp
? {
expiresIn: this.config?.jwtExpiresIn ?? "30d",
}
: {};
const jwt = jsonwebtoken.sign(token, this.getJwtSecret(), opts);
return jwt;
}
async getRedirectUrl(host: string, redirectUrl?: string) {
let redirect = redirectUrl || this.getBaseUrl(host);
if (this.config?.callbacks?.redirect) {
redirect = await this.config.callbacks.redirect(redirect);
}
return redirect;
}
async handleProviderCallback(
request: ServerRequest,
provider: Provider,
): Promise<EndpointOutput> {
const { headers, host } = request;
const [profile, redirectUrl] = await provider.callback(request);
let token = (await this.getToken(headers)) ?? { user: {} };
if (this.config?.callbacks?.jwt) {
token = await this.config.callbacks.jwt(token, profile);
} else {
token = this.setToken(headers, { user: profile });
}
const jwt = this.signToken(token);
console.log(jwt);
const redirect = await this.getRedirectUrl(host, redirectUrl);
return {
status: 302,
headers: {
"set-cookie": `svelteauthjwt=${jwt}; Path=/; HttpOnly`,
Location: redirect,
},
};
}
async handleEndpoint(request: ServerRequest): Promise<EndpointOutput> {
const { path, headers, method, host } = request;
if (path === "/api/auth/signout") {
const token = this.setToken(headers, {});
const jwt = this.signToken(token);
if (method === "POST") {
return {
headers: {
"set-cookie": `svelteauthjwt=${jwt}; Path=/; HttpOnly`,
},
body: {
signout: true,
},
};
}
const redirect = await this.getRedirectUrl(host);
return {
status: 302,
headers: {
"set-cookie": `svelteauthjwt=${jwt}; Path=/; HttpOnly`,
Location: redirect,
},
};
}
const match = path.match(/\/api\/auth\/(?<method>signin|callback)\/(?<provider>\w+)/);
if (match) {
const provider = this.config?.providers?.find(
(provider) => provider.id === match.groups.provider,
);
if (provider) {
if (match.groups.method === "signin") {
return await provider.signin(request);
} else {
return await this.handleProviderCallback(request, provider);
}
}
}
}
get: RequestHandler = async (request) => {
const { path } = request;
if (path === "/api/auth/csrf") {
return { body: "1234" }; // TODO: Generate real token
} else if (path === "/api/auth/session") {
const session = await this.getSession(request);
return {
body: {
session,
},
};
}
return await this.handleEndpoint(request);
};
post: RequestHandler = async (request) => {
return await this.handleEndpoint(request);
};
getSession: GetSession = async ({ headers }) => {
const token = await this.getToken(headers);
if (token) {
if (this.config?.callbacks?.session) {
return await this.config.callbacks.session(token, { user: token.user });
}
return { user: token.user };
}
return {};
};
}

View File

@ -0,0 +1,2 @@
export { signIn } from "./signIn";
export { signOut } from "./signOut";

View File

@ -0,0 +1,38 @@
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import type { Page } from "@sveltejs/kit";
interface SignInConfig {
redirectUrl?: string;
}
export async function signIn(provider: string, data?: any, config?: SignInConfig) {
if (data) {
const path = `/api/auth/callback/${provider}`;
const res = await fetch(path, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
return await res.json();
}
let redirectUrl: string;
if (config?.redirectUrl) {
redirectUrl = config.redirectUrl;
} else {
let $val: Page;
page.subscribe(($) => ($val = $))();
redirectUrl = `${$val.host}${$val.path}?${$val.query}`;
}
const queryData = {
redirect: redirectUrl,
};
const query = new URLSearchParams(queryData);
const path = `/api/auth/login/${provider}?${query}`;
return await goto(path);
}

View File

@ -0,0 +1,12 @@
import { session as session$ } from "$app/stores";
export async function signOut() {
const res = await fetch("/api/auth/signout", { method: "POST" });
const { signout } = await res.json();
fetch("/api/auth/session")
.then((res) => res.json())
.then(session$.set);
return signout === true;
}

2
src/SvelteAuth/index.ts Normal file
View File

@ -0,0 +1,2 @@
export { Auth } from "./auth";
export { Provider } from "./providers";

View File

@ -0,0 +1,13 @@
export interface JWT {
user: User;
[key: string]: any;
}
export interface User {
[key: string]: any;
}
export interface Session {
user: User;
[key: string]: any;
}

View File

@ -0,0 +1,32 @@
import type { EndpointOutput } from "@sveltejs/kit";
import type { ServerRequest } from "@sveltejs/kit/types/endpoint";
import type { CallbackResult } from "../types";
export interface ProviderConfig {
id?: string;
profile?: (profile: any, account: any) => any | Promise<any>;
}
export abstract class Provider<T extends ProviderConfig = ProviderConfig> {
id: string;
constructor(protected readonly config: T) {
this.id = config.id;
}
getUri(host: string, path: string) {
return `http://${host}${path}`;
}
getCallbackUri(host: string) {
return this.getUri(host, `${"/api/auth/callback/"}${this.id}`);
}
abstract signin<Locals extends Record<string, any> = Record<string, any>, Body = unknown>(
request: ServerRequest<Locals, Body>,
): EndpointOutput | Promise<EndpointOutput>;
abstract callback<Locals extends Record<string, any> = Record<string, any>, Body = unknown>(
request: ServerRequest<Locals, Body>,
): CallbackResult | Promise<CallbackResult>;
}

View File

@ -0,0 +1,78 @@
import type { ServerRequest } from "@sveltejs/kit/types/endpoint";
import { OAuth2Provider, OAuth2ProviderConfig } from "./oauth2";
interface FacebookAuthProviderConfig extends OAuth2ProviderConfig {
clientId: string;
clientSecret: string;
userProfileFields?: string;
scope?: string;
}
const defaultConfig: Partial<FacebookAuthProviderConfig> = {
id: "facebook",
scope: "email public_profile user_link",
userProfileFields:
"id,name,first_name,last_name,middle_name,name_format,picture,short_name,email",
};
export class FacebookAuthProvider extends OAuth2Provider<FacebookAuthProviderConfig> {
constructor(config: FacebookAuthProviderConfig) {
super({
...defaultConfig,
...config,
});
}
getSigninUrl({ host }: ServerRequest, state: string) {
const endpoint = "https://www.facebook.com/v10.0/dialog/oauth";
const data = {
client_id: this.config.clientId,
scope: this.config.scope,
redirect_uri: this.getCallbackUri(host),
state,
};
const url = `${endpoint}?${new URLSearchParams(data)}`;
return url;
}
async getTokens(code: string, redirectUri: string) {
const endpoint = "https://graph.facebook.com/v10.0/oauth/access_token";
const data = {
code,
client_id: this.config.clientId,
redirect_uri: redirectUri,
client_secret: this.config.clientSecret,
};
const res = await fetch(`${endpoint}?${new URLSearchParams(data)}`);
return await res.json();
}
async inspectToken(tokens: any) {
const endpoint = "https://graph.facebook.com/debug_token";
const data = {
input_token: tokens.access_token,
access_token: `${this.config.clientId}|${this.config.clientSecret}`,
};
const res = await fetch(`${endpoint}?${new URLSearchParams(data)}`);
return await res.json();
}
async getUserProfile(tokens: any) {
const inspectResult = await this.inspectToken(tokens);
const endpoint = `https://graph.facebook.com/v10.0/${inspectResult.data.user_id}`;
const data = {
access_token: tokens.access_token,
fields: this.config.userProfileFields,
};
const res = await fetch(`${endpoint}?${new URLSearchParams(data)}`);
return await res.json();
}
}

View File

@ -0,0 +1,82 @@
import type { ServerRequest } from "@sveltejs/kit/types/endpoint";
import { OAuth2Provider, OAuth2ProviderConfig } from "./oauth2";
interface GoogleOAuthProviderConfig extends OAuth2ProviderConfig {
clientId: string;
clientSecret: string;
discoveryDocument?: string;
scope?: string;
}
const defaultConfig: Partial<GoogleOAuthProviderConfig> = {
id: "google",
discoveryDocument: "https://accounts.google.com/.well-known/openid-configuration",
scope: "openid profile email",
};
export class GoogleOAuthProvider extends OAuth2Provider<GoogleOAuthProviderConfig> {
constructor(config: GoogleOAuthProviderConfig) {
super({
...defaultConfig,
...config,
});
}
async getProviderMetadata() {
const res = await fetch(this.config.discoveryDocument);
const metadata = await res.json();
return metadata;
}
async getEndpoint(key: string) {
const metadata = await this.getProviderMetadata();
return metadata[key] as string;
}
async getSigninUrl({ host }: ServerRequest, state: string) {
const authorizationEndpoint = await this.getEndpoint("authorization_endpoint");
const data = {
response_type: "code",
client_id: this.config.clientId,
scope: this.config.scope,
redirect_uri: this.getCallbackUri(host),
state,
login_hint: "example@provider.com",
nonce: Math.round(Math.random() * 1000).toString(), // TODO: Generate random based on user values
};
const url = `${authorizationEndpoint}?${new URLSearchParams(data)}`;
return url;
}
async getTokens(code: string, redirectUri: string) {
const tokenEndpoint = await this.getEndpoint("token_endpoint");
const data = {
code,
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
redirect_uri: redirectUri,
grant_type: "authorization_code",
};
const res = await fetch(tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
return await res.json();
}
async getUserProfile(tokens: any) {
const userProfileEndpoint = await this.getEndpoint("userinfo_endpoint");
const res = await fetch(userProfileEndpoint, {
headers: { Authorization: `${tokens.token_type} ${tokens.access_token}` },
});
return await res.json();
}
}

View File

@ -0,0 +1,2 @@
export { Provider } from "./base";
export { GoogleOAuthProvider } from "./google";

View File

@ -0,0 +1,59 @@
import type { EndpointOutput, ServerRequest } from "@sveltejs/kit/types/endpoint";
import type { CallbackResult } from "../types";
import { Provider, ProviderConfig } from "./base";
export interface OAuth2ProviderConfig extends ProviderConfig {
profile?: (profile: any, tokens: any) => any | Promise<any>;
}
export abstract class OAuth2Provider<T extends OAuth2ProviderConfig> extends Provider<T> {
abstract getSigninUrl(request: ServerRequest, state: string): string | Promise<string>;
abstract getTokens(code: string, redirectUri: string): any | Promise<any>;
abstract getUserProfile(tokens: any): any | Promise<any>;
async signin(request: ServerRequest): Promise<EndpointOutput> {
const { method, host, query } = request;
const state = [`redirect=${query.get("redirect") ?? this.getUri(host, "/")}`].join(",");
const base64State = Buffer.from(state).toString("base64");
const url = await this.getSigninUrl(request, base64State);
if (method === "POST") {
return {
body: {
redirect: url,
},
};
}
return {
status: 302,
headers: {
Location: url,
},
};
}
getStateValue(query: URLSearchParams, name: string) {
if (query.get("state")) {
const state = Buffer.from(query.get("state"), "base64").toString();
return state
.split(",")
.find((state) => state.startsWith(`${name}=`))
?.replace(`${name}=`, "");
}
}
async callback({ query, host }: ServerRequest): Promise<CallbackResult> {
const code = query.get("code");
const redirect = this.getStateValue(query, "redirect");
const tokens = await this.getTokens(code, this.getCallbackUri(host));
let user = await this.getUserProfile(tokens);
if (this.config.profile) {
user = await this.config.profile(user, tokens);
}
return [user, redirect ?? this.getUri(host, "/")];
}
}

View File

@ -0,0 +1,119 @@
import type { ServerRequest } from "@sveltejs/kit/types/endpoint";
import { OAuth2Provider, OAuth2ProviderConfig } from "./oauth2";
interface RedditOAuthProviderConfig extends OAuth2ProviderConfig {
apiKey: string;
apiSecret: string;
scope?: string;
duration?: "temporary" | "permanent";
}
const defaultConfig: Partial<RedditOAuthProviderConfig> = {
id: "reddit",
scope: "identity",
duration: "temporary",
profile: ({
is_employee,
has_external_account,
snoovatar_img,
verified,
id,
over_18,
is_gold,
is_mod,
awarder_karma,
has_verified_email,
is_suspended,
icon_img,
pref_nightmode,
awardee_karma,
password_set,
link_karma,
total_karma,
name,
created,
created_utc,
comment_karma,
}) => ({
is_employee,
has_external_account,
snoovatar_img,
verified,
id,
over_18,
is_gold,
is_mod,
awarder_karma,
has_verified_email,
is_suspended,
icon_img,
pref_nightmode,
awardee_karma,
password_set,
link_karma,
total_karma,
name,
created,
created_utc,
comment_karma,
}),
};
export class RedditOAuthProvider extends OAuth2Provider<RedditOAuthProviderConfig> {
constructor(config: RedditOAuthProviderConfig) {
super({
...defaultConfig,
...config,
});
}
async getSigninUrl({ host }: ServerRequest, state: string) {
const endpoint = "https://www.reddit.com/api/v1/authorize";
const data = {
client_id: this.config.apiKey,
response_type: "code",
state,
redirect_uri: this.getCallbackUri(host),
duration: this.config.duration,
scope: this.config.scope,
};
const url = `${endpoint}?${new URLSearchParams(data)}`;
return url;
}
async getTokens(code: string, redirectUri: string) {
const endpoint = "https://www.reddit.com/api/v1/access_token";
const data = {
code,
redirect_uri: redirectUri,
grant_type: "authorization_code",
};
const body = Object.entries(data)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join("&");
const res = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization:
"Basic " +
Buffer.from(`${this.config.apiKey}:${this.config.apiSecret}`).toString("base64"),
},
body,
});
return await res.json();
}
async getUserProfile(tokens: any) {
const endpoint = "https://oauth.reddit.com/api/v1/me";
const res = await fetch(endpoint, {
headers: { Authorization: `${tokens.token_type} ${tokens.access_token}` },
});
return await res.json();
}
}

View File

@ -0,0 +1,87 @@
import type { ServerRequest } from "@sveltejs/kit/types/endpoint";
import type { CallbackResult } from "../types";
import { OAuth2Provider, OAuth2ProviderConfig } from "./oauth2";
interface TwitterAuthProviderConfig extends OAuth2ProviderConfig {
apiKey: string;
apiSecret: string;
}
const defaultConfig: Partial<TwitterAuthProviderConfig> = {
id: "twitter",
};
export class TwitterAuthProvider extends OAuth2Provider<TwitterAuthProviderConfig> {
constructor(config: TwitterAuthProviderConfig) {
super({
...defaultConfig,
...config,
});
}
async getRequestToken(host: string) {
const endpoint = "https://api.twitter.com/oauth/request_token";
const data = {
oauth_callback: encodeURIComponent(this.getCallbackUri(host)),
oauth_consumer_key: this.config.apiKey,
};
const res = await fetch(`${endpoint}?${new URLSearchParams(data)}`, { method: "POST" });
const { oauth_token, oauth_token_secret, oauth_callback_confirmed } = await res.json();
return {
oauthToken: oauth_token,
oauthTokenSecret: oauth_token_secret,
oauthCallbackConfirmed: oauth_callback_confirmed,
};
}
async getSigninUrl({ host }: ServerRequest) {
const endpoint = "https://api.twitter.com/oauth/authorize";
const { oauthToken } = await this.getRequestToken(host);
const data = {
oauth_token: oauthToken,
};
const url = `${endpoint}?${new URLSearchParams(data)}`;
return url;
}
async getTokens(oauthToken: string, oauthVerifier: string) {
const endpoint = "https://api.twitter.com/oauth/access_token";
const data = {
oauth_consumer_key: this.config.apiKey,
oauth_token: oauthToken,
oauth_verifier: oauthVerifier,
};
const res = await fetch(`${endpoint}?${new URLSearchParams(data)}`, { method: "POST" });
return await res.json();
}
async getUserProfile({ oauth_token, oauth_token_secret: _ }: any) {
const endpoint = "https://api.twitter.com/1.1/account/verify_credentials.json";
const res = await fetch(endpoint, { headers: { Authorization: `Bearer ${oauth_token}` } });
return await res.json();
}
async callback({ query, host }: ServerRequest): Promise<CallbackResult> {
const oauthToken = query.get("oauth_token");
const oauthVerifier = query.get("oauth_verifier");
const redirect = this.getStateValue(query, "redirect");
const tokens = await this.getTokens(oauthToken, oauthVerifier);
let user = await this.getUserProfile(tokens);
if (this.config.profile) {
user = await this.config.profile(user, tokens);
}
return [user, redirect ?? this.getUri(host, "/")];
}
}

2
src/SvelteAuth/types.ts Normal file
View File

@ -0,0 +1,2 @@
export type Profile = any;
export type CallbackResult = [Profile, string | null];