Requirements:
npm install @byu-oit-sdk/svelte
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
orgetIdToken
function into the plugin configuration. AbyuOitGetIdToken
function is exported which can be used to override the defaultgetIdToken
function. ThebyuOitGetIdToekn
function must be used for websites using BYU's API manager.
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.
logIn
route.redirect
query parameter
from the login step, or falls back to the logInRedirect
option passed into the configuration.errorRedirect
and an error message will be displayed in the
query parameters of the url.logOut
route.logOutRedirect
route.errorRedirect
and an error message will be displayed in the
query parameters of the url.There are two functions exported by this package that must be implemented, and there are others that may be implemented to achieve additional functionality.
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:
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.
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.
export { topLevelLoad as load } from '@byu-oit-sdk/svelte'
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:
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 }
}
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:
<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.
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.
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 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:
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.
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.
<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.
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'))
}
}
The unit tests for this package are run via ava using the Typescript importer tsimp
and the Typescript runtime tsx
.
--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).--loader=tsx
node argument is necessary to prevent a TypeError (ERR_UNKNOWN_FILE_EXTENSION) caused by unrecognized file extensions during unit test execution.