mirror of
				https://github.com/Dan6erbond/sk-auth.git
				synced 2025-10-26 10:22:56 +01:00 
			
		
		
		
	✨ Import codebase from Cheeri-No
This commit is contained in:
		
							parent
							
								
									4ad637dd36
								
							
						
					
					
						commit
						d7c4438f43
					
				
							
								
								
									
										201
									
								
								src/SvelteAuth/auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								src/SvelteAuth/auth.ts
									
									
									
									
									
										Normal 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 {};
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								src/SvelteAuth/client/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/SvelteAuth/client/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
export { signIn } from "./signIn";
 | 
			
		||||
export { signOut } from "./signOut";
 | 
			
		||||
							
								
								
									
										38
									
								
								src/SvelteAuth/client/signIn.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/SvelteAuth/client/signIn.ts
									
									
									
									
									
										Normal 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);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								src/SvelteAuth/client/signOut.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/SvelteAuth/client/signOut.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										2
									
								
								src/SvelteAuth/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
export { Auth } from "./auth";
 | 
			
		||||
export { Provider } from "./providers";
 | 
			
		||||
							
								
								
									
										13
									
								
								src/SvelteAuth/interfaces.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/SvelteAuth/interfaces.ts
									
									
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								src/SvelteAuth/providers/base.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/SvelteAuth/providers/base.ts
									
									
									
									
									
										Normal 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>;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										78
									
								
								src/SvelteAuth/providers/facebook.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/SvelteAuth/providers/facebook.ts
									
									
									
									
									
										Normal 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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										82
									
								
								src/SvelteAuth/providers/google.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/SvelteAuth/providers/google.ts
									
									
									
									
									
										Normal 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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								src/SvelteAuth/providers/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/SvelteAuth/providers/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
export { Provider } from "./base";
 | 
			
		||||
export { GoogleOAuthProvider } from "./google";
 | 
			
		||||
							
								
								
									
										59
									
								
								src/SvelteAuth/providers/oauth2.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/SvelteAuth/providers/oauth2.ts
									
									
									
									
									
										Normal 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, "/")];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										119
									
								
								src/SvelteAuth/providers/reddit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/SvelteAuth/providers/reddit.ts
									
									
									
									
									
										Normal 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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										87
									
								
								src/SvelteAuth/providers/twitter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/SvelteAuth/providers/twitter.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										2
									
								
								src/SvelteAuth/types.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
export type Profile = any;
 | 
			
		||||
export type CallbackResult = [Profile, string | null];
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user