Skip to content
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

Overhaul refresh token property #3

Merged
merged 6 commits into from
Jul 22, 2022
Merged
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
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
# Lucia

Lucia is a JWT based authentication library for SvelteKit that works with your code, and not the other way around. It provides the necessary building blocks for implementing authentication, allowing you to customize it to your own needs.
Lucia is a simple, JWT based authentication library for SvelteKit that connects your SvelteKit app with your database. It handles the bulk of the authentication process, like creating and validating tokens, but only just enough that you can build on top of it to fit your use case. That said, it isn't _just_ a JWT authentication library. It uses short-lived tokens, implements rotating refresh tokens, automatically refreshes tokens, and detects refresh token theft. It's main aim is to simplify the development process while not being a pain in the ass to customize!

It's important to note that this __isn't__ an out-of-the-box authentication library. It does not validate the user's input, it does not provide UI elements, and it does not provide a OAuth authentication (though it's simple to implement). These are out of the scope of this library and is left up to you. What it does provide is a set of tools for handling authentication, like `createUser` which saves the user in the database and generate a set of tokens.

> This library requires a database to work. If you need a free option, check out [Supabase](https://supabase.com), which Lucia supports out of the box.

Documentation: https://lucia-sveltekit.vercel.app

## Why Lucia ?

There are tons of client-side authentication services out there like Firebase, Auth0, or Supabase. But, they don't support SSR (SvelteKit) out of the box, and even if you get it to work, it's usually a very hacky solution that isn't worth the time. On the other hand, there are authentication libraries like NextAuth for Next.js that handles everything once you configure the database. But it's still limited in how it can be customized. Lucia aims to be a flexible customizable solution that is simple and intuitive.

## Installation

Expand Down
3 changes: 3 additions & 0 deletions apps/documentation/src/routes/[...doc].svelte
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,7 @@
:global(.markdown blockquote) {
@apply bg-indigo-100 px-2 py-1 rounded-md text-sm;
}
:global(.markdown .breaking) {
@apply text-red-500 font-medium;
}
</style>
6 changes: 6 additions & 0 deletions apps/documentation/src/routes/api/docs/[...doc].ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export const get: RequestHandler = async ({ params }) => {
tableWrapperDiv.appendChild(tableTopWrapper);
tableWrapperDiv.appendChild(tableBottomWrapper);
});
dom.window.document.querySelectorAll('li').forEach((element) => {
element.innerHTML = element.innerHTML.replaceAll(
'[Breaking]',
'<span class="breaking">[Breaking]</span>'
);
});
html = dom.serialize();
return {
body: JSON.stringify({
Expand Down
13 changes: 13 additions & 0 deletions apps/example-password-oauth/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example

# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
20 changes: 20 additions & 0 deletions apps/example-password-oauth/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
}
};
8 changes: 8 additions & 0 deletions apps/example-password-oauth/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
13 changes: 13 additions & 0 deletions apps/example-password-oauth/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example

# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
6 changes: 6 additions & 0 deletions apps/example-password-oauth/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100
}
18 changes: 18 additions & 0 deletions apps/example-password-oauth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Lucia Github and password auth example

```bash
npm install
```

## Setting up Supabase.

Follow [this page](https://lucia-sveltekit/adapters/supabase) on setting up supabase. In addition to that, add a `email` varchar column in `users` table with `unique=true`.

Add the Supabase url and service role secret to `lib/lucia.ts`.

## Github

Create 2 new Github OAuth app, one for development and one for production. The callback url should be `locahost:3000/api/github` for dev and its equivalent for prod.

Add the client id and client secret to `routes/index.svelte` and `routes/api/github.ts`.

40 changes: 40 additions & 0 deletions apps/example-password-oauth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "example",
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"package": "svelte-kit package",
"preview": "vite preview",
"prepare": "svelte-kit sync",
"check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
"format": "prettier --write --plugin-search-dir=. ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"autoprefixer": "^10.4.7",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^4.0.0",
"postcss": "^8.4.14",
"prettier": "^2.6.2",
"prettier-plugin-svelte": "^2.7.0",
"svelte": "^3.44.0",
"svelte-check": "^2.7.1",
"svelte-preprocess": "^4.10.7",
"tailwindcss": "^3.1.6",
"tslib": "^2.3.1",
"typescript": "^4.7.4",
"vite": "^3.0.0"
},
"type": "module",
"dependencies": {
"@lucia-sveltekit/adapter-supabase": "^0.2.3",
"lucia-sveltekit": "0.3.0"
}
}
6 changes: 6 additions & 0 deletions apps/example-password-oauth/postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
3 changes: 3 additions & 0 deletions apps/example-password-oauth/src/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
6 changes: 6 additions & 0 deletions apps/example-password-oauth/src/app.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="@sveltejs/kit" />
declare namespace App {
interface Session {
lucia: LuciaSvelteKitSession
}
}
12 changes: 12 additions & 0 deletions apps/example-password-oauth/src/app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body>
<div>%sveltekit.body%</div>
</body>
</html>
4 changes: 4 additions & 0 deletions apps/example-password-oauth/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { auth } from "$lib/lucia"

