mirror of
https://github.com/Dan6erbond/sk-auth.git
synced 2024-12-18 19:05:47 +01:00
Merge branch 'main' into CrispyBacon1999/main
This commit is contained in:
commit
5c984a3653
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@ -0,0 +1,4 @@
|
||||
README.md
|
||||
app/
|
||||
*.cjs
|
||||
dist/
|
21
.eslintrc-shared.cjs
Normal file
21
.eslintrc-shared.cjs
Normal file
@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
extends: ["prettier"],
|
||||
plugins: ["@typescript-eslint"],
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaVersion: 2019,
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true,
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["off", { argsIgnorePattern: "^_" }],
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
},
|
||||
};
|
@ -1,22 +1,9 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||
plugins: ["@typescript-eslint"],
|
||||
ignorePatterns: ["*.cjs", "app/**/*"],
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaVersion: 2019,
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["off", { argsIgnorePattern: "^_" }],
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"./.eslintrc-shared.cjs",
|
||||
],
|
||||
};
|
||||
|
7
.github/workflows/lint-on-pr.yml
vendored
7
.github/workflows/lint-on-pr.yml
vendored
@ -15,9 +15,6 @@ jobs:
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- run: yarn install
|
||||
- run: yarn lint
|
||||
- run: yarn install
|
||||
working-directory: app
|
||||
- run: yarn lint
|
||||
- run: yarn install --frozen-lockfile && yarn lint
|
||||
- run: yarn install --frozen-lockfile && yarn lint
|
||||
working-directory: app
|
||||
|
2
.github/workflows/npm-publish.yml
vendored
2
.github/workflows/npm-publish.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- run: yarn install
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -76,4 +76,4 @@ typings/
|
||||
.fusebox/
|
||||
|
||||
# Build output
|
||||
dist/*
|
||||
dist/
|
||||
|
@ -1,2 +1,4 @@
|
||||
dist/**
|
||||
node_modules/**
|
||||
dist/
|
||||
node_modules/
|
||||
README.md
|
||||
app/
|
||||
|
@ -31,7 +31,7 @@ SvelteKitAuth also comes with first-class support for Typescript out of the box,
|
||||
|
||||
SvelteKitAuth is very easy to setup! All you need to do is instantiate the `SvelteKitAuth` class, and configure it with some default providers, as well as a JWT secret key used to verify the cookies:
|
||||
|
||||
_**Warning**: env variables prefixed with `VITE_` can be exposed and leaked into client-side bundles if they are referenced in any client-side code. Make sure this is not the case, or consider using an alternative method such as loading them via dotenv directly instead._
|
||||
***Warning**: env variables prefixed with `VITE_` can be exposed and leaked into client-side bundles if they are referenced in any client-side code. Make sure this is not the case, or consider using an alternative method such as loading them via dotenv directly instead.*
|
||||
|
||||
```ts
|
||||
export const appAuth = new SvelteKitAuth({
|
||||
@ -144,6 +144,10 @@ SvelteKitAuth is inspired by the [NextAuth.js](https://next-auth.js.org/) packag
|
||||
|
||||
As it leverages classes and Typescript, the implementation of such providers is very straightforward, and in the future it will even be possible to register multiple SvelteKitAuth handlers in the same project, should the need arise, by leveraging a class-based client and server setup.
|
||||
|
||||
## Examples
|
||||
|
||||
Looking for help? Check out the [example app](./app/) in the repository source. Make something cool you want to show off? Share it with others [in the discussion section](https://github.com/Dan6erbond/sk-auth/discussions/72).
|
||||
|
||||
## Contributing
|
||||
|
||||
🚧 Work in Progress!
|
||||
|
6
app/.eslintignore
Normal file
6
app/.eslintignore
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
README.md
|
||||
*.cjs
|
||||
.svelte-kit/
|
||||
static/
|
||||
build/
|
@ -1,20 +1,15 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
extends: ["../.eslintrc.cjs"],
|
||||
plugins: ["svelte3", "@typescript-eslint"],
|
||||
ignorePatterns: ["*.cjs"],
|
||||
overrides: [{ files: ["*.svelte"], processor: "svelte3/svelte3" }],
|
||||
extends: ["../.eslintrc-shared.cjs"],
|
||||
plugins: ["svelte3"],
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.svelte"],
|
||||
processor: "svelte3/svelte3",
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
"svelte3/typescript": () => require("typescript"),
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaVersion: 2019,
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true,
|
||||
},
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
.svelte-kit/**
|
||||
static/**
|
||||
build/**
|
||||
node_modules/**
|
||||
.svelte-kit/
|
||||
static/
|
||||
build/
|
||||
node_modules/
|
||||
|
@ -5,11 +5,11 @@
|
||||
"dev": "svelte-kit dev",
|
||||
"build": "svelte-kit build",
|
||||
"preview": "svelte-kit preview",
|
||||
"lint": "prettier --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
|
||||
"format": "prettier --write --plugin-search-dir=. ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/kit": "next",
|
||||
"@sveltejs/kit": "^1.0.0-next.259",
|
||||
"@types/prismjs": "^1.16.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.19.0",
|
||||
"@typescript-eslint/parser": "^4.19.0",
|
||||
@ -33,6 +33,7 @@
|
||||
"dependencies": {
|
||||
"@fontsource/fira-mono": "^4.3.0",
|
||||
"@fontsource/inter": "^4.3.0",
|
||||
"clipboard": "^2.0.8",
|
||||
"clsx": "^1.1.1",
|
||||
"prismjs": "^1.23.0",
|
||||
"sk-auth": "file:../"
|
||||
|
2
app/src/global.d.ts
vendored
2
app/src/global.d.ts
vendored
@ -11,5 +11,7 @@ interface ImportMetaEnv {
|
||||
VITE_TWITTER_API_SECRET: string;
|
||||
VITE_REDDIT_API_KEY: string;
|
||||
VITE_REDDIT_API_SECRET: string;
|
||||
VITE_SPOTIFY_CLIENT_ID: string;
|
||||
VITE_SPOTIFY_CLIENT_SECRET: string;
|
||||
VITE_JWT_SECRET_KEY: string;
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
import type { Handle } from "@sveltejs/kit";
|
||||
import { appAuth } from "$lib/appAuth";
|
||||
|
||||
export const handle: Handle = async ({ request, render }) => {
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// TODO https://github.com/sveltejs/kit/issues/1046
|
||||
if (request.query.has("_method")) {
|
||||
request.method = request.query.get("_method").toUpperCase();
|
||||
|
||||
|
||||
if (event.request.query.has("_method")) {
|
||||
event.request.method = event.request.query.get("_method").toUpperCase();
|
||||
}
|
||||
|
||||
const response = await render(request);
|
||||
const response = await resolve(event);
|
||||
|
||||
return response;
|
||||
};
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
GoogleOAuth2Provider,
|
||||
RedditOAuth2Provider,
|
||||
TwitterAuthProvider,
|
||||
SpotifyOAuth2Provider,
|
||||
} from "sk-auth/providers";
|
||||
|
||||
export const appAuth = new SvelteKitAuth({
|
||||
@ -45,6 +46,13 @@ export const appAuth = new SvelteKitAuth({
|
||||
return { ...slim, provider: "reddit" };
|
||||
},
|
||||
}),
|
||||
new SpotifyOAuth2Provider({
|
||||
clientId: import.meta.env.VITE_SPOTIFY_CLIENT_ID,
|
||||
clientSecret: import.meta.env.VITE_SPOTIFY_CLIENT_SECRET,
|
||||
profile(profile) {
|
||||
return { ...profile, provider: "spotify" };
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
jwt(token, profile) {
|
||||
|
@ -180,6 +180,38 @@
|
||||
<span>Sign in with Reddit</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/api/auth/signin/spotify?redirect=/profile"
|
||||
class={clsx(
|
||||
"text-sm",
|
||||
"md:text-base",
|
||||
"inline-flex",
|
||||
"space-x-4",
|
||||
"py-2",
|
||||
"px-4",
|
||||
"border-gray-400",
|
||||
"rounded",
|
||||
"hover:no-underline",
|
||||
"border",
|
||||
"hover:bg-gray-100",
|
||||
"transition-colors",
|
||||
"items-center",
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
viewBox="0 0 168 168"
|
||||
class={clsx("h-4", "w-4", "md:h-6", "md:w-6")}
|
||||
>
|
||||
<path
|
||||
fill="#1ED760"
|
||||
d="m83.996 0.277c-46.249 0-83.743 37.493-83.743 83.742 0 46.251 37.494 83.741 83.743 83.741 46.254 0 83.744-37.49 83.744-83.741 0-46.246-37.49-83.738-83.745-83.738l0.001-0.004zm38.404 120.78c-1.5 2.46-4.72 3.24-7.18 1.73-19.662-12.01-44.414-14.73-73.564-8.07-2.809 0.64-5.609-1.12-6.249-3.93-0.643-2.81 1.11-5.61 3.926-6.25 31.9-7.291 59.263-4.15 81.337 9.34 2.46 1.51 3.24 4.72 1.73 7.18zm10.25-22.805c-1.89 3.075-5.91 4.045-8.98 2.155-22.51-13.839-56.823-17.846-83.448-9.764-3.453 1.043-7.1-0.903-8.148-4.35-1.04-3.453 0.907-7.093 4.354-8.143 30.413-9.228 68.222-4.758 94.072 11.127 3.07 1.89 4.04 5.91 2.15 8.976v-0.001zm0.88-23.744c-26.99-16.031-71.52-17.505-97.289-9.684-4.138 1.255-8.514-1.081-9.768-5.219-1.254-4.14 1.08-8.513 5.221-9.771 29.581-8.98 78.756-7.245 109.83 11.202 3.73 2.209 4.95 7.016 2.74 10.733-2.2 3.722-7.02 4.949-10.73 2.739z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Sign in with Spotify</span>
|
||||
</a>
|
||||
|
||||
<p class={clsx("text-gray-600", "text-center", "border-gray-400", "border-b", "pb-2")}>
|
||||
Coming soon.
|
||||
</p>
|
||||
|
@ -201,6 +201,47 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class={clsx("flex", "items-center", "space-x-4")}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
viewBox="0 0 168 168"
|
||||
class={clsx("h-4", "w-4", "md:h-6", "md:w-6")}
|
||||
>
|
||||
<path
|
||||
fill="#1ED760"
|
||||
d="m83.996 0.277c-46.249 0-83.743 37.493-83.743 83.742 0 46.251 37.494 83.741 83.743 83.741 46.254 0 83.744-37.49 83.744-83.741 0-46.246-37.49-83.738-83.745-83.738l0.001-0.004zm38.404 120.78c-1.5 2.46-4.72 3.24-7.18 1.73-19.662-12.01-44.414-14.73-73.564-8.07-2.809 0.64-5.609-1.12-6.249-3.93-0.643-2.81 1.11-5.61 3.926-6.25 31.9-7.291 59.263-4.15 81.337 9.34 2.46 1.51 3.24 4.72 1.73 7.18zm10.25-22.805c-1.89 3.075-5.91 4.045-8.98 2.155-22.51-13.839-56.823-17.846-83.448-9.764-3.453 1.043-7.1-0.903-8.148-4.35-1.04-3.453 0.907-7.093 4.354-8.143 30.413-9.228 68.222-4.758 94.072 11.127 3.07 1.89 4.04 5.91 2.15 8.976v-0.001zm0.88-23.744c-26.99-16.031-71.52-17.505-97.289-9.684-4.138 1.255-8.514-1.081-9.768-5.219-1.254-4.14 1.08-8.513 5.221-9.771 29.581-8.98 78.756-7.245 109.83 11.202 3.73 2.209 4.95 7.016 2.74 10.733-2.2 3.722-7.02 4.949-10.73 2.739z"
|
||||
/>
|
||||
</svg>
|
||||
<div class={clsx("flex", "flex-col", "items-start", "space-y-1")}>
|
||||
{#if $session.user.connections.spotify}
|
||||
<p class={clsx("font-bold")}>Signed in as:</p>
|
||||
|
||||
{$session.user.connections.spotify.display_name}
|
||||
{:else}
|
||||
<p class={clsx("font-bold")}>Not signed in</p>
|
||||
<div
|
||||
class={clsx(
|
||||
"text-xs",
|
||||
"md:text-sm",
|
||||
"py-1",
|
||||
"px-2",
|
||||
"border-gray-400",
|
||||
"rounded",
|
||||
"hover:no-underline",
|
||||
"border",
|
||||
"hover:bg-gray-100",
|
||||
"transition-colors",
|
||||
"items-center",
|
||||
"cursor-not-allowed",
|
||||
"inline-block",
|
||||
)}
|
||||
>
|
||||
Connect
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class={clsx("text-lg", "mb-2")}>Session</p>
|
||||
|
3308
app/yarn.lock
3308
app/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sk-auth",
|
||||
"version": "0.3.7",
|
||||
"version": "0.4.1",
|
||||
"description": "Authentication library for use with SvelteKit featuring built-in OAuth providers and zero restriction customization!",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.esm.js",
|
||||
@ -18,10 +18,11 @@
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "npm run build",
|
||||
"build": "rollup --config",
|
||||
"dev": "rollup --config --watch",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"lint": "prettier --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
|
||||
"format": "prettier --write --plugin-search-dir=. ."
|
||||
},
|
||||
"keywords": [
|
||||
@ -47,7 +48,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@sveltejs/kit": "^1.0.0-next.107",
|
||||
"@sveltejs/kit": "^1.0.0-next.259",
|
||||
"@types/jsonwebtoken": "^8.5.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
||||
"@typescript-eslint/parser": "^4.23.0",
|
||||
|
67
src/auth.ts
67
src/auth.ts
@ -1,6 +1,6 @@
|
||||
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 type { EndpointOutput } from "@sveltejs/kit/types/endpoint";
|
||||
import { RequestEvent } from "@sveltejs/kit/types/hooks";
|
||||
import cookie from "cookie";
|
||||
import * as jsonwebtoken from "jsonwebtoken";
|
||||
import type { JWT, Session } from "./interfaces";
|
||||
@ -13,6 +13,7 @@ interface AuthConfig {
|
||||
jwtSecret?: string;
|
||||
jwtExpiresIn?: string | number;
|
||||
host?: string;
|
||||
protocol?: string;
|
||||
basePath?: string;
|
||||
}
|
||||
|
||||
@ -43,12 +44,12 @@ export class Auth {
|
||||
return "svelte_auth_secret";
|
||||
}
|
||||
|
||||
async getToken(headers: Headers) {
|
||||
if (!headers.cookie) {
|
||||
async getToken(headers: any) {
|
||||
if (!headers.get("cookie")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cookies = cookie.parse(headers.cookie);
|
||||
const cookies = cookie.parse(headers.get("cookie"));
|
||||
|
||||
if (!cookies.svelteauthjwt) {
|
||||
return null;
|
||||
@ -69,7 +70,9 @@ export class Auth {
|
||||
}
|
||||
|
||||
getBaseUrl(host?: string) {
|
||||
return this.config?.host ?? `http://${host}`;
|
||||
const protocol = this.config?.protocol ?? "https";
|
||||
host = this.config?.host ?? host;
|
||||
return `${protocol}://${host}`;
|
||||
}
|
||||
|
||||
getPath(path: string) {
|
||||
@ -82,7 +85,7 @@ export class Auth {
|
||||
return new URL(pathname, this.getBaseUrl(host)).href;
|
||||
}
|
||||
|
||||
setToken(headers: Headers, newToken: JWT | any) {
|
||||
setToken(headers: any, newToken: JWT | any) {
|
||||
const originalToken = this.getToken(headers);
|
||||
|
||||
return {
|
||||
@ -109,12 +112,10 @@ export class Auth {
|
||||
return redirect;
|
||||
}
|
||||
|
||||
async handleProviderCallback(
|
||||
request: ServerRequest,
|
||||
provider: Provider,
|
||||
): Promise<EndpointOutput> {
|
||||
const { headers, host } = request;
|
||||
const [profile, redirectUrl] = await provider.callback(request, this);
|
||||
async handleProviderCallback(event: RequestEvent, provider: Provider): Promise<EndpointOutput> {
|
||||
const { headers } = event.request;
|
||||
const { url } = event;
|
||||
const [profile, redirectUrl] = await provider.callback(event, this);
|
||||
|
||||
let token = (await this.getToken(headers)) ?? { user: {} };
|
||||
if (this.config?.callbacks?.jwt) {
|
||||
@ -124,7 +125,7 @@ export class Auth {
|
||||
}
|
||||
|
||||
const jwt = this.signToken(token);
|
||||
const redirect = await this.getRedirectUrl(host, redirectUrl ?? undefined);
|
||||
const redirect = await this.getRedirectUrl(url.host, redirectUrl ?? undefined);
|
||||
|
||||
return {
|
||||
status: 302,
|
||||
@ -135,11 +136,12 @@ export class Auth {
|
||||
};
|
||||
}
|
||||
|
||||
async handleEndpoint(request: ServerRequest): Promise<EndpointOutput> {
|
||||
const { path, headers, method, host } = request;
|
||||
async handleEndpoint(event: RequestEvent): Promise<EndpointOutput> {
|
||||
const { headers, method } = event.request;
|
||||
const { url } = event;
|
||||
|
||||
if (path === this.getPath("signout")) {
|
||||
const token = this.setToken(headers, {});
|
||||
if (url.pathname === this.getPath("signout")) {
|
||||
const token = this.setToken(event.request.headers, {});
|
||||
const jwt = this.signToken(token);
|
||||
|
||||
if (method === "POST") {
|
||||
@ -153,7 +155,7 @@ export class Auth {
|
||||
};
|
||||
}
|
||||
|
||||
const redirect = await this.getRedirectUrl(host);
|
||||
const redirect = await this.getRedirectUrl(url.host);
|
||||
|
||||
return {
|
||||
status: 302,
|
||||
@ -165,7 +167,7 @@ export class Auth {
|
||||
}
|
||||
|
||||
const regex = new RegExp(join([this.basePath, `(?<method>signin|callback)/(?<provider>\\w+)`]));
|
||||
const match = path.match(regex);
|
||||
const match = url.pathname.match(regex);
|
||||
|
||||
if (match && match.groups) {
|
||||
const provider = this.config?.providers?.find(
|
||||
@ -173,9 +175,9 @@ export class Auth {
|
||||
);
|
||||
if (provider) {
|
||||
if (match.groups.method === "signin") {
|
||||
return await provider.signin(request, this);
|
||||
return await provider.signin(event, this);
|
||||
} else {
|
||||
return await this.handleProviderCallback(request, provider);
|
||||
return await this.handleProviderCallback(event, provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -186,13 +188,13 @@ export class Auth {
|
||||
};
|
||||
}
|
||||
|
||||
get: RequestHandler = async (request) => {
|
||||
const { path } = request;
|
||||
get: RequestHandler = async (event: RequestEvent): Promise<any> => {
|
||||
const { url } = event;
|
||||
|
||||
if (path === this.getPath("csrf")) {
|
||||
if (url.pathname === this.getPath("csrf")) {
|
||||
return { body: "1234" }; // TODO: Generate real token
|
||||
} else if (path === this.getPath("session")) {
|
||||
const session = await this.getSession(request);
|
||||
} else if (url.pathname === this.getPath("session")) {
|
||||
const session = await this.getSession(event);
|
||||
return {
|
||||
body: {
|
||||
session,
|
||||
@ -200,15 +202,16 @@ export class Auth {
|
||||
};
|
||||
}
|
||||
|
||||
return await this.handleEndpoint(request);
|
||||
return await this.handleEndpoint(event);
|
||||
};
|
||||
|
||||
post: RequestHandler = async (request) => {
|
||||
return await this.handleEndpoint(request);
|
||||
post: RequestHandler = async (event: RequestEvent) => {
|
||||
return await this.handleEndpoint(event);
|
||||
};
|
||||
|
||||
getSession: GetSession = async ({ headers }) => {
|
||||
const token = await this.getToken(headers);
|
||||
getSession: GetSession = async (event: RequestEvent) => {
|
||||
const { request } = event;
|
||||
const token = await this.getToken(request.headers);
|
||||
|
||||
if (token) {
|
||||
if (this.config?.callbacks?.session) {
|
||||
|
19
src/client/helpers.ts
Normal file
19
src/client/helpers.ts
Normal file
@ -0,0 +1,19 @@
|
||||
function mergePath(basePaths: (string | null)[], path: string) {
|
||||
if (path.startsWith("/")) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
let retPath;
|
||||
for (let basePath of basePaths) {
|
||||
if (basePath !== null) {
|
||||
if (!basePath.startsWith("/")) {
|
||||
basePath = "/" + basePath;
|
||||
}
|
||||
if (!basePath.endsWith("/")) {
|
||||
basePath = basePath + "/";
|
||||
}
|
||||
retPath = basePath + path;
|
||||
}
|
||||
}
|
||||
|
||||
return retPath;
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
/* import { goto } from "@sveltejs/kit/assets/runtime/app/navigation";
|
||||
import { page } from "@sveltejs/kit/assets/runtime/app/stores"; */
|
||||
import type { Page } from "@sveltejs/kit";
|
||||
import type { LoadInput } from "@sveltejs/kit";
|
||||
import type { ClientRequestConfig } from "./types";
|
||||
|
||||
interface SignInConfig {
|
||||
interface SignInConfig extends ClientRequestConfig {
|
||||
redirectUrl?: string;
|
||||
}
|
||||
|
||||
export async function signIn(provider: string, data?: any, config?: SignInConfig) {
|
||||
if (data) {
|
||||
const path = `/api/auth/callback/${provider}`;
|
||||
const path = mergePath(["/api/auth", config?.basePath ?? null], `/callback/${provider}`);
|
||||
const res = await fetch(path, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -23,10 +24,10 @@ export async function signIn(provider: string, data?: any, config?: SignInConfig
|
||||
if (config?.redirectUrl) {
|
||||
redirectUrl = config.redirectUrl;
|
||||
} else {
|
||||
let $val: Page | undefined;
|
||||
let $val: LoadInput | undefined;
|
||||
/* page.subscribe(($) => ($val = $))(); */
|
||||
if ($val) {
|
||||
redirectUrl = `${$val.host}${$val.path}?${$val.query}`;
|
||||
redirectUrl = `${$val.url.host}${$val.url.pathname}?${$val.url.searchParams}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +35,7 @@ export async function signIn(provider: string, data?: any, config?: SignInConfig
|
||||
redirect: redirectUrl ?? "/",
|
||||
};
|
||||
const query = new URLSearchParams(queryData);
|
||||
const path = `/api/auth/login/${provider}?${query}`;
|
||||
const path = mergePath(["/api/auth", config?.basePath ?? null], `/signin/${provider}?${query}`);
|
||||
|
||||
return path; // await goto(path);
|
||||
}
|
||||
|
@ -1,14 +1,18 @@
|
||||
/* import { session as session$ } from "$app/stores"; */
|
||||
|
||||
export async function signOut() {
|
||||
let res = await fetch("/api/auth/signout", { method: "POST" });
|
||||
import type { ClientRequestConfig } from "./types";
|
||||
|
||||
export async function signOut(config?: ClientRequestConfig) {
|
||||
let res = await fetch(mergePath(["/api/auth", config?.basePath ?? null], "signout"), {
|
||||
method: "POST",
|
||||
});
|
||||
const { signout } = await res.json();
|
||||
|
||||
if (!signout) {
|
||||
throw new Error("Sign out not successful!");
|
||||
}
|
||||
|
||||
res = await fetch("/api/auth/session");
|
||||
res = await fetch(mergePath(["/api/auth", config?.basePath ?? null], "session"));
|
||||
const session = await res.json();
|
||||
|
||||
return session;
|
||||
|
3
src/client/types.ts
Normal file
3
src/client/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface ClientRequestConfig {
|
||||
basePath?: string;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import type { EndpointOutput } from "@sveltejs/kit";
|
||||
import type { ServerRequest } from "@sveltejs/kit/types/endpoint";
|
||||
import { RequestEvent } from "@sveltejs/kit/types/hooks";
|
||||
import type { Auth } from "../auth";
|
||||
import type { CallbackResult } from "../types";
|
||||
|
||||
@ -28,12 +28,12 @@ export abstract class Provider<T extends ProviderConfig = ProviderConfig> {
|
||||
}
|
||||
|
||||
abstract signin<Locals extends Record<string, any> = Record<string, any>, Body = unknown>(
|
||||
request: ServerRequest<Locals, Body>,
|
||||
event: RequestEvent,
|
||||
svelteKitAuth: Auth,
|
||||
): EndpointOutput | Promise<EndpointOutput>;
|
||||
|
||||
abstract callback<Locals extends Record<string, any> = Record<string, any>, Body = unknown>(
|
||||
request: ServerRequest<Locals, Body>,
|
||||
event: RequestEvent,
|
||||
svelteKitAuth: Auth,
|
||||
): CallbackResult | Promise<CallbackResult>;
|
||||
}
|
||||
|
50
src/providers/github.ts
Normal file
50
src/providers/github.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { OAuth2Provider, OAuth2ProviderConfig } from "./oauth2";
|
||||
|
||||
export interface GitHubProfile {
|
||||
id: number;
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
url: string;
|
||||
name: string;
|
||||
// complete with more info
|
||||
}
|
||||
|
||||
export interface GitHubTokens {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
}
|
||||
|
||||
type GitHubOAuth2ProviderConfig = OAuth2ProviderConfig<GitHubProfile, GitHubTokens>;
|
||||
|
||||
const defaultConfig: Partial<GitHubOAuth2ProviderConfig> = {
|
||||
id: "github",
|
||||
scope: "user",
|
||||
accessTokenUrl: "https://github.com/login/oauth/access_token",
|
||||
authorizationUrl: "https://github.com/login/oauth/authorize",
|
||||
profileUrl: "https://api.github.com/user",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
export class GitHubOAuth2Provider extends OAuth2Provider<
|
||||
GitHubProfile,
|
||||
GitHubTokens,
|
||||
GitHubOAuth2ProviderConfig
|
||||
> {
|
||||
constructor(config: GitHubOAuth2ProviderConfig) {
|
||||
super({
|
||||
...defaultConfig,
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
async getUserProfile(tokens: GitHubTokens): Promise<GitHubProfile> {
|
||||
const tokenType = "token"; // 🤷♂️ token type returned is "bearer" but GitHub uses "token" keyword
|
||||
const res = await fetch(this.config.profileUrl!, {
|
||||
headers: { Authorization: `${tokenType} ${tokens.access_token}` },
|
||||
});
|
||||
return await res.json();
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
export { Provider } from "./base";
|
||||
export { TwitchOAuth2Provider } from "./twitch";
|
||||
export type { TwitchProfile, TwitchTokens } from "./twitch";
|
||||
export { GitHubOAuth2Provider } from "./github";
|
||||
export type { GitHubProfile, GitHubTokens } from "./github";
|
||||
export { GoogleOAuth2Provider } from "./google";
|
||||
export type { GoogleProfile, GoogleTokens } from "./google";
|
||||
export { TwitterAuthProvider } from "./twitter";
|
||||
export { FacebookOAuth2Provider } from "./facebook";
|
||||
export type { FacebookProfile, FacebookTokens } from "./facebook";
|
||||
export { OAuth2BaseProvider } from "./oauth2.base";
|
||||
@ -13,3 +12,8 @@ export { OktaOAuth2Provider } from "./okta";
|
||||
export type { OktaProfile, OktaTokens, OktaAddress } from "./okta";
|
||||
export { RedditOAuth2Provider } from "./reddit";
|
||||
export type { RedditProfile, RedditTokens } from "./reddit";
|
||||
export { SpotifyOAuth2Provider } from "./spotify";
|
||||
export type { SpotifyProfile, SpotifyTokens } from "./spotify";
|
||||
export { TwitchOAuth2Provider } from "./twitch";
|
||||
export type { TwitchProfile, TwitchTokens } from "./twitch";
|
||||
export { TwitterAuthProvider } from "./twitter";
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type { EndpointOutput, ServerRequest } from "@sveltejs/kit/types/endpoint";
|
||||
import type { EndpointOutput } from "@sveltejs/kit/types/endpoint";
|
||||
import { RequestEvent } from "@sveltejs/kit/types/hooks";
|
||||
import type { Auth } from "../auth";
|
||||
import type { CallbackResult } from "../types";
|
||||
import { Provider, ProviderConfig } from "./base";
|
||||
@ -24,7 +25,7 @@ export abstract class OAuth2BaseProvider<
|
||||
T extends OAuth2BaseProviderConfig,
|
||||
> extends Provider<T> {
|
||||
abstract getAuthorizationUrl(
|
||||
request: ServerRequest,
|
||||
event: RequestEvent,
|
||||
auth: Auth,
|
||||
state: string,
|
||||
nonce: string,
|
||||
@ -32,17 +33,20 @@ export abstract class OAuth2BaseProvider<
|
||||
abstract getTokens(code: string, redirectUri: string): TokensType | Promise<TokensType>;
|
||||
abstract getUserProfile(tokens: any): ProfileType | Promise<ProfileType>;
|
||||
|
||||
async signin(request: ServerRequest, auth: Auth): Promise<EndpointOutput> {
|
||||
const { method, host, query } = request;
|
||||
const state = [`redirect=${query.get("redirect") ?? this.getUri(auth, "/", host)}`].join(",");
|
||||
async signin(event: RequestEvent, auth: Auth): Promise<EndpointOutput> {
|
||||
const { method } = event.request;
|
||||
const { url } = event;
|
||||
const state = [
|
||||
`redirect=${url.searchParams.get("redirect") ?? this.getUri(auth, "/", url.host)}`,
|
||||
].join(",");
|
||||
const base64State = Buffer.from(state).toString("base64");
|
||||
const nonce = Math.round(Math.random() * 1000).toString(); // TODO: Generate random based on user values
|
||||
const url = await this.getAuthorizationUrl(request, auth, base64State, nonce);
|
||||
const authUrl = await this.getAuthorizationUrl(event, auth, base64State, nonce);
|
||||
|
||||
if (method === "POST") {
|
||||
return {
|
||||
body: {
|
||||
redirect: url,
|
||||
redirect: authUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -50,7 +54,7 @@ export abstract class OAuth2BaseProvider<
|
||||
return {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: url,
|
||||
Location: authUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -65,17 +69,18 @@ export abstract class OAuth2BaseProvider<
|
||||
}
|
||||
}
|
||||
|
||||
async callback({ query, host }: ServerRequest, auth: Auth): Promise<CallbackResult> {
|
||||
const code = query.get("code");
|
||||
const redirect = this.getStateValue(query, "redirect");
|
||||
async callback(event: RequestEvent, auth: Auth): Promise<any> {
|
||||
const { request, url } = event;
|
||||
const code = url.searchParams.get("code");
|
||||
const redirect = this.getStateValue(url.searchParams, "redirect");
|
||||
|
||||
const tokens = await this.getTokens(code!, this.getCallbackUri(auth, host));
|
||||
const tokens = await this.getTokens(code!, this.getCallbackUri(auth, url.host));
|
||||
let user = await this.getUserProfile(tokens);
|
||||
|
||||
if (this.config.profile) {
|
||||
user = await this.config.profile(user, tokens);
|
||||
}
|
||||
|
||||
return [user, redirect ?? this.getUri(auth, "/", host)];
|
||||
return [user, redirect ?? this.getUri(auth, "/", url.host)];
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { ServerRequest } from "@sveltejs/kit/types/endpoint";
|
||||
import { RequestEvent } from "@sveltejs/kit/types/hooks";
|
||||
import type { Auth } from "../auth";
|
||||
import { ucFirst } from "../helpers";
|
||||
import { OAuth2BaseProvider, OAuth2BaseProviderConfig, OAuth2Tokens } from "./oauth2.base";
|
||||
@ -37,19 +37,19 @@ export class OAuth2Provider<
|
||||
});
|
||||
}
|
||||
|
||||
getAuthorizationUrl({ host }: ServerRequest, auth: Auth, state: string, nonce: string) {
|
||||
getAuthorizationUrl({ url }: RequestEvent, auth: Auth, state: string, nonce: string) {
|
||||
const data = {
|
||||
state,
|
||||
nonce,
|
||||
response_type: this.config.responseType,
|
||||
client_id: this.config.clientId,
|
||||
scope: Array.isArray(this.config.scope) ? this.config.scope.join(" ") : this.config.scope!,
|
||||
redirect_uri: this.getCallbackUri(auth, host),
|
||||
redirect_uri: this.getCallbackUri(auth, url.host),
|
||||
...(this.config.authorizationParams ?? {}),
|
||||
};
|
||||
|
||||
const url = `${this.config.authorizationUrl}?${new URLSearchParams(data)}`;
|
||||
return url;
|
||||
const authUrl = `${this.config.authorizationUrl}?${new URLSearchParams(data)}`;
|
||||
return authUrl;
|
||||
}
|
||||
|
||||
async getTokens(code: string, redirectUri: string): Promise<TokensType> {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { OAuth2Provider, OAuth2ProviderConfig } from "./oauth2";
|
||||
import type { ProfileCallback } from "./oauth2.base";
|
||||
|
||||
export interface RedditProfile {
|
||||
is_employee: boolean;
|
||||
|
73
src/providers/spotify.ts
Normal file
73
src/providers/spotify.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { OAuth2Provider, OAuth2ProviderConfig } from "./oauth2";
|
||||
|
||||
export interface SpotifyProfile {
|
||||
display_name: string;
|
||||
email: string;
|
||||
external_urls: SpotifyProfileExternalUrls;
|
||||
followers: SpotifyProfileFollowers;
|
||||
href: string;
|
||||
id: string;
|
||||
images: SpotifyProfileImage[];
|
||||
type: string;
|
||||
uri: string;
|
||||
// This field is only available when the current user has granted access to the user-read-private scope.
|
||||
explicit_content?: SpotifyExplicitContent;
|
||||
// This field is only available when the current user has granted access to the user-read-private scope.
|
||||
product?: string;
|
||||
// This field is only available when the current user has granted access to the user-read-private scope.
|
||||
country?: string;
|
||||
}
|
||||
|
||||
export interface SpotifyExplicitContent {
|
||||
filter_enabled: boolean;
|
||||
filter_locked: boolean;
|
||||
}
|
||||
|
||||
export interface SpotifyProfileImage {
|
||||
height: number;
|
||||
url: string;
|
||||
width: string;
|
||||
}
|
||||
|
||||
export interface SpotifyProfileFollowers {
|
||||
href: string;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface SpotifyProfileExternalUrls {
|
||||
spotify: string;
|
||||
}
|
||||
|
||||
export interface SpotifyTokens {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
interface SpotifyOAuth2ProviderConfig extends OAuth2ProviderConfig<SpotifyProfile, SpotifyTokens> {
|
||||
show_dialog: boolean;
|
||||
}
|
||||
|
||||
const defaultConfig: Partial<SpotifyOAuth2ProviderConfig> = {
|
||||
id: "spotify",
|
||||
scope: "user-read-email",
|
||||
accessTokenUrl: "https://accounts.spotify.com/api/token",
|
||||
authorizationUrl: "https://accounts.spotify.com/authorize",
|
||||
profileUrl: "https://api.spotify.com/v1/me",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
};
|
||||
|
||||
export class SpotifyOAuth2Provider extends OAuth2Provider<
|
||||
SpotifyProfile,
|
||||
SpotifyTokens,
|
||||
SpotifyOAuth2ProviderConfig
|
||||
> {
|
||||
constructor(config: SpotifyOAuth2ProviderConfig) {
|
||||
super({
|
||||
...defaultConfig,
|
||||
...config,
|
||||
});
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import type { ServerRequest } from "@sveltejs/kit/types/endpoint";
|
||||
import { RequestEvent } from "@sveltejs/kit/types/hooks";
|
||||
import type { Auth } from "../auth";
|
||||
import type { CallbackResult } from "../types";
|
||||
import { OAuth2BaseProvider, OAuth2BaseProviderConfig } from "./oauth2.base";
|
||||
|
||||
interface TwitterAuthProviderConfig extends OAuth2BaseProviderConfig {
|
||||
@ -38,17 +37,17 @@ export class TwitterAuthProvider extends OAuth2BaseProvider<any, any, TwitterAut
|
||||
};
|
||||
}
|
||||
|
||||
async getAuthorizationUrl({ host }: ServerRequest, auth: Auth, state: string, nonce: string) {
|
||||
async getAuthorizationUrl({ url }: RequestEvent, auth: Auth, state: string, nonce: string) {
|
||||
const endpoint = "https://api.twitter.com/oauth/authorize";
|
||||
|
||||
const { oauthToken } = await this.getRequestToken(auth, host);
|
||||
const { oauthToken } = await this.getRequestToken(auth, url.host);
|
||||
|
||||
const data = {
|
||||
oauth_token: oauthToken,
|
||||
};
|
||||
|
||||
const url = `${endpoint}?${new URLSearchParams(data)}`;
|
||||
return url;
|
||||
const authUrl = `${endpoint}?${new URLSearchParams(data)}`;
|
||||
return authUrl;
|
||||
}
|
||||
|
||||
async getTokens(oauthToken: string, oauthVerifier: string) {
|
||||
@ -71,10 +70,11 @@ export class TwitterAuthProvider extends OAuth2BaseProvider<any, any, TwitterAut
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
async callback({ query, host }: ServerRequest, auth: Auth): Promise<CallbackResult> {
|
||||
const oauthToken = query.get("oauth_token");
|
||||
const oauthVerifier = query.get("oauth_verifier");
|
||||
const redirect = this.getStateValue(query, "redirect");
|
||||
async callback(event: RequestEvent, auth: Auth): Promise<any> {
|
||||
const { url } = event;
|
||||
const oauthToken = url.searchParams.get("oauth_token");
|
||||
const oauthVerifier = url.searchParams.get("oauth_verifier");
|
||||
const redirect = this.getStateValue(url.searchParams, "redirect");
|
||||
|
||||
const tokens = await this.getTokens(oauthToken!, oauthVerifier!);
|
||||
let user = await this.getUserProfile(tokens);
|
||||
@ -83,6 +83,6 @@ export class TwitterAuthProvider extends OAuth2BaseProvider<any, any, TwitterAut
|
||||
user = await this.config.profile(user, tokens);
|
||||
}
|
||||
|
||||
return [user, redirect ?? this.getUri(auth, "/", host)];
|
||||
return [user, redirect ?? this.getUri(auth, "/", url.host)];
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,2 @@
|
||||
export type Profile = any;
|
||||
export type CallbackResult = [Profile, string | null];
|
||||
export type CallbackResult = [Profile, string | null, { error: string } | null];
|
||||
|
Loading…
Reference in New Issue
Block a user