diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json
index ef74cbdbf973cfbfe0a0c596ae4e97961d923f76..0557dd77520ee8597aa6213c09a952ee8e5499d4 100644
--- a/frontend/.eslintrc.json
+++ b/frontend/.eslintrc.json
@@ -22,6 +22,7 @@
   "plugins": ["react", "@typescript-eslint"],
   "rules": {
     "semi": "off",
+    "react/jsx-props-no-spreading": "off",
     "@typescript-eslint/semi": ["error", "never"]
   }
 }
diff --git a/frontend/public/index.html b/frontend/public/index.html
index aa069f27cbd9d53394428171c3989fd03db73c76..2bc0e084ce0bca917aa25836d941d54011b64d6a 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -10,34 +10,11 @@
       content="Web site created using create-react-app"
     />
     <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
-    <!--
-      manifest.json provides metadata used when your web app is installed on a
-      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-    -->
     <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
-    <!--
-      Notice the use of %PUBLIC_URL% in the tags above.
-      It will be replaced with the URL of the `public` folder during the build.
-      Only files inside the `public` folder can be referenced from the HTML.
-
-      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
-      work correctly both with client-side routing and a non-root public URL.
-      Learn how to configure a non-root public URL by running `npm run build`.
-    -->
-    <title>React App</title>
+    <title>Guest Registration</title>
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>
     <div id="root"></div>
-    <!--
-      This HTML file is a template.
-      If you open it directly in the browser, you will see an empty page.
-
-      You can add webfonts, meta tags, or analytics to this file.
-      The build step will place the bundled scripts into the <body> tag.
-
-      To begin the development, run `npm start` or `yarn start`.
-      To create a production bundle, use `npm run build` or `yarn build`.
-    -->
   </body>
 </html>
diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json
index 23b623dc27804e6390f5ce5422ae88d408e85172..4c2ab711ff2501d634315c3041da3394a142b2eb 100644
--- a/frontend/public/locales/en/common.json
+++ b/frontend/public/locales/en/common.json
@@ -8,11 +8,14 @@
   },
   "fnr": "National identity number",
   "header": {
-    "applicationTitleShort": "GREG",
-    "applicationTitleLong": "Guest Registration",
+    "applicationTitle": "Guest Registration",
+    "applicationDescription": "Registration service for guests",
     "selectLanguage": "Select language"
   },
   "loading": "Loading...",
   "termsHeader": "Terms",
-  "staging": "Staging"
+  "staging": "Staging",
+  "firstName": "First name",
+  "lastName": "Last name",
+  "dateOfBirth": "Date of birth"
 }
diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json
index d916c4911609a188677a2bbf3dffcdf27761dbc4..11a25642f5fb9b4214464761810703467700b845 100644
--- a/frontend/public/locales/nb/common.json
+++ b/frontend/public/locales/nb/common.json
@@ -7,12 +7,15 @@
     "change": "Bytt språk til {{lang}}"
   },
   "fnr": "Fødselsnummer",
-  "header":{
-    "applicationTitleShort": "GREG",
-    "applicationTitleLong": "Gjesteregistrering",
+  "header": {
+    "applicationTitle": "Gjesteregistrering",
+    "applicationDescription": "Registreringstjeneste for gjester",
     "selectLanguage": "Velg språk"
   },
   "loading": "Laster...",
   "termsHeader": "Vilkår",
-  "staging": "Staging"
+  "staging": "Staging",
+  "firstName": "Fornavn",
+  "lastName": "Etternavn",
+  "dateOfBirth": "Fødselsdato"
 }
diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json
index db11f21eacd26a8402d7d1244315094909efbe62..21039b90560962d4338ef53f60348eb0d8edba48 100644
--- a/frontend/public/locales/nn/common.json
+++ b/frontend/public/locales/nn/common.json
@@ -8,12 +8,15 @@
     "change": "Bytt språk til {{lang}}"
   },
   "fnr": "National identity number",
-  "header":{
-    "applicationTitleShort": "GREG",
-    "applicationTitleLong": "Gjesteregistrering",
+  "header": {
+    "applicationTitle": "Gjesteregistrering",
+    "applicationDescription": "Registreringsteneste for gjester",
     "selectLanguage": "Velg språk"
   },
   "loading": "Lastar...",
   "termsHeader": "Vilkår",
-  "staging": "Staging"
+  "staging": "Staging",
+  "firstName": "Fornamn",
+  "lastName": "Etternamn",
+  "dateOfBirth": "Fødselsdato"
 }
diff --git a/frontend/src/components/button/index.tsx b/frontend/src/components/button/index.tsx
index 9eb200519a6b51d5342bd15a8a5b9a1b188e2fe7..7054c37015069fd236de9a1312f0979977135538 100644
--- a/frontend/src/components/button/index.tsx
+++ b/frontend/src/components/button/index.tsx
@@ -2,6 +2,7 @@ import styled from 'styled-components/macro'
 
 export const Button = styled.a`
   display: inline-block;
+  cursor: pointer;
   border-radius: 3px;
   padding: 0.5rem 0;
   margin: 0.5rem 1rem;
diff --git a/frontend/src/components/dateinput/index.tsx b/frontend/src/components/dateinput/index.tsx
index b7623b9cf0465ac7654eb3ff6ea2ab473034e716..a12c37a7dd0260d5190847a53849dc3e01923f40 100644
--- a/frontend/src/components/dateinput/index.tsx
+++ b/frontend/src/components/dateinput/index.tsx
@@ -1,29 +1,25 @@
-import React, { useState } from 'react'
-import DatePicker, { registerLocale } from 'react-datepicker'
+import React from 'react'
+import DatePicker, {
+  ReactDatePickerProps,
+  registerLocale,
+} from 'react-datepicker'
+
+import 'react-datepicker/dist/react-datepicker.css'
+import { useTranslation } from 'react-i18next'
+
 import nb from 'date-fns/locale/nb'
 import nn from 'date-fns/locale/nn'
-import { addDays } from 'date-fns'
-import { useTranslation } from 'react-i18next'
-import 'react-datepicker/dist/react-datepicker.css'
 
 registerLocale('nb', nb)
 registerLocale('nn', nn)
 // CSS Modules, react-datepicker-cssmodules.css
 // import 'react-datepicker/dist/react-datepicker-cssmodules.css';
 
-const DateInput = () => {
-  const [startDate, setStartDate] = useState(new Date())
+const DateInput = (props: ReactDatePickerProps) => {
   const { i18n } = useTranslation()
 
   return (
-    <DatePicker
-      dateFormat="yyyy-MM-dd"
-      locale={i18n.language}
-      selected={startDate}
-      minDate={startDate}
-      maxDate={addDays(new Date(), 365)}
-      onChange={(date: any) => setStartDate(date)}
-    />
+    <DatePicker dateFormat="yyyy-MM-dd" locale={i18n.language} {...props} />
   )
 }
 
diff --git a/frontend/src/components/debug/index.tsx b/frontend/src/components/debug/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..51f894357d626b1961d9f7feb1e75e053365cfdd
--- /dev/null
+++ b/frontend/src/components/debug/index.tsx
@@ -0,0 +1,145 @@
+import React, { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+
+import { appTheme, appTimezone, appVersion, appInst } from 'appConfig'
+
+export const Debug = () => {
+  const [apiHealth, setApiHealth] = useState('not yet')
+  const [didContactApi, setDidContactApi] = useState(false)
+  const { i18n } = useTranslation(['common'])
+  const [csrf, setCsrf] = useState<String | null>(null)
+  const [isAuthenticated, setIsAuthenticated] = useState(false)
+  const [username, setUsername] = useState(undefined)
+
+  if (!didContactApi) {
+    setDidContactApi(true)
+    fetch('http://localhost:3000/api/health/')
+      .then((res) => res.text())
+      .then((result) => {
+        if (result === 'OK') {
+          setApiHealth('yes')
+        } else {
+          setApiHealth(result)
+        }
+      })
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      .catch((error) => {
+        setApiHealth('error')
+      })
+  }
+
+  const getCSRF = () => {
+    fetch('/api/ui/v1/csrf/', {
+      credentials: 'same-origin',
+    })
+      .then((res) => {
+        const csrfToken = res.headers.get('X-CSRFToken')
+        setCsrf(csrfToken)
+        console.log(csrfToken)
+      })
+      .catch((err) => {
+        console.log(err)
+      })
+  }
+
+  const getSession = () => {
+    fetch('/api/ui/v1/session/?format=json', {
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      credentials: 'same-origin',
+    })
+      .then((res) => res.json())
+      .then((data) => {
+        console.log(data)
+        if (data.isAuthenticated) {
+          setIsAuthenticated(true)
+        } else {
+          setIsAuthenticated(false)
+          getCSRF()
+        }
+      })
+      .catch((err) => {
+        setIsAuthenticated(false)
+        console.log(err)
+      })
+  }
+
+  const whoami = () => {
+    fetch('/api/ui/v1/whoami/', {
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      credentials: 'same-origin',
+    })
+      .then((res) => res.json())
+      .then((data) => {
+        setUsername(data.username)
+        console.log(`You are logged in as: ${data.username}`)
+      })
+      .catch((err) => {
+        console.log(err)
+      })
+  }
+
+  function isResponseOk(response: any) {
+    if (response.status >= 200 && response.status <= 299) {
+      return response.json()
+    }
+    throw Error(response.statusText)
+  }
+
+  const logout = () => {
+    fetch('/api/ui/v1/logout/', {
+      credentials: 'same-origin',
+    })
+      .then(isResponseOk)
+      .then((data) => {
+        console.log(data)
+        setIsAuthenticated(false)
+        getCSRF()
+      })
+      .catch((err) => {
+        console.log(err)
+      })
+  }
+
+  const d = [
+    ['NODE_ENV', process.env.NODE_ENV],
+    ['Version', appVersion],
+    ['Timezone', appTimezone],
+    ['Language', i18n.language],
+    ['Theme', appTheme],
+    ['Institution', appInst],
+    ['API reachable?', apiHealth],
+    ['Csrf', csrf],
+    ['Authenticated?', isAuthenticated ? 'Authenticated' : 'Not Authenticated'],
+    ['username', username],
+  ]
+  return (
+    <table>
+      <thead>
+        <strong>Debug</strong>
+      </thead>
+      <button type="button" onClick={() => getSession()}>
+        AM I AUTHENTICATED?
+      </button>
+      <button type="button" onClick={() => whoami()}>
+        WHO AM I?
+      </button>
+      <button type="button" onClick={() => logout()}>
+        LOGOUT
+      </button>
+      <tbody>
+        {d.map(([key, value]) => (
+          <tr>
+            <td>{key}</td>
+            <td>{value}</td>
+          </tr>
+        ))}
+      </tbody>
+    </table>
+  )
+}
+
+export default Debug
diff --git a/frontend/src/components/form/fnr.tsx b/frontend/src/components/form/fnr.tsx
index 16e356125bdef88dfbae3e90d18164ef31e6b318..ee7cd1f2a920bc3b443d11f4c5a084f0321b5065 100644
--- a/frontend/src/components/form/fnr.tsx
+++ b/frontend/src/components/form/fnr.tsx
@@ -1,11 +1,7 @@
 import React from 'react'
-import validator from '@navikt/fnrvalidator'
-import { UseFormReturn } from 'react-hook-form'
 
-export function isValidIdnr(data: string) {
-  const validationResult = validator.idnr(data)
-  return validationResult.status === 'valid'
-}
+import { UseFormReturn } from 'react-hook-form'
+import { isValidFnr } from 'utils'
 
 interface FnrProps extends Partial<Pick<UseFormReturn, 'register'>> {
   name: string
@@ -26,7 +22,7 @@ function Fnr(props: FnrProps) {
         // eslint-disable-next-line react/jsx-props-no-spreading
         {...register(name, {
           required: 'Fnr is required',
-          validate: isValidIdnr,
+          validate: isValidFnr,
         })}
         id="fnr"
       />
diff --git a/frontend/src/components/form/form.tsx b/frontend/src/components/form/form.tsx
index 305eb2d302e1eb138480cff9399447c69d1c0e92..2964e7f2591152ec5e6fb2d5e83fe2ac40861538 100644
--- a/frontend/src/components/form/form.tsx
+++ b/frontend/src/components/form/form.tsx
@@ -22,6 +22,7 @@ export default function Form(props: FormProps) {
               ...{
                 // eslint-disable-next-line react/jsx-props-no-spreading
                 ...child.props,
+                control: methods.control,
                 register: methods.register,
                 errors,
                 key: child.props.name,
diff --git a/frontend/src/components/form/input.tsx b/frontend/src/components/form/input.tsx
index 231a731c4b4be4fd8e98e684373dbba1bbca134f..94e7332f465c98a0fa1561ffb3756ed4c37c8846 100644
--- a/frontend/src/components/form/input.tsx
+++ b/frontend/src/components/form/input.tsx
@@ -1,12 +1,25 @@
 import React from 'react'
+import styled from 'styled-components/macro'
 import { UseFormReturn } from 'react-hook-form'
 
-interface InputProps extends Partial<Pick<UseFormReturn, 'register'>> {
+interface InputProps
+  extends Partial<React.InputHTMLAttributes<HTMLInputElement>>,
+    Partial<UseFormReturn> {
   name: string
   errors?: any
   type?: 'text' | 'email' | 'number'
 }
 
+export const StyledInput = styled.input`
+  width: 50%;
+  border: 1px solid;
+  border-width: 0.013rem;
+  border-radius: 0.3rem;
+  border-color: #ccc;
+  box-shadow: inset 0 0 0 0.013rem #4d4d4d;
+  padding: 0.625rem;
+`
+
 function Input(props: InputProps) {
   // eslint-disable-next-line react/jsx-props-no-spreading
   const { register, name, errors, type, ...rest } = props
@@ -14,7 +27,7 @@ function Input(props: InputProps) {
     return <></>
   }
   // eslint-disable-next-line react/jsx-props-no-spreading
-  return <input {...register(name)} {...rest} />
+  return <StyledInput {...register(name)} {...rest} />
 }
 
 Input.defaultProps = {
diff --git a/frontend/src/components/page/page.tsx b/frontend/src/components/page/page.tsx
index f7b0c7adb6ed8f029d7b8a268134376cbf0ea197..2d95a68cc813c3a214ec92d67108016c3c431d13 100644
--- a/frontend/src/components/page/page.tsx
+++ b/frontend/src/components/page/page.tsx
@@ -1,10 +1,16 @@
 import React from 'react'
 import { Helmet } from 'react-helmet'
+import { useTranslation } from 'react-i18next'
 
 import styled from 'styled-components/macro'
 
 const StyledPage = styled.main`
   display: block;
