mirror of
https://github.com/Dan6erbond/sk-auth.git
synced 2024-11-20 19:07:20 +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