Client-only routes i Gatsby brukes for de delene av nettsiden du ikke vil skal bygges som statiske HTML-sider.

Del 3: Slik bygget jeg sameiets nye nettsider. Autentisering og private ruter i Gatsby

I del 1 og 2 av denne serien har jeg gått gjennom hvilke teknologivalg jeg gjorde da jeg skulle bygge nye nettsider for boligsameiet, og hvordan jeg satte opp løsningen med Gatsby som frontend og Contentful som backend.

Gatsby omtales gjerne som en "static site generator", som betyr at i det du taster inn kommandoen gatsby build så begynner Gatsby å hente innhold fra et CMS, et API eller kanskje markdownfiler på disken. Innhold og data fra ulike kilder kombineres, og Gatsby rendrer statiske HTML-filer og pakker alt pent og pyntelig sammen — uten at du behøver å kunne noe som helst om Webpack-konfigurering, code splitting eller andre ting som ofte kan være litt komplisert å sette opp.

Det er mange fordeler med statiske nettsider, hvor kanskje høy ytelse er noe av det første i hvert fall jeg tenker på. Høy sikkerhet er også et argument. Ettersom nettsidene lages i det du bygger siden, og brukeren får servert statiske HTML-sider, er angrepsflaten betydelig redusert. Det er for eksempel ikke mulig for en angriper å få tilgang til annet innhold fra databaser eller CMS-er du bruker, utover det innholdet Gatsby hentet da de statiske sidene ble generert. Det har vært mange eksempler opp igjennom på Wordpress-plugins fulle av sikkerhetshull, ofte med fatale konsekvenser. Sånt slipper du med Gatsby.

Gatsby trenger ikke være bare statiske sider

Som nevnt i de første delene av denne serien, ønsket jeg å ha et eget område på sameiets nettsider som kun skulle være tilgjengelig for de som bor i sameiet. Det betyr at jeg ikke ønsker at disse sidene skal være statiske sider, men også ha mulighet til å hente innhold dynamisk etter behov — i mitt tilfelle avhengig av om brukeren er innlogget eller ikke.

Før jeg går inn på hvordan jeg har satt opp selve innloggingsfunksjonen, må jeg si litt om hvordan Gatsby kan håndtere sider som kun er tilgjengelige for innloggede brukere.

Gatsby har støtte for såkalte client-only routes. Dette gjør det mulig å lage sider som eksisterer kun på klienten (i nettleseren) og der det altså ikke opprettes statiske HTML-sider som havner i /public-mappen når du kjører gatsby build-kommandoen. Client-only-ruter fungerer mer som en tradisjonell single page-app i React, og ved å bruke Reach Router som er innebygget i Gatsby kan du håndtere de ulike rutene som kun innloggede brukere skal se.

I tillegg til det du har innebygget i Gatsby, trenger du en autentiseringsløsning. Jeg havnet til slutt på Auth0, siden det er en utbredt og velprøvd løsning som har muligheter jeg trenger senere for å bygge en løsning for brukeradministrasjon. Ved hjelp av Auth0 kan jeg beskytte tilgangen til alle client-only-ruter.

Under er et forenklet diagram som viser hvordan det fungerer hos meg. De blå boksene er statiske sider som lages ved bygging av Gatsby-siten. For ruten /informasjon lages det også en statisk side som hvis brukeren ikke er logget inn viser en tekstplakat med beskjed om at du må logge inn for å se innholdet. Hvis brukeren er logget inn, brukes Reach Router for å vise den riktige React-komponenten avhengig av hvilken rute brukeren prøver å nå. Dette pakkes inn i en <Privateroute>-komponent som bruker en higher order-komponent i auth0-react kalt withAutenthicationRequired til å sjekke om en bruker er logget inn eller ikke. Jeg har ikke noe mer avansert autentisering enn det, dvs. enten er man logget inn og har tilgang — eller så er man det ikke. Det er kun styret i sameiet som kan opprette nye brukere i Auth0, dermed har vi kontroll på hvem som har brukerkonto til enhver tid.

