🎓Tutorial: What you will learn
* How to create an OAuth Application using Github
* How to redirect requests using SvelteKit
* How to handle OAuth Callbacks
* How to use the Access Token to get Github User Information
* How to store http-only secure cookies using SvelteKit
* How to use the hooks middleware in SvelteKit
* How to read session information in the SvelteKit client
SvelteKit is the new way to build svelte applications. SvelteKit gives you the ability to run your application on the server and client. With this new approach you have the option to leverage http-only (server-side) cookies to manage authentication state. In this post, we will walk through the process of setting up OAuth authentication using Github and SvelteKit.
Prerequisites
What do I need to know for this tutorial?
- Javascript - https://developer.mozilla.org/en-US/docs/Web/JavaScript
- Fetch API - https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
- NodeJS v14+ - https://nodejs.org/
- A Github Account
Getting Started
Ready, set, go! SvelteKit provides a command-line application that we can use to spin up a new project, the CLI will ask us a bunch of questions, lets step through them. In your terminal create a new folder for this project. Let's call the project authy
or any name you prefer:
mkdir authy
cd authy
Use the npm init
function to create the SvelteKit project
npm init svelte@next
Let's go through the questions:
create-svelte version 2.0.0-next.73
Welcome to SvelteKit!
This is beta software; expect bugs and missing features.
If you encounter a problem, open an issue on https://github.com/sveltejs/kit/issues if none exists already.
? Directory not empty. Continue? › (y/N) y
? Which Svelte app template? › - Use arrow-keys. Return to submit.
[Choose Skeleton project]
? Use TypeScript? › No / Yes -> No
? Add ESLint for code linting? › No / Yes -> No
? Add Prettier for code formatting? › No / Yes -> No
✨ Yay! We just setup SvelteKit
Create Github OAuth Application
Go to https://github.com/settings/applications/new in your browser and create a new application called authy
with a homepage of http://localhost:3000
and a callback url of http://localhost:3000/callback
Click Register application
You will be redirected to a page that looks similar to this:
In your project directory, create a .env
file and in this file take the client id from the github page and add to the .env file as VITE_CLIENT_ID
and then click the Generate a new client secret
then copy the secret and add it to the .env
file as VITE_CLIENT_SECRET
VITE_CLIENT_ID=XXXXXXX
VITE_CLIENT_SECRET=XXXXXXXXXX
Save and close your .env file
🎉 you have created a Github OAuth application! Now we can wireup the OAuth application into our project to create a secure workflow.
Setup the login button
Setting up the login, we will need to add a button to src/routes/index.svelte
and then create a Sveltekit endpoint, this endpoint will perform a redirect to Github for authentication.
src/routes/index.svelte
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
<a href="/login">
<button>Login using Github</button>
</a>
Create the /login endpoint
SvelteKit not only leverages the file system to define page routes, SvelteKit leverages the file system to define endpoints as well. In the routes folder or any child folder in the routes folder, if a file ends with the .svelte
extension it is a page if the file ends with a .js
extension it is an endpoint. Using the exports feature of esm, you can map http
verbs to javascript handlers. In our case, we want to create a src/routes/login.js
file and map the GET
http verb to the exported get
function.
export async function get(req) {
return {
body: 'Hello'
}
}
With the get
handler on src/routes/login.js
defined, it will take a Request object as input and return a Response object as output. Each of these object types are defined as part of the fetch
specification:
https://fetch.spec.whatwg.org/#request-class
https://fetch.spec.whatwg.org/#response-class
In the SvelteKit documentation you can see them defined as typescript types:
type Headers = Record<string, string>;
type Request<Locals = Record<string, any>, Body = unknown> = {
method: string;
host: string;
headers: Headers;
path: string;
params: Record<string, string>;
query: URLSearchParams;
rawBody: string | Uint8Array;
body: ParameterizedBody<Body>;
locals: Locals; // populated by hooks handle
};
type EndpointOutput = {
status?: number;
headers?: Headers;
body?: string | Uint8Array | JSONValue;
};
type RequestHandler<Locals = Record<string, any>> = (
request: Request<Locals>
) => void | EndpointOutput | Promise<EndpointOutput>;
So what do we want to accomplish here?
We want to redirect the request to the github authentication endpoint with our CLIENT_ID
.
In order to respond from the server to the client with a redirect directive, we need to return a 3xx
status code, lets use 302
and we need to provide a location
in the header. This location should be github oauth authorization location. https://github.com/login/oauth/authorize
src/routes/login.js
const ghAuthURL = 'https://github.com/login/oauth/authorize'
const clientId = import.meta.env.VITE_CLIENT_ID
export async function get(req) {
const sessionId = '1234'
return {
status: 302,
headers: {
location: `${ghAuthURL}?client_id=${clientId}&state=${sessionId}`
}
}
}
Handling the callback
When Github authorizes or does not authorize, Github needs a way to let our application know. This is why we gave Github the callback
url. This url is the endpoint we need to create next. Create a new file src/routes/callback.js
and in that file provide a get
handler.
src/routes/callback.js
export async function get(req) {
return {
body: 'callback'
}
}
When we redirect the user to Github, Github will ask them to login, then authorize our application. If the user chooses to authorize the application, Github will redirect the browser to our callback endpoint passing with it a code
query parameter. We want to use that code
query parameter to get an access_token for the authorized user. Then we will use the access_token
to get the user information from Github.
We can use query.get
method off of the request
object to get the code
value. We can use the fetch
function from the node-fetch
library to make our request.
yarn add node-fetch
Get access token
src/routes/callback.js
import fetch from 'node-fetch'
const tokenURL = 'https://github.com/login/oauth/access_token'
const clientId = import.meta.env.VITE_CLIENT_ID
const secret = import.meta.env.VITE_CLIENT_SECRET
export async function get(req) {
const code = req.query.get('code')
const accessToken = await getAccessToken(code)
return {
body: JSON.stringify(accessToken)
}
}
function getAccessToken(code) {
return fetch(tokenURL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
client_id: clientId,
client_secret: secret,
code
})
}).then(r => r.json())
.then(r => r.access_token)
}
Get user info
const userURL = 'https://api.github.com/user'
function getUser(accessToken) {
return fetch(userURL, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`
}
})
.then(r => r.json())
}
modify get function
export async function get(req) {
const code = req.query.get('code')
const accessToken = await getAccessToken(code)
const user = await getUser(accessToken)
return {
body: JSON.stringify(user)
}
}
In our callback
handler we should now be seeing the user object! Great Job you have the happy path of Github OAuth working in SvelteKit. But we are not done.
Setting a cookie for user session
We need to instruct SvelteKit to write a http-only cookie. This cookie will keep our user session.
hooks
We need to create a src/hooks.js
file, this file will contain a handle
function that will allow us to read cookies and write cookies as it wraps the incoming request for every request.
import cookie from 'cookie'
export async function handle({request, resolve}) {
const cookies = cookie.parse(request.headers.cookie || '')
// code here happends before the endpoint or page is called
const response = await resolve(request)
// code here happens after the endpoint or page is called
return response
}
After the resolve function we want to check and see if the request's locals object was modified with a user
key. If it was, we want to set the cookie with the value.
import cookie from 'cookie'
export async function handle({ request, resolve }) {
const cookies = cookie.parse(request.headers.cookie || '')
// code here happends before the endpoint or page is called
const response = await resolve(request)
// code here happens after the endpoint or page is called
response.headers['set-cookie'] = `user=${request.locals.user || ''}; Path=/; HttpOnly`
return response
}
By setting the cookie with HttpOnly
- this will ensure that it can only be written by the server. A cookie will be stored in the browser and remain there until we clear it. So if we want to access the cookie information in any of our page or endpoint handlers we need to parse the cookie and set the value on the request.locals
object.
import cookie from 'cookie'
export async function handle({ request, resolve }) {
const cookies = cookie.parse(request.headers.cookie || '')
// code here happends before the endpoint or page is called
request.locals.user = cookies.user
console.log({ user: request.locals.user })
const response = await resolve(request)
// code here happens after the endpoint or page is called
response.headers['set-cookie'] = `user=${request.locals.user || ''}; Path=/; HttpOnly`
return response
}
set the request.locals.user value in callback.js
In src/routes/callback.js
we need to set the request.locals.user
value with the user.login
identifier, which is guaranteed to be unique and it work nicely for this demo.
export async function get(req) {
const code = req.query.get('code')
const accessToken = await getAccessToken(code)
const user = await getUser(accessToken)
// this mutates the locals object on the request
// and will be read by the hooks/handle function
// after the resolve
req.locals.user = user.login
return {
status: 302,
headers: {
location: '/'
}
}
}
Send session information to SvelteKit Load
In the src/hooks.js
file we can setup another function called getSession
this function will allow us to set a session object to be received by every load
function on a SvelteKit page component.
export async function getSession(request) {
return {
user: request.locals.user
}
}
Get session in the script module tag
In our src/routes/index.js
page component we are going to add two script tags, the first script tag will be of context module
and will run on the server, the second script tag will contain our client side logic for our Svelte Component.
<script context="module">
export async function load({ session }) {
return {
props: {
user: session.user,
},
};
}
</script>
<script>
export let user
</script>
<h1>Welcome to SvelteKit</h1>
<p>
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
</p>
{#if user}
<h2>Welcome {user}</h2>
<a href="/logout">
<button>Logout</button>
</a>
{:else}
<a href="/login">
<button>Login using Github</button>
</a>
{/if}
We use both script
tags to pass the session value from the load
function to the client script. This allows us to modify the view based on if the user is present in the session. We are able to show the user login name on the screen.
Sweet! ⚡️
Logout
Create a new file called src/routes/logout.js
in this file we will create a get
endpoint handler function. In this function, we want to set the user equal to null and redirect the request back to the home page.
export async function get(req) {
req.locals.user = null
console.log(req.locals.user)
return {
status: 302,
headers: {
location: '/'
}
}
}
Now, when you click the logout button, the user is set to an empty string versus the user.login.
Protecting pages and endpoints
Now that you have authentication working with Github OAuth, you may want to protect some pages and endpoints. You can perform a test on each page that you want to protect, or you can use the __layout.svelte
component and create an accepted list of paths that you would like to protect.
src/routes/__layout.js
<script context="module">
export async function load({page, session}) {
if (/^\/admin\/(.*)/.test(page.path) && session.user === '') {
return { redirect: '/', status: 302 }
}
return { props: {} }
}
</script>
<slot />
In this example, we are protecting all pages that start with /admin/*
in their path.
Summary
That is the end of this little journey my friend, it was a nice trip, hopefully you laughed more than cried, and learned something about SvelteKit. The SvelteKit routing bits are straightforward when you are able to walk through how they work, not much magic, and by setting http-only cookies, you can create simple long lived sessions for your applications. Remember, the information stored in the cookie is not encrypted so do not store any secrets, use a cache or a database if you need to put some more session/user specific data together.
Demo Repository: https://github.com/hyper63/tutorial-sveltekit-authentication
Sponsored by hyper
If you are building an application and want your application to be:
- Easy to Maintain!
- Easy to Test!
- Without un-intentional Technical Debt
You should check out hyper! https://hyper.io