diff --git a/.gitignore b/.gitignore index 7ec6c61..cd62174 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /dist/ /docs/ /types/ -/pnpm-lock.yaml \ No newline at end of file +/pnpm-lock.yaml +.idea/ diff --git a/package.json b/package.json index ec918ec..100119e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "OAuth2 authorization for Svelte", "author": "MacFJA", "license": "MIT", - "version": "1.1.0", + "version": "1.1.1", "bugs": { "url": "https://github.com/macfja/svelte-oauth2/issues" }, @@ -26,9 +26,9 @@ "build": "rollup -c", "lint": "eslint src/", "pretest:svelte": "rollup -c rollup.test.config.js", - "pretest:sveltekit": "npm run build", + "pretest:sveltekit": "pnpm run build", "test:svelte": "sirv tests/svelte", - "test:sveltekit": "cd tests/sk; npm run dev", + "test:sveltekit": "cd tests/sk; pnpm run dev", "prepublishOnly": "npm run build" }, "devDependencies": { @@ -73,6 +73,7 @@ "cookie": "^0.4.1", "js-base64": "^3.6.1", "js-cookie": "^3.0.1", - "pkce": "^1.0.0-beta2" + "pkce": "^1.0.0-beta2", + "vite": "^3.1.0-beta.1" } } diff --git a/rollup.config.js b/rollup.config.js index 2c1b46d..9016094 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,20 +1,21 @@ -import svelte from 'rollup-plugin-svelte'; -import resolve from '@rollup/plugin-node-resolve'; -import typescript from "@rollup/plugin-typescript"; -import commonjs from "@rollup/plugin-commonjs"; -import pkg from './package.json'; +import commonjs from "@rollup/plugin-commonjs" +import resolve from "@rollup/plugin-node-resolve" +import typescript from "@rollup/plugin-typescript" +import svelte from "rollup-plugin-svelte" + +import pkg from "./package.json" export default { - input: 'src/index.ts', + input: "src/index.ts", output: [ - { file: pkg.module, 'format': 'es' }, - { file: pkg.main, 'format': 'umd', name: 'Auth' } + { file: pkg.module, "format": "es" }, + { file: pkg.main, "format": "umd", name: "Auth" } ], - external: ['$app/navigation', '$app/stores', '$app/env'], + external: ["$app/navigation", "$app/stores", "$app/environment"], plugins: [ svelte(), typescript(), - commonjs({ignore: ['crypto']}), + commonjs({ignore: ["crypto"]}), resolve() ] -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/integration.ts b/src/integration.ts index 39d6cd5..7540a6a 100644 --- a/src/integration.ts +++ b/src/integration.ts @@ -1,11 +1,12 @@ -import type {ServerResponse} from "@sveltejs/kit/types/hooks" -import { get } from "svelte/store" +import { debug } from "svelte/internal" -import {getTokenStorageType} from "./oauth" -import type {TokenStorage} from "./oauth/tokenStorage" -import {browserCookie} from "./oauth/tokenStorage/browserCookie" -import {localStorage} from "./oauth/tokenStorage/localStorage" -import {getResponseCookie, serverCookie, setRequestCookies} from "./oauth/tokenStorage/serverCookie" +import { getTokenStorageType } from "./oauth" +import type { TokenStorage } from "./oauth/tokenStorage" +import { browserCookie } from "./oauth/tokenStorage/browserCookie" +import { localStorage } from "./oauth/tokenStorage/localStorage" +import { getResponseCookie, serverCookie, setRequestCookies } from "./oauth/tokenStorage/serverCookie" + +import { browser } from "$app/environment" const inMemoryStorage: Record = {} @@ -19,14 +20,7 @@ export interface ContextStrategy { * Redirect to an url * @param {string} url */ - redirect(url: string): Promise - - /** - * Get data from an URL (Fetch API) - * @param {string} uri The URI of the data - * @param {Record} [options] Fetch options - */ - fetch(uri:string, options?:Record): Promise, + redirect(url: string): Promise, /** * Get the storage where token is saved @@ -37,7 +31,7 @@ export interface ContextStrategy { * Get data from the temporary storage * @param {string} key The name/key of the data */ - getFromTemporary(key: string): Promise, + getFromTemporary(key: string): Promise, /** * Save data in the temporary storage @@ -50,17 +44,12 @@ export interface ContextStrategy { export const svelteKitStrategy: ContextStrategy = new class implements ContextStrategy { private fetchFunc private redirectedTo = null - private queryObject: URLSearchParams|null = null - - fetch(uri: string, options?: Record): Promise { - return this.fetchFunc(uri, options) - } + private queryObject: URLSearchParams | null = null async redirect(url: string): Promise { const navigation = await import("$app/navigation") - const env = await import("$app/env") - if (env.browser) { + if (browser) { return navigation.goto(url) } else { this.redirectedTo = url @@ -72,24 +61,22 @@ export const svelteKitStrategy: ContextStrategy = new class implements ContextSt if (this.queryObject !== null) { return Promise.resolve(this.queryObject) } - const stores = await import("$app/stores") - return get(stores.page).query + + // Old version + // const stores = await import("$app/stores") + // return get(stores.page).query + + // New version, except the page store is a Readable and can only be subscribed + // const page = getStores().page + // return page.url.searchParams } - getRedirection(): string|null { + getRedirection(): string | null { const redirection = this.redirectedTo + "" this.redirectedTo = null return redirection } - /** - * Set the fetch function to use - * @param {Function} func - */ - setFetch(func) { - this.fetchFunc = func - } - /** * Set the request Query * @param query @@ -99,62 +86,62 @@ export const svelteKitStrategy: ContextStrategy = new class implements ContextSt } async tokenStorage(): Promise { - const env = await import("$app/env") if (getTokenStorageType() === "cookie") { - return env.browser ? browserCookie : serverCookie + return browser ? browserCookie : serverCookie } return localStorage } /** * Handle hooks for SSR - * @param {import("@sveltejs/kit/types/hooks").ServerRequest} request The server request - * @param {Function} resolve The request resolver + * https://kit.svelte.dev/docs/types#sveltejs-kit-handle */ - async handleHook({request, resolve}) { - const env = await import("$app/env") + async handleHook({event, resolve}) { + if (getTokenStorageType() === "cookie" && !browser) { + setRequestCookies(event.request.headers["cookie"] || "") + } + + const response = await resolve(event) + + const cookies = getResponseCookie() + if (cookies !== "") { + let existing = response.headers["set-cookie"] || [] + if (typeof existing === "string") existing = [existing] + existing.push(cookies) + response.headers.set("set-cookie", existing) + } - if (getTokenStorageType() === "cookie" && !env.browser) { - setRequestCookies(request.headers["cookie"] || "") + // eslint-disable @typescript-eslint/ban-ts-comment + // @ts-ignore: Object is possibly 'undefined'. + const redirection = this.getRedirection() + + if (redirection !== null && redirection !== "null") { + return new Response(null, { + status: 302, + headers: { + ...response.headers, + location: redirection + } + }) } - /** @type {Promise} response */ - const response = resolve(request) - - return Promise.resolve(response).then((response: ServerResponse) => { - const cookies = getResponseCookie() - if (cookies !== "") { - let existing = response.headers["set-cookie"] || [] - if (typeof existing === "string") existing = [existing] - existing.push(cookies) - response.headers["set-cookie"] = existing - } - const redirection = this.getRedirection() - if (redirection !== null && redirection !== "null") { - response.status = 302 - response.headers.location = redirection - response.body = null - } - return response - }) + + return response } async getFromTemporary(key: string): Promise { - const env = await import("$app/env") - if (!env.browser) { + if (!browser) { return inMemoryStorage[key] || null } return window.sessionStorage.getItem(key) } async saveInTemporary(key: string, data: string) { - const env = await import("$app/env") - if (!env.browser) { + if (!browser) { inMemoryStorage[key] = data return } return window.sessionStorage.setItem(key, data) } - } export const browserStrategy: ContextStrategy = new class implements ContextStrategy { @@ -162,12 +149,11 @@ export const browserStrategy: ContextStrategy = new class implements ContextStra window.location.href = url return Promise.resolve() } + query(): Promise { return Promise.resolve(new URL(window.location.href).searchParams) } - fetch(uri: string, options?: Record): Promise { - return fetch(uri, options) - } + tokenStorage(): Promise { if (getTokenStorageType() === "cookie") { return Promise.resolve(browserCookie) diff --git a/src/oauth/grant.ts b/src/oauth/grant.ts index 426f1f3..48623ff 100644 --- a/src/oauth/grant.ts +++ b/src/oauth/grant.ts @@ -25,22 +25,26 @@ export interface Grant { export abstract class BaseGrant implements Grant { protected integration: ContextStrategy private tokenUri: string + protected headers: Headers - constructor(integration: ContextStrategy, tokenUri: string) { + constructor(integration: ContextStrategy, tokenUri: string, headers?: Headers) { this.integration = integration this.tokenUri = tokenUri + this.headers = headers || new Headers() } protected getToken(params: Record, headers: HeadersInit = {}): Promise { - const requestHeader = new Headers(headers) - requestHeader.set("content-type", "application/json") + const requestHeaders = new Headers(headers) + requestHeaders.set("content-type", "application/json") - return this.integration.fetch(this.tokenUri, { + return fetch(this.tokenUri, { method: "post", body: JSON.stringify(params), - headers: requestHeader + headers: requestHeaders }) - .then((response) => response.json()) + .then((response) => { + return response.json() + }) .then(async (response) => { if (Object.keys(response).includes("error")) { (await this.integration.tokenStorage()).set(null) @@ -50,6 +54,10 @@ export abstract class BaseGrant implements Grant { } return response }) + .catch(({reason, }) => { + console.log(`getToken failed: ${reason}`) + return Promise.resolve(false) + }) } onRequest(): Promise { diff --git a/src/oauth/grant/pkce.ts b/src/oauth/grant/pkce.ts index ba0bea2..fcbf1d7 100644 --- a/src/oauth/grant/pkce.ts +++ b/src/oauth/grant/pkce.ts @@ -5,6 +5,7 @@ import type {ContextStrategy} from "../../integration" import {ManInTheMiddle} from "../exception/ManInTheMiddle" import {BaseGrant} from "../grant" import type {Grant} from "../grant" +import { debug } from "svelte/internal"; export class AuthorizationCodePKCE extends BaseGrant implements Grant { @@ -20,6 +21,7 @@ export class AuthorizationCodePKCE extends BaseGrant implements Grant * @param {string} tokenUri The Auth Server URI where to get the access token. * @param {string} authorizationUri The Auth Server URI where to go for authentication. * @param {string} authorizationRedirectUri The application URI to go back from the Auth Server + * @param headers optional {Headers} Additional headers that will be passed as part of the bearer token request (e.g. 'X-API-Key') */ constructor( integration: ContextStrategy, @@ -27,9 +29,10 @@ export class AuthorizationCodePKCE extends BaseGrant implements Grant postLoginRedirectUri: string, tokenUri: string, authorizationUri: string, - authorizationRedirectUri: string + authorizationRedirectUri: string, + headers?: Headers ) { - super(integration, tokenUri) + super(integration, tokenUri, headers) this.authorizationRedirectUri = authorizationRedirectUri this.authorizationUri = authorizationUri this.clientId = clientId @@ -38,7 +41,7 @@ export class AuthorizationCodePKCE extends BaseGrant implements Grant async onRequest(): Promise { const params = await this.integration.query() - if (params.has("code") && params.has("state")) { + if (params?.has("code") && params?.has("state")) { const state = params.get("state") const code = params.get("code") @@ -48,18 +51,24 @@ export class AuthorizationCodePKCE extends BaseGrant implements Grant return this.getToken({ grant_type: "authorization_code", code: code, - client_id: this.clientId, + client_id: this.clientId, redirect_uri: this.postLoginRedirectUri, code_verifier: await this.integration.getFromTemporary("svelte-oauth-code-verifier") - }).then(async () => { + }, + this.headers + ).then(async () => { await this.integration.redirect(this.postLoginRedirectUri) return Promise.resolve(true) + }).catch(reason => { + console.log(reason) + return Promise.resolve(false) }) } return super.onRequest() } async onUnauthenticated(scopes: Array): Promise { await super.onUnauthenticated(scopes) + await this.integration.saveInTemporary("svelte-oauth-tries", "0") const url = new URL(this.authorizationUri) url.searchParams.append("response_type", "code") url.searchParams.append("scope", scopes.join(" ")) diff --git a/svelte.config.js b/svelte.config.js deleted file mode 100644 index b14355e..0000000 --- a/svelte.config.js +++ /dev/null @@ -1,5 +0,0 @@ -const sveltePreprocess = require("svelte-preprocess"); - -module.exports = { - preprocess: sveltePreprocess(), -}; \ No newline at end of file