export const handle = auth.handleAuth
export const getSession = auth.getAuthSession
7 changes: 7 additions & 0 deletions apps/example-password-oauth/src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const sendForm = async (formElement: HTMLFormElement) => {
const response = await fetch(formElement.action, {
method: formElement.method,
body: new FormData(formElement)
});
return response;
};
12 changes: 12 additions & 0 deletions apps/example-password-oauth/src/lib/lucia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import lucia from 'lucia-sveltekit';
import supabase from '@lucia-sveltekit/adapter-supabase';
import { prod } from '$app/env';

export const auth = lucia({
adapter: supabase(
"SUPABASE_URL",
"SUPABSE_SERVICE_ROLE"
),
secret: "SECRET_KEY", // should be long and random
env: prod ? 'PROD' : 'DEV'
});
46 changes: 46 additions & 0 deletions apps/example-password-oauth/src/routes/__layout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script lang="ts">
import '../app.css';

import { autoRefreshTokens } from 'lucia-sveltekit/client';
import { session } from '$app/stores';
import { onDestroy } from 'svelte';

const unsubscribe = autoRefreshTokens(session, (e) => {
console.log(e);
});

onDestroy(() => {
unsubscribe();
});
</script>

<slot />

<style lang="postcss">
/* I've used @apply to apply tailwind classes for demo purposes to remove/hide as much non-lucia related things
Inline classes should be used for Tailwind */
:global(body) {
@apply px-4 pt-8;
}
:global(h2) {
@apply text-2xl font-bold;
}
:global(input) {
@apply border appearance-none outline-none my-1;
}
:global(button[type='submit'], .github, button) {
@apply bg-black text-white px-8 my-1;
}
:global(label) {
@apply text-sm;
}
:global(.github) {
@apply py-1;
}
:global(div) {
@apply py-4;
}
:global(.error) {
@apply text-red-400 text-sm;
}
</style>
21 changes: 21 additions & 0 deletions apps/example-password-oauth/src/routes/api/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { auth } from '$lib/lucia.js';
import type { RequestHandler } from '@sveltejs/kit';

export const GET: RequestHandler = async ({ request }) => {
try {
await auth.validateRequest(request);
const number = Math.floor(Math.random() * 100);
return {
body: JSON.stringify({
number
})
};
} catch (e) {
return {
status: 401,
body: JSON.stringify({
error: 'unauthorized'
})
};
}
};
106 changes: 106 additions & 0 deletions apps/example-password-oauth/src/routes/api/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { dev } from '$app/env';
import { auth } from '$lib/lucia.js';
import type { RequestHandler } from '@sveltejs/kit';
import type { LuciaError } from 'lucia-sveltekit';

const clientId = dev ? "DEV_GITHUB_CLIENT_ID" : "PROD_GITHUB_CLIENT_ID";
const clientSecret = dev ? "DEV_GITHUB_CLIENT_SECRET" : "PROD_GITHUB_CLIENT_SECRET";

export const GET: RequestHandler = async ({ url }) => {
const code = url.searchParams.get('code');
if (!code) {
return {
status: 400,
body: JSON.stringify({
message: 'Invalid request url parameters.'
})
};
}
const getAccessTokenResponse = await fetch(
`https://github.com/login/oauth/access_token?client_id=${clientId}&client_secret=${clientSecret}&code=${code}`,
{
method: 'POST',
headers: {
Accept: 'application/json'
}
}
);
if (!getAccessTokenResponse.ok) {
return {
status: 500,
body: JSON.stringify({
message: 'Failed to fetch data from Github'
})
};
}
const getAccessToken = await getAccessTokenResponse.json();
const accessToken = getAccessToken.access_token;
const getUserEmailsResponse = await fetch('https://api.github.com/user/emails', {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
if (!getUserEmailsResponse.ok) {
return {
status: 500,
body: JSON.stringify({
message: 'Failed to fetch data from Github'
})
};
}
const emails = (await getUserEmailsResponse.json()) as { email: string; primary: boolean }[];
const email = emails.find((val) => val.primary)?.email || emails[0].email;
const user = await auth.getUser('github', email);
if (user) {
try {
const authenticateUser = await auth.authenticateUser('github', email);
return {
status: 302,
headers: {
'set-cookie': authenticateUser.cookies,
location: '/profile'
}
};
} catch {
// Cannot connect to database
return {
status: 500,
body: JSON.stringify({
message: 'An unknown error occured'
})
};
}
}
try {
const createUser = await auth.createUser('github', email, {
user_data: {
email
}
});
return {
status: 302,
headers: {
'set-cookie': createUser.cookies,
location: '/profile'
}
};
} catch (e) {
const error = e as LuciaError;
// violates email column unqiue constraint
if (error.message === 'AUTH_DUPLICATE_USER_DATA') {
return {
status: 400,
body: JSON.stringify({
message: 'Email already in use'
})
};
}
// database connection error
return {
status: 500,
body: JSON.stringify({
message: 'An unknown error occured'
})
};
}
};
Loading