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