En <Privateroute>-komponent fungerer som en slags "portvakt" som slipper gjennom kun innloggede brukere.
En <Privateroute>-komponent fungerer som en slags "portvakt" som slipper gjennom kun innloggede brukere.

For å gjøre det enklere å lage client-only-ruter, bruker jeg en offisiell plugin kalt gatsby-plugin-create-client-paths. Når den er installert, kan du i gatsby-config.js konfigurere hvilke ruter du vil skal være private, og som altså Gatsby ikke skal lage statiske sider ut av når du kjører gatsby build:

// ./gatsby-config.js

plugins: [
{
      resolve: `gatsby-plugin-create-client-paths`,
      options: { prefixes: [`/informasjon/*`, `/min-side/*`] },
},
]

Her vil alt som ligger under /informasjon og /min-side være ikke-statiske sider. På sameiets nettsider er det et menyelement på navigasjonslinjen som heter For beboere som ligger under ruten /informasjon (altså https://gartnerihagen-askim.no/informasjon ). Jeg opprettet derfor filen informasjon.tsx under /src/pages, og bruker i denne Reach Router for å vise ulike React-komponenter avhengig av rute. Går du for eksempel til /informasjon/referater, er det <Referater>-komponenten som skal vises. Jeg har valgt å legge alle komponenter som kun skal vises til innloggede brukere i mappen /src/components/private-components i stedet for å bare hive alt sammen i en svær haug sammen med de andre komponentene. Mest for å gjøre kodebasen litt enklere å forholde seg til og øke lesbarheten, men også som en liten påminnelse til meg selv om at dette er komponenter der jeg kanskje kan gjøre en ekstra sjekk av om brukeren er logget inn, kanskje vise et brukernavn, osv. For eksempel har jeg en Min side-komponent hvor innloggede brukere kan få informasjon om brukeren, bytte passord, osv.

Slik ser informasjon.tsx-siden ut hos meg, og slik er rutingen satt opp (forkortet, se fullstendig kildekode på https://github.com/klekanger/gartnerihagen):

// ./src/pages/informasjon.tsx

import * as React from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import { Router } from '@reach/router';
import PrivateRoute from '../utils/privateRoute';

import InfoPage from '../components/private-components/informasjon';
import Referater from '../components/private-components/referater';

import LoadingSpinner from '../components/loading-spinner';
import NotLoggedIn from '../components/private-components/notLoggedIn';

const Informasjon = () => {
  const { isLoading, isAuthenticated, error } = useAuth0();

  if (isLoading) {
    return (
      <Box>
        <LoadingSpinner spinnerMessage='Autentiserer bruker' />
      </Box>
    );
  }

  if (error) {
    return <div>Det har oppstått en feil... {error.message}</div>;
  }

  if (!isAuthenticated) {
    return <NotLoggedIn />;
  }

  return (
    <Router>
      <PrivateRoute path='/informasjon' component={InfoPage} />
      <PrivateRoute
        path='/informasjon/referater/'
        component={Referater}
        title='Referater fra årsmøter'
        excerpt='På denne siden finner du referater fra alle tidligere årsmøter. Er det noe du savner, ta kontakt med styret.'
      />
    </Router>
  );
};

export default Informasjon;

<PrivateRoute>-komponenten min ser du nedenfor. Den sørger for at du må være innlogget for å få tilgang. Hvis ikke sendes brukeren til en innloggings-dialogboks levert fra Auth0.

// ./src/utils/privateRoute.tsx

import * as React from 'react';
import { withAuthenticationRequired } from '@auth0/auth0-react';

interface IPrivateroute {
  component: any;
  location?: string;
  path: string;
  postData?: any;
  title?: string;
  excerpt?: string;
}

function PrivateRoute({ component: Component, ...rest }: IPrivateroute) {
  return <Component {...rest} />;
}

export default withAuthenticationRequired(PrivateRoute);

Navbar med innlogging

Private ruter er vel og bra — men som nevnt trenger vi en autentiseringsløsning for å finne ut hvem som skal ha tilgang og ikke. Første versjon av sameiets nettsider ble satt opp med Netlify Identity og Netlify Identity Widget, på grunn av at jeg allerede brukte Netlify til å hoste nettsidene — og at løsningen var veldig enkel å sette opp.

Det viste seg imidlertid fort at det var en del begrensninger med Netlify Identity. Den ene var at innloggingsboksen ikke var på norsk (jeg oversatte og åpnet en pull request, men orket ikke å vente på at den skulle gå igjennom...). I tillegg begynte jeg å jobbe med et litt mer avansert frontend-dashbord for brukerkontoadministrasjon (som ikke er ferdig bygget ennå) hvor jeg ville trenge noe mer funksjonalitet enn det jeg fant i Netlify Identity Widget. Etter litt research, endte jeg opp med å velge Auth0.

Etter at jeg hadde registrert meg og satt opp alt hos Auth0.com, installerte jeg Auth0 React SDK slik: npm install @auth0/auth0-react

Auth0 React SDK bruker React Context, slik at du kan wrappe hele applikasjonen din i en Auth0Provider som sørger for at Auth0 vet om brukeren er logget inn eller ikke, uansett hvor i applikasjonen brukeren befinner seg. Da kan du senere, hvor som helst i applikasjonen, importere useAuth-hooken slik: import { useAuth0 } from '@auth0/auth0-react' og fra useAuth hente ut ulike metoder eller egenskaper som har med innlogging å gjøre, for eksempel sjekke om brukeren er autentisert, få opp en innloggingsboks, osv. F.eks. slik: const { isAuthenticated } = useAuth0() for å senere kunne sjekke om brukeren er innlogget ved å gjøre noe sånt som if("isAuthenticated){ return <NotLoggedIn /> }

I Gatsby kan du wrappe root-elementet til nettsiden med en annen komponent (f.eks. Auth0Provider) ved å eksportere wrapRootElement fra filen gatsby-browser.js. Les mer om det i Gatsby-dokumentasjonen.

Slik ser min gatsby-browser.js-fil ut, med Auth0Provider satt opp for at alle sider på nettsiden skal ha tilgang til informasjon om hvorvidt brukeren er logget inn eller ikke:

// ./gatsby-browser.js

import * as React from 'react';
import { wrapPageElement as wrap } from './src/chakra-wrapper';
import { Auth0Provider } from '@auth0/auth0-react';
import { navigate } from 'gatsby';

const onRedirectCallback = (appState) => {
  // Use Gatsby's navigate method to replace the url
  navigate(appState?.returnTo || '/', { replace: true });
};

export const wrapRootElement = ({ element }) => (
  <Auth0Provider
    domain={process.env.GATSBY_AUTH0_DOMAIN}
    clientId={process.env.GATSBY_AUTH0_CLIENT_ID}
    redirectUri={window.location.origin}
    onRedirectCallback={onRedirectCallback}
  >
    {element}
  </Auth0Provider>
);

export const wrapPageElement = wrap;

Jeg laget en innloggingsknapp i navigasjonslinjen øverst på skjermen. Når brukeren prøver å logge inn, blir vedkommende sendt til Auth0 sin innloggingsside — og tilbake igjen til sameiets nettside hvis brukernavn og passord er riktig.

Under innloggingsknappen har jeg også laget en Min side der brukeren får opp informasjon om hvem som er logget inn, og har mulighet til å bytte passord. Av sikkerhetsmessige årsaker endres ikke passordet direkte, men i stedet vil Bytt passord-knappen sende en POST-request til Auth0s autentiserings-API med forespørsel om å bytte passord. Auth0 har en beskrivelse av hvordan dette fungerer her.

"Min Side" dukker opp under innloggingsknappen når brukeren er logget inn, og gir tilgang til å bytte passord.
"Min Side" dukker opp under innloggingsknappen når brukeren er logget inn, og gir tilgang til å bytte passord.

Sikkert som banken? Noen betraktninger til slutt

De mest kunnskapsrike og observante leserne som har kikket på kildekoden til noen av de "private" komponentene mine, vil kanskje finne et potensielt sikkerhetsproblem i min løsning. Jeg har imidlertid valgt å se gjennom fingrene med dette i første versjon av nettstedet siden det ikke er sensitiv informasjon som ligger tilgjengelig for sameiets beboere (det er mer at det ikke er interessant for andre enn de som bor her).

Sikkerhetsproblemet er som følger: Selv om jeg har laget nettsidene for innloggede brukere som client only routes i Gatsby, og man må være innlogget for å komme dit, har jeg brukt Gatsbys GraphQL-datalag for å hente informasjonen som skal være tilgjengelig kun for innloggede brukere. Dette er kun tilgjengelig ved build time, noe som betyr at dataene fra spørringen over faktisk blir hentet ved build time (når du bruker useStaticQuery-hooken f.eks.), og at de blir tilgjengelig i det som til slutt blir levert til klienten.

For eksempel i Dokumenter-komponenten som brukes for å gi innloggede brukere en oversikt over referater etc. fra årsmøter brukes denne GraphQL-queryen for å hente ut informasjon:

// ./src/components/private-components/dokumenter.tsx

export default function Dokumenter({ title, excerpt, ...props }: IDokumenter) {
  const { menu }: IMenu = useStaticQuery(graphql`
    {
      menu: contentfulServiceMenu {
        files: menu6Files {
          contentful_id
          title
          file {
            url
            fileName
          }
          createdAt(formatString: "DD.MM.YYYY")
          updatedAt(formatString: "DD.MM.YYYY")
        }
      }
    }
  `);

  const content = menu?.files || [];

Men det kreves litt kunnskap om hvordan du bruker utviklerverktøyene og network-taben i nettleseren for å finne igjen data fra disse spørringene.

Det er mange måter å løse dette på, der én av måtene jeg har vurdert er å bruke en tredjeparts GraphQL-klient (Apollo f.eks.) til å hente data ved run-time, etter at jeg har sjekket at brukeren er logget inn. En annen mulighet er å bygge et backend-API som tar seg av henting av "hemmelige" data, etter en sjekk av om brukeren har rettigheter til dette eller ikke. Jeg holder uansett på å lage et brukeradmin-panel som vil være basert på Netlify Functions (som igjen er basert på AWS Lambda), hvor jeg på frontend henter et access token for brukeren som er innlogget og sender med dette når jeg gjør et kall til brukeradmin-API-et som ligger som en serverless function hos Netlify. Brukeradmin-API-et (backend) verifiserer deretter dette access-tokenet og sjekker at brukeren har de nødvendige rettigheter før API-et gjennomfører operasjoner som f.eks. å endre brukerdata, slette eller opprette brukere, osv. En tilsvarende løsning kan brukes til å returnere "hemmelige" data fra backend til kun autoriserte brukere. Les mer om rollebasert aksesskontroll (RBAC) hos Auth0.

Her er hvordan brukeradmin-dashboardet vil se ut, og en skjematisk fremstilling av hvordan autentiseringen kan foregå. Som sagt, "work in progress"...

Jeg jobber nå med et system for brukeradministrasjon, der alt som går på endringer av brukerkontoer av sikkerhetshensyn gjøres server-side og med Auth0 sitt Management API..
Jeg jobber nå med et system for brukeradministrasjon, der alt som går på endringer av brukerkontoer av sikkerhetshensyn gjøres server-side og med Auth0 sitt Management API..

Hvordan jeg løser dette, vil jeg komme tilbake til i en eventuell senere artikkel. Brukeradmin-panelet er et litt større prosjekt som jeg holder på å kikke på i de få ledige stundene jeg har. Dette er et fritidsprosjekt, og jeg sitter jo ikke og koder hele tiden på fritiden (selv om kona påstår det :-) ).

Gode tips mottas gjerne!

Gå til forsiden