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

Server-side user authentication+previewing, and server- and client-side error logging #274

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

Benaiah
Copy link
Contributor

@Benaiah Benaiah commented Jul 1, 2023

Jira ticket: LDAF-201

Proposed changes

  • Adds a handleToken hook that authenticates provided user tokens (in either the Authorization header or an HttpOnly ldafUserToken cookie) against a Redis instance (set up in production and runnable locally using either redis-server with no arguments or docker compose up) containing user objects that list the data required to render the current user (which is also sent to the root layout and the browser) and the user's Contentful Management API token (which is not). (Note that the ldafUserToken is not the Contentful Management API token.)

  • The handleToken hook initializes the Contentful client that requests normal Contentful content.

  • In the handleToken hook, if the request is for a url containing a preview, gets the management API token from Redis and makes a call to the Contentful space to ensure the user is still allowed to view preview content in that space. If the authentication+authorization succeeds, we replace the Contentful client with one that uses the preview API token and passes a preview: true variable to every GraphQL request (I haven't found a good way to automatically pass the variable as an argument to the queries, so the queries do need to be written with the preview variable in mind for previewing to work).

  • Adds /login route that has a default form action taking a POST request containing a Contentful Management API token, authenticates with Contentful using the token (it only requests the current user's information - the actual authorization to see unpublished content in the space is done on every preview request by the handleToken hook), and then stores the user info in Redis to use for further authentication.

  • Adds /logout route that clears the ldafUserToken cookie and (attempts to) delete the stored user info in Redis (this is given an expiration time of one week in Redis itself, so unsuccessful deletes will not leave data around indefinitely).

  • Adds an OAuth2 callback at /callback/oauth2 that receives a request from Contentful containing a Management API token (which is passed in the hash, so the OAuth2 callback requires client-side JS to work - you can still log in without JS by entering a personal access token on the /login page) and then requests /login from the client side to save the token to Redis. This is the only time that client-side code has access to the Management API token (and the Management API token is not stored in the cookie either, so we're not sending it to third parties in HTTP requests - the cookie has to be SameSite=None in order to work inside the iframe used by the Contentful preview in the Contentful interface.)

  • Adds a "Login with Contentful" link to the /login page that takes you to Contentful, where you can log in and authorize the OAuth app to use your account and then be redirected back to the OAuth2 callback page to save the Contentful Management API token to Redis.

  • Adds a server-side and client-side logger that currently logs to the console (in dev or tests) or to the #ldaf-eng-logs Slack channel (in production, including npm run build && npm run preview).

  • Adds handleError hooks to both the server and the client that use the respective logger.

  • Sets the appropriate Content-Security-Policy to allow embedding the site in the preview iframe in the Contentful interface.

  • Adds lots of tests for everything above (except the logger, which was verified manually - it's possible to test the logger as well and we should probably do it eventually but this PR is already quite large).

  • Adds a Dockerfile and docker-compose.yml that allow spinning up other services in addition to the main web app. This can be completely ignored if you don't want to deal with Docker - the site works (or at least should work) exactly the same without Redis running if you're not requesting preview content. Even if you need to work with Redis, you can run redis-server with no arguments and then start the app and everything including user sessions and authentication will work.

Acceptance criteria validation

  • Tested manually
  • Tests added to cover the change
  • Full security writeup finished (in progress).
  • Additions made to CONTRIBUTING.md explaining how to work with all the new features like logins and logging in development.

Requested feedback

See my upcoming self-review.

@vercel
Copy link

vercel bot commented Jul 1, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
ldaf ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 18, 2023 2:49am

src/hooks.server.ts Outdated Show resolved Hide resolved
src/hooks.client.ts Outdated Show resolved Hide resolved
Copy link
Member

@LouisFettet LouisFettet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local issues I'm having aside, this is looking good. Appreciate all the tests and comments! I definitely owe this a more thorough review when I get back from PTO if y'all haven't merged it by then.

I wasn't able to get Docker working, but I was able to get things running with a normal install of Redis. The initial auth is the only thing that's working for me though; I'm able to provide a token and it gives me a success message, but then when redirecting back to a URL with ?preview I get a 401 error telling me to log in again (this isn't an issue on non-preview pages; everything looks right and I get the welcome message and logout link in the header). Not sure if this is intentional or not for this stage, or if you're seeing something different on your end, so I figured I would call it out.

Also, is there a timeout on the Redis connection? After a few minutes of inactivity I notice that I get a Socket closed unexpectedly error with the Redis client. Could be something wrong with my install if you're not seeing this though.

I've got some other comments below; only other note is that it might be good to update the README with instructions for running with Docker / installing Redis.

.eslintrc.cjs Outdated Show resolved Hide resolved
src/app.d.ts Show resolved Hide resolved
Dockerfile Outdated Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to write a test for this, even if it doesn't cover the on:click handler (I assume that would be a bit of a mocking nightmare).

Comment on lines +13 to +17
// Regular setContext _only exists on the server_. On the client, _always_ use
// setPublicContext. setPublicContext can also be used on the server to set context that will be
// accessible by the client logger (so it must be serializable!). Note that "public" means "public
// to the browser session of the current user", so things like the current user data are safe to store
// in public context because only that user will see them.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this note! Maybe to avoid confusion we could replace Public with UserSession instead? Not sure if that's any more or less accurate but I am worried that "public" could be misinterpreted when reading it outside of this file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't like that name because the backend logging is also for a specific user session. What about replacing Public with Client?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like Client!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tom Hanks in Catch Me If You Can

src/routes/+layout.svelte Show resolved Hide resolved
src/routes/about/organization/[slug]/+page.server.ts Outdated Show resolved Hide resolved
@github-actions
Copy link

Coverage after merging benaiah/server-side-previewing-squashed-and-rebased into main will be

85.64%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   hooks.server.ts78.45%75.68%60%79.60%104–105, 112, 131, 131, 141, 154–156, 175–180, 19–21, 218–219, 22, 220–228, 23, 237–238, 24, 244–248, 25–36, 39–41, 44–49
src/lib/actions
   logout.ts48.94%0%0%51.11%21–43
src/lib/components/Alert
   Alert.svelte94.74%57.14%100%100%34, 41, 41
src/lib/components/Breadcrumb
   Breadcrumb.svelte100%100%100%100%
src/lib/components/Button
   Button.svelte100%100%100%100%
   buttonOptions.ts100%100%100%100%
   index.ts100%100%100%100%
src/lib/components/Card
   Card.svelte97.50%0%100%100%7
   index.ts100%100%100%100%
src/lib/components/ContactCard
   ContactCard.svelte100%100%100%100%
   Section.svelte100%100%100%100%
src/lib/components/ContentfulRichText
   ContentfulRichText.svelte100%100%100%100%
   headings.ts93.18%50%100%95.12%37–39
   predicates.ts97.87%100%93.33%98.46%63
src/lib/components/ContentfulRichText/nodes
   Blockquote.svelte100%100%100%100%
   Heading1.svelte100%100%100%100%
   Heading2.svelte100%100%100%100%
   Heading3.svelte100%100%100%100%
   Heading4.svelte100%100%100%100%
   Heading5.svelte100%100%100%100%
   Heading6.svelte100%100%100%100%
   Hr.svelte100%100%100%100%
   Hyperlink.svelte100%100%100%100%
   ListItem.svelte100%100%100%100%
   Node.svelte100%100%100%100%
   OrderedList.svelte100%100%100%100%
   Paragraph.svelte100%100%100%100%
   Table.svelte100%100%100%100%
   TableCell.svelte100%100%100%100%
   TableHeaderCell.svelte100%100%100%100%
   TableRow.svelte100%100%100%100%
   Text.svelte100%100%100%100%
   UnorderedList.svelte100%100%100%100%
   index.ts100%100%100%100%
src/lib/components/CopyToClipboard
   CopyToClipboard.svelte97.50%60%100%100%71, 71
   index.ts100%100%100%100%
src/lib/components/Header
   Header.svelte93.55%50%80%100%49, 49–50, 50, 53
src/lib/components/Header/Nav
   Nav.svelte97.06%83.33%100%100%19
   NavItem.svelte100%100%100%100%
   NavLink.svelte100%100%100%100%
   NavMenu.svelte98.25%83.33%100%100%59, 68
   index.ts100%100%100%100%
src/lib/components/Header/Title
   Title.svelte100%100%100%100%
   index.ts100%100%100%100%
src/lib/components/Header/User
   User.svelte95%0%100%100%13
   index.ts100%100%100%100%
src/lib/components/Icon
   Icon.svelte87.50%20%100%100%19, 19, 24, 24
   index.ts100%100%100%100%
src/lib/components/Image
   BlurhashRenderer.svelte100%100%100%100%
   Image.svelte87.31%68.75%33.33%91.98%112, 117–121, 137, 137, 156–157, 17–22, 40, 69, 76–77, 89, 91–92
   generateSources.ts31.58%33.33%0%32.39%10–14, 19–40, 43–49, 5, 50–59, 6, 60–61, 8–9
   index.ts100%100%100%100%
   renderBlurhash.ts100%100%100%100%
src/lib/components/IntersectionObserver
   IntersectionObserver.svelte94.74%75%100%100%36–37, 39
   RootIntersectionObserver.svelte85.19%0%100%92%14–17
   index.ts100%100%100%100%
   key.ts100%100%100%100%
   observe.ts99.03%93.33%100%100%73
src/lib/components/Link
   Link.svelte100%100%100%100%
   index.ts100%100%100%100%
src/lib/components/LoginLink
   LoginLink.svelte95.65%50%100%100%11
   index.ts100%100%100%100%
src/lib/components/LogoutLink
   LogoutLink.svelte100%100%100%100%
   index.ts100%100%100%100%
src/lib/components/Search
   Search.svelte90.63%33.33%100%96.49%32–34, 39, 39, 43
   index.ts100%100%100%100%
   options.ts100%100%100%100%
src/lib/components/SideNav
   SideNav.svelte100%100%100%100%
   SideNavItem.svelte100%100%100%100%
src/lib/constants
   support.ts35.71%0%100%40%12–14, 17–25, 4–9
   tokenDuration.ts100%100%100%100%
src/lib/contexts
   currentUser.ts100%100%100%100%
src/lib/logger
   contextOptions.ts100%100%100%100%
   logMessages.ts42.86%0%0%46.99%23, 28–31, 34–37, 40–43, 46–48, 51–56, 59–67, 70–83
   logMethods.ts28.57%100%0%31.25%12–16, 19–32, 7–9
   logRawMessageInDevOrTests.ts44.44%100%0%50%5–8
   private.server.ts81.54%62.50%66.67%83.62%26, 26–28, 41–45, 50–62
   setupLoggerMethod.ts68.75%25%100%74.55%34, 37–38, 38–39, 39–43, 45–54
src/lib/logger/__mocks__
   private.server.ts100%100%100%100%
src/lib/server
   getCurrentUser.ts97.96%66.67%100%100%24
src/lib/services/contentful
   graphqlClient.ts75.48%54.55%44.44%79.26%121, 123, 123, 50–51, 54–55, 58–75, 80–83, 86–87, 97–98
   index.ts100%100%100%100%
src/lib/util
   catchMeIfYouCan.ts43.75%20%100%50%3–4, 4–5, 5–9
   classNames.ts100%100%100%100%
   consoleErrorIfYouCan.ts7.14%100%0%7.69%10–13, 2–9
   getErrorMessage.ts7.14%100%0%7.69%10–13, 2–9
   getErrorMessageFromResponse.ts23.44%25%25%23.21%10–16, 19–37, 4, 41–49, 5, 50–56, 8–9
   getErrorStatus.ts9.68%100%0%10%10–26, 29–49, 5, 50–59, 6, 60, 7–9
   indent.ts100%100%100%100%
   warn.ts100%100%100%100%
src/routes/login
   +page.server.ts84.52%61.54%100%88.57%22–24, 35–39, 60, 60–61, 65–66
src/routes/logout
   +page.server.ts83.33%66.67%100%88.24%12–14, 9
src/stories
   CardView.svelte99.09%50%100%100%74

@github-actions
Copy link

Copy link
Contributor

@hinzed1127 hinzed1127 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all your work on this, it's a lot but looks pretty solid. I had a few small documentation things to note, but otherwise not a whole lot jumped out.

Local development is broken for me right now (I'm currently getting a 500 page with "got malformed submenu"), which I believe is due to some of the recent navigation updates. I'll give things another go locally to make sure things run as expected once that's sorted out, but other than that, stuff looks good 👍

@@ -0,0 +1 @@
export default 60 * 60 * 24 * 7;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe just add a clarifying comment that this is (if I'm correctly interpreting) creating a week-long token where the base unit is 1 second that's getting multiplied through seconds*minutes*hours*days

params,
locals: { contentfulClient },
}): Promise<{ text: string }> => {
// TODO: create fallback fixture
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just highlighting this in case we want to specifically capture this in a follow-up story

SQUASHME: client creation in Nav

rename contentful implementation file

remove unnecessary type declaration

don't override page data with the document; pass query to frontend

rename contentful/client to contentful/graphqlClient

convert under construction page to use GraphQL client

fix e2e header tests

fix loadMainNav when there is no Contentful connection

remove unnecessary query variable from page data

add server-side contentful previewing w/contentful auth

ignore unused vars that start with _

improve server loading

enable previewing in nav graphql query

WIP

fix import

add management client to try block

delete cookie on logout

don't store the user token in localStorage; let it expire

fix oauth callback page

make sure user is activated

add some debug logging

decode the cookie values

fiddling with the cookie

move logout logic to an action

use sveltekit's fetch function instead of global fetch when possible

debugging

reduce token scope

can we use the user's token instead of a global one?

move codegen config; separate schema and schema type generation

switch default scalar type to "unknown" to avoid "any" pollution

update package scripts to generate schema types correctly

make preview authentication universal

let contentful embed the site

fix package scripts

throw preview authentication errors in layout not handler

fix broken oauth link

request correct token

WIP disable layout error

WIP catch preview auth errors with an actual layout

import component

remove unused preview client

add missing baseURL variable

stop throwing errors in the root layout

make errors more detailed

silly mistake

another silly mistake

set CSP in a hook in addition to vercel settings

try it again

try with x-frame-options

allow cookies to pass to frame

fix sameSite setting

WIP does this work

fix cookie samesite handling

tighten up CSP

improve CSP headers

create the server-side contentful client in a single place

add previewing to all gql queries

update schemas

fix homepage

fix linting; add query variable typing

add a trailing newline

add new env variables to env.d.ts

make LoginLink accept anchor element props

open login link in new window if it's in an iframe

fix a test

make contentful oauth endpoint an environment variable

refresh the page on logout

make LoginLink a little nicer

improve parseHashQuery

parseHashQuery should return Record<string, string>

logout should only be available in the browser

run prebuild before building storybook

avoid errors when $page or $page.url are undefined

update schemas

use null to explicitly delete cookie and localStorage entries

improve cookie deletion

add trailing newline

reuse setCookie to define deleteCookie

undo "fix" to parseHashQuery

add preview parameter when navigating from a preview

set preview to previous preview value

use workaround to preserve query parameter

cf. sveltejs/kit#10122

small refactors to root layout

only add preview param to URLs that don't already have it

move a variable

refactor logout action with "get" from svelte/store

improve graphqlClient tests

get current user on the server

fix User component

work around weird test error

use new /login and /logout endpoints for http-only contentful auth

remove unnecessary type annotations

make login/logout logic work without hard refreshing

mock $app/stores to fix tests

disable flaky eslint rule that's already covered by ts

use "import type" in app.d.ts

remove unused imports

add tests of cookie functionality

more tests of cookies

remove a bunch of dead code

various changes prior to setting up redis token handling

tests for handleToken

set up docker compose with redis

redis authentication complete; still need to fix tests

update office/organization page to support new preview functionality

also queries based on the slug instead of grabbing all office page entries

support passing "unknown" to ContentfulRichText

fix test-contentful-content page

lots of testing of handleToken; some improvements to it

shorten a line

don't merge office page with layout data

add missing test file

lots of testing and improvements

lots more progress and tests, including adding a logging service

add handleError hooks to the client and server

set url context in handleError on both client and server

add some comments

rearrange logging code; fix tests and dev; use new message format

fix a type error

remove a weird emacs thing in tsconfig

stop exposing logger on window

expand some comments

fix file capitalization

fix syntax error

add type definitions for new env variables

add example values for new env variables

remove .only from describe

remove unused imports

simplify ContentfulLoginLink

show footer when there is a previewAuthenticationError

use Link for special links; fix a LoginLink bug

update schema and migrate from "metadata" to "pageMetadata"

await error logging in handleError

actually run the error handling

fix some types

fix some prop types

improve the nav loading to get rid of type assertions

fix test data

move redis client creation into its own hook

fix type errors

don't import devalue parse ahead of time

fix type errors

get the redis client working in docker

get types checking and some tests passing again

fix nav

fix a type issue

move server hooks into their own files (still need to fix tests)

big refactoring and improved tests

remove old redis service

move contentful service into a server-only folder

improvements to error handling and logging

use dynamic env for KV_URL

run end-to-end tests with docker compose

fix formatting

don't show logout button when not logged in

small updates to login page

fix bad import

add comment to token duration
@Benaiah Benaiah force-pushed the benaiah/server-side-previewing-squashed-and-rebased branch from 5163ba1 to f9c6176 Compare August 18, 2023 02:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants