Module Server Svelte

@byu-oit-sdk/svelte

Requirements:

  • Node.js 18+
    • or Node.js 10+ with fetch and crypto polyfills
  • npm v9+

Installing

npm install @byu-oit-sdk/svelte

Introduction

Use this module to configure SvelteKit websites that need to implement the OAuth 2.0 Authorization Code grant type. This package requires you to also implement @byu-oit-sdk/session-svelte. See usage examples below.

This module assumes that the access token returned after the authorization code exchange will be a jwt that it can decode to extract user information. This behavior is configurable by passing in the userInfoCallback or getIdToken function into the plugin configuration. A byuOitGetIdToken function is exported which can be used to override the default getIdToken function. The byuOitGetIdToekn function must be used for websites using BYU's API manager.

Options

In addition to the options below, any AuthorizationCodeProvider options may also be passed into the configuration.

Option Type Default Description
logIn string /signin The path which redirects the caller to the authorization url to sign in.
logInRedirect string / The path to redirect the caller to after signing in.
logOut string /signout The path to call to clear a user's session and revoke the access token.
logOutRedirect string / The path to redirect the caller to after signing out.
errorRedirect string /error The path to redirect the caller to after an error is encountered during the authorization flow.
userInfoCookieName string user_info The name of the cookie containing the user information parsed from the token.
sessionIdCookieName string sessionId The name of the cookie where the users session id can be found. If overriding the default, this value must be identical to the value passed in to @byu-oit-sdk/session-svelte
getIdToken function (returns the access token) Configures how the package gets the id token.
userInfoCallback function (returns the decoded token using the getIdToken function) Configures how the package gets the user information.
clientId string - The client identifier issued to the client during the registration process.
clientSecret string - The client secret of the account being used. Some Oauth providers (such as BYU's API manager) will not require this value, but others, like Okta, will.
redirectUri string - The redirection endpoint that the authorization server should redirect to after authenticating the resource owner. This can be a relative URL (e.g. /callback) or absolute (e.g. https://somesite.byu.edu/auth/callback)
discoveryEndpoint string - Used to configure where the user will be sent to sign in.
scope string - An Oauth property for determining access to data (docs). Set this to open_id when using BYU's API manager.

Note that any of the parameters of type string can also be defined through environment variables with the BYU_OIT_ prefix.

In the code examples below, if any of the required parameters are not provided it is because environment variables were used instead.

Signing In

  1. For a user to log in, the browser should direct the user to the logIn route.
  2. After the user logs in, the server will redirect the user to the location provided in the redirect query parameter from the login step, or falls back to the logInRedirect option passed into the configuration.
  3. If the user encounters an error, they will be redirected to errorRedirect and an error message will be displayed in the query parameters of the url.

Signing Out

  1. For a user to log out, the browser should direct the user to thelogOut route.
  2. When the user logs out, their session is cleared but the token is not revoked.
  3. After logging out, they will be redirected to the logOutRedirect route.
  4. If the user encounters an error, they will be redirected to errorRedirect and an error message will be displayed in the query parameters of the url.

Usage

There are two functions exported by this package that must be implemented, and there are others that may be implemented to achieve additional functionality.

Install hooks with AuthorizationCodeFlow()

The only piece that absolutely must be implemented is the AuthorizationCodeFlow function which exports a SvelteKit handle function that sets up a series of hooks that handle log-in, log-out, and other authentication functionality. That handle function should be exported as handle through a sequence() call from your hooks.server.ts file so that you can have the handle from @byu-oit-sdk/session-svelte be executed first. See the example below:

import { SessionHandler } from '@byu-oit-sdk/session-svelte'
import { AuthorizationCodeFlow, byuOitGetIdToken as getIdToken } from '@byu-oit-sdk/svelte'
import { sequence } from '@sveltejs/kit/hooks'
import type { Handle } from '@sveltejs/kit'
import env from 'env-var'
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { DynamoSessionStore } from '@byu-oit-sdk/session-dynamo'

export const handle = await (async () => {
// this file runs (twice!) when running `vite build`, so if we are building, skip this code block.
// See https://github.com/sveltejs/kit/issues/8795
if (!building) {
return sequence()
}
// this is one example of how you could dynamically switch between using dynamo and in-memory stores for local and production use.
const isProduction = env.get('NODE_ENV').default('development').asEnum(['production', 'development']) === 'production'

let store // if store is undefined (e.g. for local development), an in-memory store will be used
if (isProduction) {
const client = new DynamoDBClient({
region: env.get('AWS_REGION').required().asString()
})
store = new DynamoSessionStore({ client, tableName: 'sessions' })
}

const sessionHandle = await SessionHandler({ store })

// the SessionHandler handle must be executed before the AuthorizationCodeFlow handle
return sequence(sessionHandle, AuthorizationCodeFlow({
redirectUri: '/callback',
getIdToken
}))
})()

Oauth data, including information from the decoded token, will now be stored in event.locals.session in server-side SvelteKit functions. For example, here is how you could access it in a request handler in a +server.ts file:

Example
import type {RequestHandler, RequestEvent} from './$types'

export const GET: RequestHandler = async function (event: RequestEvent) {
const session = event.locals.session.data ?? {}
if (!session.token.accessToken || !session?.user?.user_id)
return new Response(JSON.stringify({error: 'No access token'}), {status: 401})
}
const accessToken: string = session.token.accessToken

/* make an api call using the access token */

return new Response(/* add api call response data here */)
}

Note that the above example assumes that the id token contains a user_id property.

Access SessionHandler Information with topLevelLoad()

In addition to implementing the AuthorizationCodeFlow function in the hooks.server.ts, you should also export topLevelLoad() as load() from your top-level +layout.server.ts file (i.e. src/routes/+layout.server.ts). This function attaches information obtained from the token (i.e. user name and id) to your pagedata so that you can access it easily. SvelteKit is expecting a load() function to be exported from this file, so do not call it anything else.

ESM/Typescript Syntax
export { topLevelLoad as load } from '@byu-oit-sdk/svelte'
CommonJS Syntax
const { topLevelLoad } = require('@byu-oit-sdk/svelte')
exports.load = topLevelLoad

If you would like to implement your own load functionality in that file, you can define a load() function that executes the imported load function, as shown below:

ESM/Typescript Syntax
import { topLevelLoad } from '@byu-oit-sdk/svelte'
import type { LayoutServerLoad } from './$types'

export const load: LayoutServerLoad = (event) => {
const sessionLoadResults = topLevelLoad(event)

/* perform custom load functionality */
const foo = 'bar'

return { foo /* additional pagedata */, ...sessionLoadResults }
}
CommonJS Syntax
const { topLevelLoad } = require('@byu-oit-sdk/svelte')

exports.load = (event) => {
const sessionLoadResults = topLevelLoad(event)

/* perform custom load functionality */
const foo = 'bar'

return { foo /* additional pagedata */, ...sessionLoadResults }
}

Then, session data can be accessed in your svelte files. For example, a +layout.svelte needing to access your display_name might do something like the following:

Example:
<script lang="ts">
import type { PageData } from './$types'

export let data: PageData
</script>

{#if data.session.user}
<span slot="user-name">{data.session.user.display_name}</span>
{/if}

Note that this example depends on the id token including a display_name property.

Protect Sensitive Pages with Required or Automatic Authentication

In order to restrict access to certain pages (requiring users to log-in first), we recommend creating a layout group via a folder whose name is wrapped in parentheses (e.g. (authenticated)) so that all the routes that require the user to be logged-in can be stored in that folder. The parenthesis tell SvelteKit not to use that folder name in the actual path of the route (e.g. /(authenticated)/foo/bar is accessed by navigating to /foo/bar).

Create a +layout.server.ts file at the base of the directory for your protected routes, and in its exported load function check if the user has logged in. If not, then either throw an error, or automatically redirect the user to the login screen with the getLoginUrl function which is added to event.locals by AuthorizationCodeFlow.

Example:
import type { LayoutServerLoad } from './$types'
import { redirect } from '@sveltejs/kit'

export const load: LayoutServerLoad = async ({ locals }) => {
if (locals.session.data.token == null) {
redirect(302, locals.getLoginUrl('/return_to_me_after_login'))
}
}

If You Are Using BYUs API Manager...

If you are using calling APIs through BYU's API manager, the default behavior of this package may not work because BYU doesn't use id tokens by default. However, this package exports a byuOitGetIdToken function for this purpose:

Example:
import { SessionHandler } from '@byu-oit-sdk/session-svelte'
import { AuthorizationCodeFlow, byuOitGetIdToken } from '@byu-oit-sdk/svelte'
import { sequence } from '@sveltejs/kit/hooks'
import type { Handle } from '@sveltejs/kit'
import env from 'env-var'
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { DynamoSessionStore } from '@byu-oit-sdk/session-dynamo'

export const handle = await (async () => {
// this file runs (twice!) when running `vite build`, so if we are building, skip this code block.
// See https://github.com/sveltejs/kit/issues/8795
if (!building) {
return sequence()
}
// this is one example of how you could dynamically switch between using dynamo and in-memory stores for local and production use.
const isProduction = env.get('NODE_ENV').default('development').asEnum(['production', 'development']) === 'production'

let store // if store is undefined (e.g. for local development), an in-memory store will be used
if (isProduction) {
const client = new DynamoDBClient({
region: env.get('AWS_REGION').required().asString()
})
store = new DynamoSessionStore({ client, tableName: 'sessions' })
}


const sessionHandle = await SessionHandler({ store })

// the SessionHandler handle must be executed before the AuthorizationCodeFlow handle
return sequence(sessionHandle, AuthorizationCodeFlow({
redirectUri: '/callback',
getIdToken: byuOitGetIdToken
}))
})

Make sure to set your BYU_OIT_SCOPE environment variable to openid when using BYU's API manager.

Logging in and out

This package sets up two ways for users to log in and out. The main way is to have a link that sends the user to the value passed in as logIn or logOut in the AuthorizationCodeFlow({}) function (the default is /signin and /signout, respectively). The user could even manually navigate to those routes in their browser.

Example:
<a href="/signout">Sign Out</a>
<a href="/signin">Sign In</a>

If you want to redirect the user to login or logout pages while running server-side code, getLoginUrl and getLogoutUrl functions are added to event.locals by AuthorizationCodeFlow. These functions can be accessed in server-side load() functions in +page.server.ts or +layout.server.ts files.

Example:
import type { LayoutServerLoad } from './$types'
import { error, redirect } from '@sveltejs/kit'

export const load: LayoutServerLoad = async (event) => {
if (event.locals.session == null) {
error(500, 'No session found')
}
if (event.locals.session.data.token == null) {
redirect(302, event.locals.getLoginUrl('/return_to_me_after_login'))
}
}

Note on unit tests

The unit tests for this package are run via ava using the Typescript importer tsimp and the Typescript runtime tsx.

  • For Node.js versions v18.19 and higher, the --import=tsimp node argument is included to address an issue where running unit tests results in an error related to package path exports (ERR_PACKAGE_PATH_NOT_EXPORTED).
  • Conversely, for Node.js versions v18.18 and lower, the --loader=tsx node argument is necessary to prevent a TypeError (ERR_UNKNOWN_FILE_EXTENSION) caused by unrecognized file extensions during unit test execution.

Index

Type Aliases

Functions