Skip to content

DRAFT: Update library to incorporate recent SvelteKit breaking changes #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
/dist/
/docs/
/types/
/pnpm-lock.yaml
/pnpm-lock.yaml
.idea/
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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": {
Expand Down Expand Up @@ -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"
}
}
23 changes: 12 additions & 11 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -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()
]
};
}
124 changes: 55 additions & 69 deletions src/integration.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {}

Expand All @@ -19,14 +20,7 @@ export interface ContextStrategy {
* Redirect to an url
* @param {string} url
*/
redirect(url: string): Promise<void>

/**
* Get data from an URL (Fetch API)
* @param {string} uri The URI of the data
* @param {Record<string,any>} [options] Fetch options
*/
fetch(uri:string, options?:Record<string, unknown>): Promise<Response>,
redirect(url: string): Promise<void>,

/**
* Get the storage where token is saved
Expand All @@ -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<string|null>,
getFromTemporary(key: string): Promise<string | null>,

/**
* Save data in the temporary storage
Expand All @@ -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<string, unknown>): Promise<Response> {
return this.fetchFunc(uri, options)
}
private queryObject: URLSearchParams | null = null

async redirect(url: string): Promise<void> {
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
Expand All @@ -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
Expand All @@ -99,75 +86,74 @@ export const svelteKitStrategy: ContextStrategy = new class implements ContextSt
}

async tokenStorage(): Promise<TokenStorage> {
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<ServerResponse>} 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<string | null> {
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 {
redirect(url: string): Promise<void> {
window.location.href = url
return Promise.resolve()
}

query(): Promise<URLSearchParams> {
return Promise.resolve(new URL(window.location.href).searchParams)
}
fetch(uri: string, options?: Record<string, unknown>): Promise<Response> {
return fetch(uri, options)
}

tokenStorage(): Promise<TokenStorage> {
if (getTokenStorageType() === "cookie") {
return Promise.resolve(browserCookie)
Expand Down
20 changes: 14 additions & 6 deletions src/oauth/grant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>, headers: HeadersInit = {}): Promise<boolean> {
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)
Expand All @@ -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<boolean> {
Expand Down
19 changes: 14 additions & 5 deletions src/oauth/grant/pkce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -20,16 +21,18 @@ 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,
clientId: string,
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
Expand All @@ -38,7 +41,7 @@ export class AuthorizationCodePKCE extends BaseGrant implements Grant

async onRequest(): Promise<boolean> {
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")

Expand All @@ -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<string>): Promise<void> {
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(" "))
Expand Down
5 changes: 0 additions & 5 deletions svelte.config.js

This file was deleted.