+  justify-content: space-between;
+  margin: 0 auto;
+  max-width: ${(props) => props.theme.appMaxWidth};
+  padding: ${(props) =>
+    `0.5rem ${props.theme.horizontalPadding} 1rem ${props.theme.horizontalPadding}`};
 `
 
 const StyledPageHeader = styled.h2`
@@ -17,15 +23,18 @@ const StyledPageHeader = styled.h2`
 
 interface IPage {
   children: React.ReactNode
-  header: string
+  header?: string
 }
 
 export default function Page(props: IPage) {
   const { header, children } = props
+  const { i18n, t } = useTranslation()
+  const appTitle = t('common:header:applicationTitle')
 
   return (
     <>
-      <Helmet>
+      <Helmet titleTemplate={`%s - ${appTitle}`} defaultTitle={appTitle}>
+        <html lang={i18n.language} />
         <title>{header}</title>
       </Helmet>
       <StyledPage>
@@ -35,3 +44,7 @@ export default function Page(props: IPage) {
     </>
   )
 }
+
+Page.defaultProps = {
+  header: null,
+}
diff --git a/frontend/src/globalStyles.ts b/frontend/src/globalStyles.ts
index f81dda019fb53dbc61108181c833a0187aa06164..d158638476ed156b47e87db0e1e30432cd210ae0 100644
--- a/frontend/src/globalStyles.ts
+++ b/frontend/src/globalStyles.ts
@@ -1,6 +1,10 @@
 import { createGlobalStyle } from 'styled-components/macro'
 
 const GlobalStyle = createGlobalStyle`
+  html {
+    font-size: clamp(24px, calc(24px * 1vw), 36px);
+  }
+
   body {
     margin: 0;
     min-height: 100vh;
diff --git a/frontend/src/routes/components/header.tsx b/frontend/src/routes/components/header.tsx
index 0a4f62cbdfa3e2ff31d8a8d7a05155ed1abf5361..bb96b6b82cbf60461f09d60366b50d5187da2619 100644
--- a/frontend/src/routes/components/header.tsx
+++ b/frontend/src/routes/components/header.tsx
@@ -1,10 +1,10 @@
 import React from 'react'
+import { Link } from 'react-router-dom'
 import styled from 'styled-components/macro'
 import { useTranslation } from 'react-i18next'
 import LogoBar from '../../components/logobars/LogoBar'
 import LanguageSelector from '../../components/languageselector'
 
-
 const MainWrapper = styled.div`
   color: ${({ theme }) => theme.page.headerColor};
   background-color: ${({ theme }) => theme.page.headerBackgroundColor};
@@ -14,8 +14,9 @@ const Main = styled.div`
   display: flex;
   justify-content: space-between;
   margin: 0 auto;
-  max-width: ${props => props.theme.appMaxWidth};
-  padding: ${props => `0.5rem ${props.theme.horizontalPadding} 1rem ${props.theme.horizontalPadding}`};
+  max-width: ${(props) => props.theme.appMaxWidth};
+  padding: ${(props) =>
+    `0.5rem ${props.theme.horizontalPadding} 1rem ${props.theme.horizontalPadding}`};
 `
 
 const Menu = styled.ul`
@@ -33,15 +34,15 @@ const TitleBox = styled.div`
   padding-left: 3rem;
 `
 
-const ShortTitle = styled.div`
+const Title = styled.div`
   font-size: 2rem;
 `
 
-const LongTitle = styled.div`
+const Description = styled.div`
   font-size: 1rem;
 `
 
-function Header() {
+const Header = () => {
   const { t } = useTranslation('common')
 
   return (
@@ -50,12 +51,14 @@ function Header() {
       <MainWrapper>
         <Main>
           <TitleBox>
-            <ShortTitle>{t('header.applicationTitleShort')}</ShortTitle>
-            <LongTitle>{t('header.applicationTitleLong')}</LongTitle>
+            <Link to="/">
+              <Title>{t('header.applicationTitle')}</Title>
+            </Link>
+            <Description>{t('header.applicationDescription')}</Description>
           </TitleBox>
           <Menu>
             <MenuItem>
-              <LanguageSelector/>
+              <LanguageSelector />
             </MenuItem>
           </Menu>
         </Main>
diff --git a/frontend/src/routes/frontpage/index.tsx b/frontend/src/routes/frontpage/index.tsx
index 826afe4dd56d8a379c1b0a550e6e05310323ed2c..d48137473b76eaebcbd02ee9b2aaaa7432f71fab 100644
--- a/frontend/src/routes/frontpage/index.tsx
+++ b/frontend/src/routes/frontpage/index.tsx
@@ -1,51 +1,27 @@
-import React, { useState } from 'react'
+import React from 'react'
 
-import DateInput from 'components/dateinput'
 import Page from 'components/page'
-import { appTimezone, appVersion, appTheme } from 'appConfig'
-import { Fnr, Form, Select, Input } from 'components/form'
-import { useTranslation } from 'react-i18next'
+import { Debug } from 'components/debug'
+import { Link } from 'react-router-dom'
 
 export default function FrontPage() {
-  const [apiHealth, setApiHealth] = useState('not yet')
-  const [didContactApi, setDidContactApi] = useState(false)
-  const { t } = useTranslation(['common', 'footer'])
-
-  if (!didContactApi) {
-    setDidContactApi(true)
-    fetch('http://localhost:3000/api/health/')
-      .then((res) => res.text())
-      .then((result) => {
-        if (result === 'OK') {
-          setApiHealth('yes')
-        } else {
-          setApiHealth(result)
-        }
-      })
-      // eslint-disable-next-line @typescript-eslint/no-unused-vars
-      .catch((error) => {
-        setApiHealth('error')
-      })
-  }
-
   return (
-    <Page header="Greg main page">
-      <DateInput />
-      <Form onSubmit={() => {}}>
-        <Input name="firstName" />
-        <Input name="lastName" />
-        <Select name="gender" options={['female', 'male', 'other']} />
-        <Fnr name={t('common:fnr')} />
-        <button type="submit">Submit</button>
-      </Form>
-      <br />
-      Version {appVersion}
-      <br />
-      Timezone {appTimezone}
-      <br />
-      Theme {appTheme}
-      <br />
-      API reachable? {apiHealth}
+    <Page>
+      <p>
+        <strong>Routes</strong>
+        <ul>
+          <li>
+            <Link to="/">Front page</Link>
+          </li>
+          <li>
+            <Link to="/sponsor/">Sponsor</Link>
+          </li>
+          <li>
+            <Link to="/register/">Registration</Link>
+          </li>
+        </ul>
+      </p>
+      <Debug />
     </Page>
   )
 }
diff --git a/frontend/src/routes/register/index.tsx b/frontend/src/routes/register/index.tsx
index 2e7736db6ab03af384e17a9c2d9ea3ec75b456b9..629080bdea3a7580c42a5e41c25f53a569aa1c94 100644
--- a/frontend/src/routes/register/index.tsx
+++ b/frontend/src/routes/register/index.tsx
@@ -1,11 +1,78 @@
 import React from 'react'
+import { useTranslation } from 'react-i18next'
 
+import { StyledInput } from 'components/form/input'
+import DateInput from 'components/dateinput'
+import Button from 'components/button'
 import Page from 'components/page'
+import { useForm, Controller, SubmitHandler } from 'react-hook-form'
+import format from 'date-fns/format'
+import { postJsonOpts } from 'utils'
+
+type RegisterFormData = {
+  first_name: string
+  last_name: string
+  date_of_birth: Date
+}
 
 export default function Register() {
+  const { t } = useTranslation(['common'])
+
+  const submit: SubmitHandler<RegisterFormData> = (data) => {
+    const payload = {
+      first_name: data.first_name,
+      last_name: data.last_name,
+      date_of_birth: format(data.date_of_birth, 'yyyy-MM-dd'),
+    }
+    console.log('submitting', JSON.stringify(payload))
+    fetch('http://localhost:3000/api/ui/v1/register/', postJsonOpts(payload))
+      .then((res) => res.text())
+      .then((result) => {
+        console.log('result', result)
+      })
+      .catch((error) => {
+        console.log('error', error)
+      })
+  }
+
+  const {
+    register,
+    control,
+    handleSubmit,
+    // setValue,
+    // formState: { errors },
+  } = useForm<RegisterFormData>()
+  const onSubmit = handleSubmit(submit)
+
   return (
-    <Page header="Register a new guest">
-      <p>Todo</p>
+    <Page header="Register as a guest">
+      <form onSubmit={onSubmit}>
+        <StyledInput
+          {...register(`first_name`)}
+          placeholder={t('common:firstName')}
+        />
+        <StyledInput
+          {...register(`last_name`)}
+          placeholder={t('common:lastName')}
+        />
+        <Controller
+          name="date_of_birth"
+          control={control}
+          render={({ field }) => (
+            <DateInput
+              customInput={<StyledInput />}
+              placeholderText={t('common:dateOfBirth')}
+              onChange={(date) => field.onChange(date)}
+              selected={field.value}
+            />
+          )}
+        />
+        {/* <Select name="gender" options={['female', 'male', 'other']} />
+        <Fnr name={t('common:fnr')} /> */}
+        <Button as="button" type="submit">
+          Submit
+        </Button>
+      </form>
     </Page>
   )
 }
diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..80e6cd182ccc39586653a9f3200ba64bcd862505
--- /dev/null
+++ b/frontend/src/utils/index.ts
@@ -0,0 +1,43 @@
+import validator from '@navikt/fnrvalidator'
+
+export function getCookie(name: string) {
+  if (!document.cookie) {
+    return null
+  }
+
+  const cookies = document.cookie
+    .split(';')
+    .map((c) => c.trim())
+    .filter((c) => c.startsWith(`${name}=`))
+
+  if (cookies.length === 0) {
+    return null
+  }
+  return decodeURIComponent(cookies[0].split('=')[1])
+}
+
+export function maybeCsrfToken() {
+  const csrfToken = getCookie('csrftoken')
+  if (!csrfToken) {
+    return null
+  }
+  return {
+    'X-CSRFToken': csrfToken,
+  }
+}
+
+export function postJsonOpts(data: object): RequestInit {
+  return {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+      ...maybeCsrfToken(),
+    },
+    body: JSON.stringify(data),
+    credentials: 'same-origin',
+  }
+}
+
+export function isValidFnr(data: string): boolean {
+  return validator.idnr(data).status === 'valid'
+}
diff --git a/greg/migrations/0005_person_user.py b/greg/migrations/0005_person_user.py
new file mode 100644
index 0000000000000000000000000000000000000000..bcdd509012f525b71f5343a5333e09573f0f797c
--- /dev/null
+++ b/greg/migrations/0005_person_user.py
@@ -0,0 +1,27 @@
+# Generated by Django 3.2.7 on 2021-09-22 06:57
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ("greg", "0004_ou_deleted_active"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="person",
+            name="user",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name="user",
+                to=settings.AUTH_USER_MODEL,
+            ),
+        ),
+    ]
diff --git a/greg/models.py b/greg/models.py
index 8e1e59fc795328ac590d0fdec0eb7e4f1e87cf6a..363cb2cd5e17b3e2d3bc2a28e01c95187418c99a 100644
--- a/greg/models.py
+++ b/greg/models.py
@@ -1,6 +1,7 @@
 from datetime import date
 
 from dirtyfields import DirtyFieldsMixin
+from django.conf import settings
 from django.db import models
 from django.db.models import Lookup
 from django.db.models.fields import Field
@@ -42,6 +43,13 @@ class Person(BaseModel):
     mobile_phone_verified_date = models.DateField(null=True)
     registration_completed_date = models.DateField(null=True)
     token = models.CharField(max_length=32, blank=True)
+    user = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        on_delete=models.SET_NULL,
+        related_name="user",
+        null=True,
+        blank=True,
+    )
 
     def __str__(self):
         return "{} {} ({})".format(self.first_name, self.last_name, self.pk)
diff --git a/gregsite/settings/base.py b/gregsite/settings/base.py
index 758c3ed9db51bb3632d2b35bb5000a2d7b6f610f..2fd4a30a2c2dd1728464fe436c1f147a049ad04f 100644
--- a/gregsite/settings/base.py
+++ b/gregsite/settings/base.py
@@ -56,15 +56,30 @@ MIDDLEWARE = [
     "django.middleware.common.CommonMiddleware",
     "django.middleware.csrf.CsrfViewMiddleware",
     "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "sesame.middleware.AuthenticationMiddleware",
     "django.contrib.messages.middleware.MessageMiddleware",
     "django.middleware.clickjacking.XFrameOptionsMiddleware",
     "gregsite.middleware.revision_user_middleware.RevisionUserMiddleware",
 ]
 
+AUTHENTICATION_BACKENDS = [
+    "django.contrib.auth.backends.ModelBackend",  # default
+    "sesame.backends.ModelBackend",  # link login
+]
+
+SESAME_MAX_AGE = 600  # lifetime of token in seconds
+SESSION_COOKIE_AGE = 1800  # lifetime of session in seconds
+
+
+CSRF_COOKIE_SAMESITE = "Strict"
+SESSION_COOKIE_SAMESITE = "Strict"
+CSRF_COOKIE_HTTPONLY = True
+SESSION_COOKIE_HTTPONLY = True
+
 REST_FRAMEWORK = {
     "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning",
     "DEFAULT_VERSION": "v1",
-    "ALLOWED_VERSIONS": ("v1",),
+    "ALLOWED_VERSIONS": ("v1", "gregui-v1"),
     "DEFAULT_AUTHENTICATION_CLASSES": (
         "rest_framework.authentication.TokenAuthentication",
         "rest_framework.authentication.SessionAuthentication",
diff --git a/gregsite/urls.py b/gregsite/urls.py
index e0f9859117293eb3708c6123a95cc8c0802b9e4a..e122ba1cd165eb494e7ffdd1c1830ee598ea46f3 100644
--- a/gregsite/urls.py
+++ b/gregsite/urls.py
@@ -17,10 +17,12 @@ from django.contrib import admin
 from django.urls import include, path
 
 from greg import urls as greg_urls
+from gregui import urls as ui_urls
 
 admin.autodiscover()
 
 urlpatterns = [
     path("admin/", admin.site.urls),
     path("", include(greg_urls.urlpatterns)),
+    path("", include(ui_urls.urlpatterns)),
 ]
diff --git a/gregui/api/__init__.py b/gregui/api/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/gregui/api/serializers/__init__.py b/gregui/api/serializers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/gregui/api/serializers/guest.py b/gregui/api/serializers/guest.py
new file mode 100644
index 0000000000000000000000000000000000000000..ecdb652c5de590faede392fc0c269c44b9755df0
--- /dev/null
+++ b/gregui/api/serializers/guest.py
@@ -0,0 +1,21 @@
+from rest_framework import serializers
+
+from greg.models import Person
+
+
+class GuestRegisterSerializer(serializers.ModelSerializer):
+    def create(self, validated_data):
+        obj = super().create(validated_data)
+        return obj
+
+    class Meta:
+        model = Person
+        fields = ("id", "first_name", "last_name", "date_of_birth")
+        read_only_fields = ("id",)
+        extra_kwargs = {
+            "first_name": {"required": True},
+            "last_name": {"required": True},
+            "date_of_birth": {"required": True},
+            # 'email': {'required': True},
+            # 'phone_number': {'required': True},
+        }
diff --git a/gregui/api/urls.py b/gregui/api/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..0d39bbd34b782108f49262803e7722186bba22a7
--- /dev/null
+++ b/gregui/api/urls.py
@@ -0,0 +1,12 @@
+from django.urls import re_path
+
+from rest_framework.routers import DefaultRouter
+
+from gregui.api.views.guest import GuestRegisterView
+
+router = DefaultRouter(trailing_slash=False)
+
+urlpatterns = router.urls
+urlpatterns += [
+    re_path(r"register/$", GuestRegisterView.as_view(), name="guest-register"),
+]
diff --git a/gregui/api/views/__init__,py b/gregui/api/views/__init__,py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/gregui/api/views/guest.py b/gregui/api/views/guest.py
new file mode 100644
index 0000000000000000000000000000000000000000..4d9370203b94acd1563b9a8a1669964841721cc1
--- /dev/null
+++ b/gregui/api/views/guest.py
@@ -0,0 +1,10 @@
+from rest_framework import viewsets, filters, permissions
+from rest_framework.generics import CreateAPIView
+
+from greg.models import Person
+from gregui.api.serializers.guest import GuestRegisterSerializer
+
+class GuestRegisterView(CreateAPIView):
+    queryset = Person.objects.all()
+    permission_classes = [permissions.AllowAny]
+    serializer_class = GuestRegisterSerializer
\ No newline at end of file
diff --git a/gregui/apps.py b/gregui/apps.py
index a0a5e660bdae6f5877d56677541ea8294a2b6db4..21bea517dfe981a67c6f3980d77d99dc6ee1b30b 100644
--- a/gregui/apps.py
+++ b/gregui/apps.py
@@ -2,5 +2,5 @@ from django.apps import AppConfig
 
 
 class GreguiConfig(AppConfig):
-    default_auto_field = 'django.db.models.BigAutoField'
-    name = 'gregui'
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "gregui"
diff --git a/gregui/tests.py b/gregui/tests.py
deleted file mode 100644
index 7ce503c2dd97ba78597f6ff6e4393132753573f6..0000000000000000000000000000000000000000
--- a/gregui/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/gregui/tests/__init__.py b/gregui/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/gregui/tests/api/__init__.py b/gregui/tests/api/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/gregui/tests/api/test_guest.py b/gregui/tests/api/test_guest.py
new file mode 100644
index 0000000000000000000000000000000000000000..17102f7634fb8973f86a34d8e41715bb930025d4
--- /dev/null
+++ b/gregui/tests/api/test_guest.py
@@ -0,0 +1,13 @@
+import pytest
+
+from rest_framework import status
+from rest_framework.reverse import reverse
+
+
+@pytest.mark.django_db
+def test_register_guest(client):
+    data = {"first_name": "Foo", "last_name": "Bar", "date_of_birth": "2020-09-21"}
+    url = reverse("gregui-v1:guest-register")
+    response = client.post(url, data)
+    assert response.status_code == status.HTTP_201_CREATED
+    assert response.json() == {"id": 1, **data}
diff --git a/gregui/tests/conftest.py b/gregui/tests/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..a59673226660f89957c51362bfc0499d0b546832
--- /dev/null
+++ b/gregui/tests/conftest.py
@@ -0,0 +1,22 @@
+from rest_framework.test import APIClient
+from django.contrib.auth import get_user_model
+
+import pytest
+
+from greg.models import (
+    Consent,
+    Notification,
+    Person,
+    Sponsor,
+    SponsorOrganizationalUnit,
+    Identity,
+    Role,
+    RoleType,
+    OrganizationalUnit,
+    ConsentType,
+)
+
+@pytest.fixture
+def client() -> APIClient:
+    client = APIClient()
+    return client
diff --git a/gregui/urls.py b/gregui/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..33070a391fb0b0cfec31461bfc2cbc5bdff826e0
--- /dev/null
+++ b/gregui/urls.py
@@ -0,0 +1,21 @@
+from typing import List
+
+from django.urls import include
+from django.urls import path
+from django.urls.resolvers import URLResolver
+
+from gregui.api import urls as api_urls
+from gregui.views import TokenCreationView
+from . import views
+
+urlpatterns: List[URLResolver] = [
+    path(
+        "api/ui/v1/", include((api_urls.urlpatterns, "gregui"), namespace="gregui-v1")
+    ),
+    path("api/ui/v1/csrf/", views.get_csrf, name="api-csrf"),
+    path("api/ui/v1/logout/", views.logout_view, name="api-logout"),
+    path("api/ui/v1/login/", views.login_view, name="api-login"),
+    path("api/ui/v1/session/", views.SessionView.as_view(), name="api-session"),
+    path("api/ui/v1/whoami/", views.WhoAmIView.as_view(), name="api-whoami"),
+    path("api/ui/v1/token/<email>", TokenCreationView.as_view()),
+]
diff --git a/gregui/views.py b/gregui/views.py
index 91ea44a218fbd2f408430959283f0419c921093e..97d2cde1f2c765bf94921719e565d568802998af 100644
--- a/gregui/views.py
+++ b/gregui/views.py
@@ -1,3 +1,104 @@
-from django.shortcuts import render
+from django.contrib.auth import get_user_model
+from django.contrib.auth import logout
+from django.http import JsonResponse
+from django.middleware.csrf import get_token
+from django.shortcuts import redirect
+from rest_framework import status
+from rest_framework.authentication import SessionAuthentication, BasicAuthentication
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from sesame.utils import get_query_string
 
-# Create your views here.
+from greg.models import Person
+
+
+class TokenCreationView(APIView):
+    """Token creation endpoint"""
+
+    # Allow anyone to request a new query string to be sent to them
+    permission_classes = []
+
+    def _get_username(self, user_model, person: Person) -> str:
+        """Find a free username in the database for a person"""
+        counter = 1
+        while True:
+            username = person.first_name[:3] + person.last_name[:3] + str(counter)
+            if not user_model.objects.filter(username=username).exists():
+                return username
+            counter += 1
+
+    def post(self, request, *args, **kwargs):
+        """
+        Send email to Person with querystring for login
+
+        Persons without a user will have one created for them.
+        """
+        email = self.kwargs["email"]
+        try:
+            person = Person.objects.get(email=email)
+        except Person.DoesNotExist:
+            # Exit if no person with that email (make sure to exit same way as when
+            # person does exist to not leak information)
+            return Response(status=status.HTTP_200_OK)
+
+        # Create user if person does not have one or fetch existing
+        if not person.user:
+            user_model = get_user_model()
+            username = self._get_username(user_model, person)
+            user = user_model.objects.create(username=username)
+            person.user = user
+            person.save()
+        else:
+            user = person.user
+
+        # Create querystring and send email
+        querystring = get_query_string(user)
+        # TODO: send email with query string
+        print(querystring)
+
+        return Response(status=status.HTTP_200_OK)
+
+
+def get_csrf(request):
+    response = JsonResponse({"detail": "CSRF cookie set"})
+    response["X-CSRFToken"] = get_token(request)
+    return response
+
+
+def logout_view(request):
+    if not request.user.is_authenticated:
+        return JsonResponse({"detail": "You're not logged in."}, status=400)
+
+    logout(request)
+    return JsonResponse({"detail": "Successfully logged out."})
+
+
+def login_view(request):
+    """
+    View for pointing login links to
+
+    Sesame will take the query string automatically and use it to create a session for
+    the user, so all this needs to do is redirect the user wherever they're supposed to
+    go after successfully logging in.
+    """
+    # TODO: redirect to whatever path the frontend ends up living at (prob '/')
+    return redirect("/api/ui/v1/whoami/")
+
+
+class SessionView(APIView):
+    authentication_classes = [SessionAuthentication, BasicAuthentication]
+    permission_classes = [IsAuthenticated]
+
+    @staticmethod
+    def get(request, format=None):
+        return JsonResponse({"isAuthenticated": True})
+
+
+class WhoAmIView(APIView):
+    authentication_classes = [SessionAuthentication, BasicAuthentication]
+    permission_classes = [IsAuthenticated]
+
+    @staticmethod
+    def get(request, format=None):
+        return JsonResponse({"username": request.user.username})
diff --git a/poetry.lock b/poetry.lock
index ff64d048394a119373a4f00f5b6f4b5b5ac9875d..4b7b8a531cd336f555f4e756d941fd0defe27f69 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -240,6 +240,21 @@ python-versions = ">=3.6"
 [package.dependencies]
 django = ">=2.0"
 
+[[package]]
+name = "django-sesame"
+version = "2.4"
+description = "Frictionless authentication with \"Magic Links\" for your Django project."
+category = "main"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[package.dependencies]
+django = ">=2.2"
+ua-parser = {version = ">=0.10", optional = true, markers = "extra == \"ua\""}
+
+[package.extras]
+ua = ["ua-parser (>=0.10)"]
+
 [[package]]
 name = "django-stubs"
 version = "1.9.0"
@@ -1007,6 +1022,14 @@ category = "main"
 optional = false
 python-versions = "*"
 
+[[package]]
+name = "ua-parser"
+version = "0.10.0"
+description = "Python port of Browserscope's user agent parser"
+category = "main"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "uritemplate"
 version = "3.0.1"
@@ -1058,7 +1081,7 @@ python-versions = "*"
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.9"
-content-hash = "75c30a6f1bcd72a8d9725dbe92509be0fab8bf9e498ceaaf0a730f2337f439b0"
+content-hash = "4854aed8a90a9f2308063cc6e8151623de62b9bed714c49a8fde3e66e7165d86"
 
 [metadata.files]
 appnope = [
@@ -1195,6 +1218,10 @@ django-reversion = [
     {file = "django-reversion-4.0.0.tar.gz", hash = "sha256:ad6d714b4b9b824e22b88d47201cc0f74b5c4294c8d4e1f8d7ac7c3631ef3188"},
     {file = "django_reversion-4.0.0-py3-none-any.whl", hash = "sha256:f059c654e38c0dd8dccd7f0990aa2f6d9ad22dab55c5e095f9596aeda8079dcd"},
 ]
+django-sesame = [
+    {file = "django-sesame-2.4.tar.gz", hash = "sha256:4f232ba1c3642b4eef23257af9a2feac36970457d2940f4083c48c6a3d9cb0a3"},
+    {file = "django_sesame-2.4-py3-none-any.whl", hash = "sha256:b0570b610ec2f3602ff12d43b5bca41c4a37a3079e6a0170ceccc69689446b87"},
+]
 django-stubs = [
     {file = "django-stubs-1.9.0.tar.gz", hash = "sha256:664843091636a917faf5256d028476559dc360fdef9050b6df87ab61b21607bf"},
     {file = "django_stubs-1.9.0-py3-none-any.whl", hash = "sha256:59c9f81af64d214b1954eaf90f037778c8d2b9c2de946a3cda177fefcf588fbd"},
@@ -1654,6 +1681,10 @@ typing-extensions = [
     {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
     {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
 ]
+ua-parser = [
+    {file = "ua-parser-0.10.0.tar.gz", hash = "sha256:47b1782ed130d890018d983fac37c2a80799d9e0b9c532e734c67cf70f185033"},
+    {file = "ua_parser-0.10.0-py2.py3-none-any.whl", hash = "sha256:46ab2e383c01dbd2ab284991b87d624a26a08f72da4d7d413f5bfab8b9036f8a"},
+]
 uritemplate = [
     {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"},
     {file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"},
diff --git a/pyproject.toml b/pyproject.toml
index 9af43a411dab7960a20861cd13135f5b29e4624b..c6c6c13e52494c91b840e4bcb33d7c0527b137ef 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,6 +22,7 @@ python-json-logger = "*"
 sentry-sdk = "*"
 whitenoise = "*"
 django-reversion = "*"
+django-sesame = {extras = ["ua"], version = "^2.4"}
 
 [tool.poetry.dev-dependencies]
 Faker = "*"