diff --git a/Dockerfile b/Dockerfile
index 1653996e773a364e4ba0c236d2216e9389329b8c..f6db5a421d2d41d84d9ac02c5cbb8300534c2de0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,12 +1,15 @@
-FROM mhart/alpine-node:15 as base
+FROM mhart/alpine-node:14 as base
 
 WORKDIR /app
 COPY package*.json ./
 RUN npm install --only=production --unsafe-perm
 
-FROM mhart/alpine-node:slim-15 as api
+FROM mhart/alpine-node:slim-14 as api
+ARG APP_VERSION
+
 LABEL maintainer="Louis Capitanchik <louis@jetsam.tech>"
 LABEL description="The Jetsam API server"
+LABEL version=$APP_VERSION
 
 RUN apk add --no-cache bash
 
diff --git a/Makefile b/Makefile
index 89609969c797e0b54289adf198f2d952d04762d2..bd5121cd40586a6a581a9315732810e852ef7a53 100644
--- a/Makefile
+++ b/Makefile
@@ -4,6 +4,9 @@ TAG=latest
 REMOTE_TAG=$(TAG)
 REMOTE=registry.digitalocean.com
 
+CHART_REPO=https://museum.lcr.gr/api/charts
+CHART_TAG=0.0.0
+
 export
 
 build:
@@ -19,6 +22,14 @@ push: tag
 
 docker: build tag push
 
+chart:
+	helm package helm/api
+
+museum:
+	curl --data-binary "@api-$(CHART_TAG).tgz" "$(CHART_REPO)"
+
+helm: chart museum
+
 hasura-claims:
 	@echo
 	@envsubst < hasura/claims_config.json | jq -c .
diff --git a/docker-compose.yml b/docker-compose.yml
index ff758fb2d366c1cd575477a18df004aeae174aca..2ae61a811f85d119bc24d4b2a30768d11a7782ce 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -33,6 +33,15 @@ services:
       - "5432:5432"
     labels:
       tech.jetsam.environment: 'staging'
+  graphql-engine:
+    image: hasura/graphql-engine:v2.0.10
+    restart: on-failure
+    ports:
+      - "15432:8080"
+    depends_on:
+      - "postgres"
+    env_file:
+      - hasura/.env
 #  hsaura:
 #    image: hasura/graphql-engine:v1.3.3
 #    ports:
diff --git a/helm/api/.helmignore b/helm/api/.helmignore
index 0e8a0eb36f4ca2c939201c0d54b5d82a1ea34778..54be7e08c62ce99461627c4f31ba17f51836999f 100644
--- a/helm/api/.helmignore
+++ b/helm/api/.helmignore
@@ -21,3 +21,4 @@
 .idea/
 *.tmproj
 .vscode/
+.dck/
diff --git a/helm/api/Chart.yaml b/helm/api/Chart.yaml
index 2184794c60b81eb17848f8734eb244151b316af1..8bc50c1b0216a2dcf5a6bd40b542a1eab6e531d2 100644
--- a/helm/api/Chart.yaml
+++ b/helm/api/Chart.yaml
@@ -15,10 +15,10 @@ type: application
 # This is the chart version. This version number should be incremented each time you make changes
 # to the chart and its templates, including the app version.
 # Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 0.2.10
+version: 0.3.0
 
 # This is the version number of the application being deployed. This version number should be
 # incremented each time you make changes to the application. Versions are not expected to
 # follow Semantic Versioning. They should reflect the version the application is using.
 # It is recommended to use it with quotes.
-appVersion: "2.0.0.beta-4"
+appVersion: "2.2.0"
diff --git a/helm/api/values.yaml b/helm/api/values.yaml
index 10a5fbd2a92cce56292ee1ad021df3e9ac792e2f..968edbf1a581c2700df7de9ec9019646975e7d1b 100644
--- a/helm/api/values.yaml
+++ b/helm/api/values.yaml
@@ -29,7 +29,7 @@ secrets:
 
 
 image:
-  repository: lcr.gr/jetsam/api
+  repository: registry.digitalocean.com/jetsam/api
   pullPolicy: IfNotPresent
   # Overrides the image tag whose default is the chart appVersion.
   tag: "latest"
diff --git a/package-lock.json b/package-lock.json
index 30b24c68da48da9de382646c64625af504175933..a2d9da7a5c3db490c61358954473d62e9a429355 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "jetsam-api",
-  "version": "2.0.0-beta.2",
+  "version": "2.2.2",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "jetsam-api",
-      "version": "2.0.0-beta.2",
+      "version": "2.2.2",
       "license": "GPL-3.0+",
       "dependencies": {
         "@commander-lol/vault-client": "^0.1.1",
@@ -59,6 +59,7 @@
         "yargs": "^13.3.2"
       },
       "devDependencies": {
+        "hasura-cli": "^2.0.9",
         "jest": "^26.6.3",
         "nodemon": "^2.0.4",
         "prettier": "^2.2.1",
@@ -4778,6 +4779,23 @@
       "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz",
       "integrity": "sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ=="
     },
+    "node_modules/hasura-cli": {
+      "version": "2.0.9",
+      "resolved": "https://registry.npmjs.org/hasura-cli/-/hasura-cli-2.0.9.tgz",
+      "integrity": "sha512-95xAxNFfF1nntncULGKGQ9UEbhEWsgcMHdqOLsreq9E1emh2CVu1xuY/WezGMaCe1D4ZII7HxSQZBIhdnF9vKg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "dependencies": {
+        "axios": "^0.21.1",
+        "chalk": "^2.4.2"
+      },
+      "bin": {
+        "hasura": "hasura"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/header-case": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.3.tgz",
@@ -16123,6 +16141,16 @@
       "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz",
       "integrity": "sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ=="
     },
+    "hasura-cli": {
+      "version": "2.0.9",
+      "resolved": "https://registry.npmjs.org/hasura-cli/-/hasura-cli-2.0.9.tgz",
+      "integrity": "sha512-95xAxNFfF1nntncULGKGQ9UEbhEWsgcMHdqOLsreq9E1emh2CVu1xuY/WezGMaCe1D4ZII7HxSQZBIhdnF9vKg==",
+      "dev": true,
+      "requires": {
+        "axios": "^0.21.1",
+        "chalk": "^2.4.2"
+      }
+    },
     "header-case": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.3.tgz",
diff --git a/package.json b/package.json
index 59878e97d640c0fcbfb6dcc52083841f5c1d51b5..126ac92971e250bee18721b0ca37c6ebaed234f9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "jetsam-api",
-  "version": "2.2.0",
+  "version": "2.2.2",
   "description": "The Jetsam App API Server",
   "main": "server.js",
   "scripts": {
@@ -68,6 +68,7 @@
     "yargs": "^13.3.2"
   },
   "devDependencies": {
+    "hasura-cli": "^2.0.9",
     "jest": "^26.6.3",
     "nodemon": "^2.0.4",
     "prettier": "^2.2.1",
diff --git a/public/css/jetsam.css b/public/css/jetsam.css
index 17beca4d722f6a0aac94281da44160a75b6e970c..0dce446709ed754e0bd4319a690da62e151b8bf8 100644
--- a/public/css/jetsam.css
+++ b/public/css/jetsam.css
@@ -12,6 +12,11 @@
 	--colour-dark-grey: #697077;
 	--colour-light-grey: #DDE1E6;
 
+	--colour-success: #48BB78;
+	--colour-success-dark: #25855A;
+	--colour-danger: #F56565;
+	--colour-danger-dark: #C53030;
+
 	--padding-block: 4rem;
 
 	--gradient-jetsam: linear-gradient(135deg, var(--colour-jetsam-light) 0%, var(--colour-jetsam-dark) 100%);
@@ -34,7 +39,7 @@ body {
 	line-height: 19px;
 }
 
-p {
+h1, h2, h3, h4, h5, p {
 	margin: 0;
 }
 
@@ -71,6 +76,10 @@ body, .font-body {
 	font-size: 12px;
 	line-height: 14px;
 }
+.text.head {
+	font-size: 32px;
+	line-height: 38px;
+}
 .text.bold {
 	font-weight: 500;
 }
@@ -158,6 +167,12 @@ body, .font-body {
 	background-color: var(--colour-light-grey);
 	transition: background-color 0.1s linear;
 }
+
+.vertical-block {
+	padding-top: 10px;
+	padding-bottom: 10px;
+}
+
 .form .control input:focus {
 	border-color: var(--colour-jetsam);
 	background-color: var(--colour-white);
@@ -189,6 +204,34 @@ body, .font-body {
 	background-color: var(--colour-jetsam-dark);
 }
 
+.button.success {
+	color: var(--colour-white);
+	background-color: var(--colour-success);
+}
+.button.success:hover {
+	background-color: var(--colour-success-dark);
+}
+
+.button.danger {
+	color: var(--colour-white);
+	background-color: var(--colour-danger);
+}
+.button.danger:hover {
+	background-color: var(--colour-danger-dark);
+}
+
+.content {
+	width: 100%;
+	max-width: var(--breakpoint-small);
+	border-radius: 12px;
+	align-self: center;
+	padding: 20px;
+}
+
+.content.focus {
+	background-color: var(--colour-white);
+}
+
 @media (max-width: 720px) {
 	:root {
 		--padding-block: 2rem;
@@ -197,4 +240,8 @@ body, .font-body {
 	.split-container {
 		flex-direction: column;
 	}
+
+	.focus-content {
+		border-radius: 0;
+	}
 }
\ No newline at end of file
diff --git a/src/config/app.js b/src/config/app.js
index 28b9fe507d3a5c6da03213a893a68da93ddc16fb..6cdbb052505bb40da2517c53f3d3513f682dac51 100644
--- a/src/config/app.js
+++ b/src/config/app.js
@@ -17,6 +17,11 @@ module.exports = {
 		public_key_b64: env('RSA_PUBLIC_KEY_B64', null),
 		private_key: null,
 		private_key_b64: env('RSA_PRIVATE_KEY_B64', null),
+		key_id: env('JWK_KEY_ID', null),
+		super_auth_clients: env('ADMIN_AUTH_CLIENTS', '')
+			.split(',')
+			.map(s => s.trim())
+			.filter(Boolean)
 	},
 }
 
diff --git a/src/core/utils/jwt.js b/src/core/utils/jwt.js
index 8c4ba3480365c33f395fb4d4cfeabf044a785591..13832755faa97c5a3afd81e3284765090f4f6199 100644
--- a/src/core/utils/jwt.js
+++ b/src/core/utils/jwt.js
@@ -75,6 +75,7 @@ exports.loadKeys = async () => {
 
 exports.sign = async payload => {
 	const threadContext = require('core/injection/ThreadContext')
+	const { config } = require('bootstrap')
 	const { default: SignJWT } = require('jose/jwt/sign')
 	const { priv } = exports.getKeys()
 
@@ -82,7 +83,7 @@ exports.sign = async payload => {
 		new SignJWT(payload)
 			.setIssuer(exports.jwtOptions.issuer)
 			.setIssuedAt()
-			.setProtectedHeader({ alg: 'RS256' })
+			.setProtectedHeader({ alg: 'RS256', kid: exports.jwtOptions.keyid_prefix + config('app.security.key_id') })
 			.sign(priv),
 	)
 }
@@ -99,6 +100,12 @@ exports.verify = async token => {
 	})
 }
 
+exports.getClaims = tokenPayload => {
+	return tokenPayload[exports.jwtOptions.claims]
+}
+
 exports.jwtOptions = {
 	issuer: 'urn:jetsam:systems:auth',
+	claims: 'urn:jetsam:resources:claims',
+	keyid_prefix: 'urn:jetsam:jwk:'
 }
diff --git a/src/core/utils/urls.js b/src/core/utils/urls.js
index ff3b8329d7a6ddafd418a4522469583dd1dead0b..2f04912890771a49b43ce85b2bf5d97f0eb04604 100644
--- a/src/core/utils/urls.js
+++ b/src/core/utils/urls.js
@@ -1,6 +1,8 @@
 const { URL } = require('url')
 const qs = require('querystring')
 const { unset, config } = require('bootstrap')
+const crypto = require('core/utils/crypto')
+const {report} = require("../../http/middleware/SentryReporter");
 
 exports.createUrl = (host, path, query = unset) => {
 	const url = new URL(path, host)
@@ -18,3 +20,59 @@ exports.queryValueToArray = (value = '') =>
 		.split(',')
 		.map(s => s.trim())
 		.filter(Boolean)
+
+const redirectPairs = [
+	['authorize', '/auth/authorize']
+]
+
+const nameToUrl = redirectPairs.reduce((cur, [k, v]) => ({ ...cur, [k]: v }), {})
+const urlToName = redirectPairs.reduce((cur, [k, v]) => ({ ...cur, [v]: k }), {})
+
+exports.createRedirectState = async ctx => {
+	const path = ctx.path
+	const redirect = urlToName[path] ?? '/'
+	const query = ctx.request.query ?? {}
+	return crypto.encrypt(JSON.stringify({ redirect, query }))
+}
+
+exports.parseRedirectState = async ctx => {
+	const state = ctx.request?.query?.login_state
+	if (!state) {
+		return {
+			path: '/',
+			redirect: 'missing',
+			query: {}
+		}
+	}
+
+	try {
+		const raw = await crypto.decrypt(state)
+		const value = JSON.parse(raw)
+		return {
+			...value,
+			path: nameToUrl[value.redirect] ?? '/',
+		}
+	} catch(e) {
+		console.error(e)
+		await report(e, ctx)
+	}
+
+	return {
+		path: '/',
+		redirect: 'missing',
+		query: {}
+	}
+}
+
+exports.createRedirectedUrl = async (ctx) => {
+	const values = await exports.parseRedirectState(ctx)
+	const params = new URLSearchParams()
+
+	console.log(values, params)
+
+	Object.entries(values.query).forEach(([key, value]) => {
+		params.set(key, value)
+	})
+
+	return `${ values.path }?${ params.toString() }`
+}
\ No newline at end of file
diff --git a/src/database/models/OAuthClient.js b/src/database/models/OAuthClient.js
index dd6ad12764961561f9f35212fa03d3bfafc2a5ab..5eaa4d320f7645e1ea3bad60fdb51bf134613ce0 100644
--- a/src/database/models/OAuthClient.js
+++ b/src/database/models/OAuthClient.js
@@ -14,11 +14,13 @@ class OAuthClient extends BaseModel {
 		this.hasMany(models.RefreshToken, { foreignKey: 'client_id' })
 	}
 
-	static async generateClient(userId, internal = false) {
+	static async generateClient(userId, params = {}, internal = false) {
 		const crypto = require('core/utils/crypto')
 		const secret = await crypto.secureHexString(32)
 		return this.create({
 			owner_id: userId,
+			name: params.name ?? '',
+			description: params.description ?? '',
 			secret,
 			grant_types: ['authorization_code', 'refresh_token'],
 			internal,
diff --git a/src/database/models/Survey.js b/src/database/models/Survey.js
index 8595c71a546a2955f199af130cb655e8e6420cc6..dbe253de1053f673195a60087c8d68261d072254 100644
--- a/src/database/models/Survey.js
+++ b/src/database/models/Survey.js
@@ -29,6 +29,8 @@ class Survey extends BaseModel {
 		const value = this.toJSON()
 		if (this.SurveyUser) {
 			value.answers = this.SurveyUser.toJSON().properties
+		} else if (this.SurveyUsers?.length === 1) {
+			value.answers = this.SurveyUsers[0].toJSON().properties
 		}
 		return value
 	}
diff --git a/src/database/models/User.js b/src/database/models/User.js
index 03e22efa8aec64d735c027970cff92211716dc37..25566ecf94b4e36a9dd14f99eb1a25503c5e2feb 100644
--- a/src/database/models/User.js
+++ b/src/database/models/User.js
@@ -1,5 +1,6 @@
 const timestamps = require('./properties/timestamps')
 const BaseModel = require('./BaseModel')
+const {jwtOptions, getClaims} = require("../../core/utils/jwt");
 
 class User extends BaseModel {
 	static associate(models) {
@@ -59,7 +60,12 @@ class User extends BaseModel {
 	static async idFromJwt(token) {
 		const { verify } = require('core/utils/jwt')
 		const payload = await verify(token)
-		return payload.session.id
+
+		if (payload.session) {
+			return payload.session.id
+		} else {
+			return getClaims(payload)?.['user-id']
+		}
 	}
 
 	static async idFromOpaque(token) {
@@ -116,9 +122,10 @@ class User extends BaseModel {
 		const { sign } = require('core/utils/jwt')
 		const roles = await this.getAuthRoles()
 		return await sign({
-			session: {
-				id: this.id,
-				roles,
+			[jwtOptions.claims]: {
+				'user-id': this.id,
+				'default-role': roles[0],
+				'allowed-roles': roles,
 			},
 			...extras,
 		})
diff --git a/src/domain/auth/AuthServer.js b/src/domain/auth/AuthServer.js
index 6a52adc0161a9b909d66ed71aba19a503340fbad..fed5552c8c8dc0c999c15dc921c5e43b3807fbd5 100644
--- a/src/domain/auth/AuthServer.js
+++ b/src/domain/auth/AuthServer.js
@@ -24,7 +24,7 @@ function createTokenPair(user, client, access, refresh = {}) {
 
 async function createClientEncryptedToken(client, userModel, scope) {
 	const user = await User.findByPk(userModel.id)
-	const payload = user.asJWTToken({ cid: client.id, scope })
+	const payload = user.asJWTToken({ sub: user.id, aud: [client.id], azp: client.id, scope })
 	return payload
 }
 
@@ -200,14 +200,12 @@ class KoaOAuthServer {
 		this.authorize = async ctx => {
 			const OAuthFlow = require('./OAuthFlow')
 			const flow = await OAuthFlow.initialiseFlow(ctx)
-			const { user, redirect } = flow
+			const { user, redirect, action } = flow
 
-			if (!user) {
-				return ctx.redirect(`/login?auth_state=${redirect}`)
-			} else if (ctx.method === 'GET') {
+			if (ctx.method === 'GET') {
 				return await OAuthFlow.showOAuthConsent(ctx, flow)
 			} else {
-				if (ctx.request.query.action === 'deny') {
+				if (action === 'deny') {
 					return await OAuthFlow.handleConsentRejection(ctx, flow)
 				}
 				return await OAuthFlow.handleConsentAcceptance(ctx, flow, this)
@@ -225,6 +223,7 @@ class KoaOAuthServer {
 		this.token = async ctx => {
 			const { req, res } = this.transformContext(ctx)
 
+			console.log(ctx.request.query)
 			console.log(ctx.request.body)
 
 			await authServer.token(req, res, {
@@ -235,8 +234,6 @@ class KoaOAuthServer {
 				ctx.response.set(name, value)
 			}
 
-			console.log(res.body)
-
 			ctx.response.status = res.status
 			ctx.response.body = res.body
 		}
diff --git a/src/domain/auth/OAuthFlow.js b/src/domain/auth/OAuthFlow.js
index fa6af3032a4fedb67679f42c08be9ab2b04dd749..665c76227e31dd78896139727191346421c30051 100644
--- a/src/domain/auth/OAuthFlow.js
+++ b/src/domain/auth/OAuthFlow.js
@@ -1,6 +1,8 @@
 const { OAuthClient } = require('database/models')
 const HttpError = require('core/errors/HttpError')
 const crypto = require('core/utils/crypto')
+const {config} = require("bootstrap");
+const debug = require('debug')('server:oauth:flow')
 
 /**
  * Retrieve the correct query value and format auth state for an oauth request
@@ -11,21 +13,19 @@ exports.initialiseFlow = async ctx => {
 	const user = await ctx.services['core.auth'].getUser()
 
 	let baseQuery = ctx.request.query
-	let queryState = null
-
-	console.log(baseQuery)
+	let queryState = baseQuery
 
 	if (baseQuery.auth_state) {
 		queryState = JSON.parse(await crypto.decrypt(baseQuery.auth_state))
 		if (queryState.query) {
 			queryState = queryState.query
+		} else {
+			queryState = null
 		}
 	}
 
 	const action = baseQuery.action
 
-	console.log({ redirect: 'authorize', query: baseQuery })
-
 	const redirectState = await crypto.encrypt(
 		JSON.stringify({ redirect: 'authorize', query: baseQuery }),
 	)
@@ -40,16 +40,32 @@ exports.initialiseFlow = async ctx => {
 }
 
 exports.showOAuthConsent = async (ctx, queryState) => {
-	const { user, query, redirect } = queryState
+	let { user, query, redirect } = queryState
+
+	if (query.auth_state) {
+		try {
+			const realState = JSON.parse(await crypto.decrypt(query.auth_state))
+			query = realState.query
+		} catch(e) {
+			console.error(e)
+			throw new HttpError(500, 'Bad auth state')
+		}
+	}
+
+	const [scopes, isPrivileged] = describeScopeRequest(query.scope)
+
+	if (isPrivileged) {
+		const allowed = new Set(config('app.security.super_auth_clients'))
+		if (!allowed.has(query.client_id)) {
+			throw new HttpError(401, 'This app is not allowed to request certain permissions')
+		}
+	}
 
 	const client = await OAuthClient.findOne({ where: { id: query.client_id } })
 	if (client == null) {
 		throw new HttpError(400, 'Invalid client id specified')
 	}
 
-	const scopes = describeScopeRequest(query.scope)
-
-	console.log(client)
 
 	return ctx.render('auth/accept-oauth', {
 		user,
@@ -59,8 +75,20 @@ exports.showOAuthConsent = async (ctx, queryState) => {
 	})
 }
 
+async function getOauthQueryFromFlow(flow) {
+	let { state: queryState } = flow
+
+	if (queryState.auth_state) {
+		const rawQueryState = await crypto.decrypt(queryState.auth_state)
+		queryState = JSON.parse(rawQueryState).query
+	}
+
+	return queryState
+}
+
 exports.handleConsentRejection = async (ctx, flow) => {
-	const { redirect_uri } = flow.query
+	const queryState = await getOauthQueryFromFlow(flow)
+	const { redirect_uri } = queryState
 	const redirect = new URL(redirect_uri, 'http://localhost')
 	const search = new URLSearchParams(redirect.searchParams)
 
@@ -69,20 +97,18 @@ exports.handleConsentRejection = async (ctx, flow) => {
 		'error_description',
 		'The user has denied the requested permissions',
 	)
+	if (queryState.state) {
+		search.set('state', queryState.state)
+	}
 	redirect.search = search.toString()
 
-	ctx.set('Location', redirect.toString())
-	ctx.status = 302
-	ctx.body = null
+	return await ctx.redirect(redirect.toString(), 302)
 }
 
 exports.handleConsentAcceptance = async (ctx, flow, server) => {
-	const { state: queryState } = flow
-	if (!queryState.state) {
-		queryState.state = crypto.insecureHexString(32)
-	}
-
+	const queryState = await getOauthQueryFromFlow(flow)
 	const { req, res } = server.transformContext(ctx, { query: queryState })
+
 	await server.getAuthServer().authorize(req, res, {
 		authenticateHandler: {
 			handle() {
@@ -93,6 +119,7 @@ exports.handleConsentAcceptance = async (ctx, flow, server) => {
 	for (const [name, value] of Object.entries(res.headers)) {
 		ctx.response.set(name, value)
 	}
+
 	ctx.response.status = res.status
 }
 
@@ -103,6 +130,18 @@ const scopeDescriptionMap = {
 		description:
 			'Full access to your account, including the ability to create, update and delete any user information, metrics and files.',
 	},
+	'admin': {
+		icon: 'admin',
+		name: 'Admin System Access',
+		description:
+			'Access to Jetsam backend data. Access requests for this permission are logged, and will only be granted to privileged users',
+	},
+	'openid': {
+		icon: 'openid',
+		name: 'Open ID Connect',
+		description:
+			'Use your Jetsam account to log in to a third party services. This will provide the third party with your name (if provided), your email address and your Jetsam account ID.'
+	},
 	'metrics:create': {
 		name: 'Create Metrics',
 		description:
@@ -133,7 +172,32 @@ const scopeDescriptionMap = {
 			'The ability to see information about your account stats, including your points and citizen scientist level',
 	},
 }
+
+/**
+ * @typedef {object} ScopeDescription
+ * @property {string} name
+ * @property {string} description
+ */
+
+/**
+ * @param scope
+ * @return {[ScopeDescription[], boolean]}
+ */
 function describeScopeRequest(scope = '*') {
-	const scopes = scope.split(' ')
-	return scopes.map(s => scopeDescriptionMap[s]).filter(Boolean)
+	let hasAdminRequest = false
+
+	console.log(scope)
+	let scopes = []
+	if (scope.includes(',')) {
+		scopes = scope.split(',')
+	} else {
+		scopes = scope.split(' ')
+	}
+
+	return [scopes.map(s => {
+		if (s.startsWith('admin') || s === '*') {
+			hasAdminRequest = true
+		}
+		return scopeDescriptionMap[s];
+	}).filter(Boolean), hasAdminRequest]
 }
diff --git a/src/domain/data/MetricsService.js b/src/domain/data/MetricsService.js
index 8aa9027bb87d283988597cd0ff8e435394821635..6775c843d5c443488867d21435e9ff1476f1ec32 100644
--- a/src/domain/data/MetricsService.js
+++ b/src/domain/data/MetricsService.js
@@ -18,7 +18,7 @@ module.exports = class MetricsService extends ContextualModule {
 
 	async recordMetric(value, type, location, meta = {}, transaction) {
 		const user = await this.ctx.services['core.auth'].getUser()
-		const surveys = await user.getSurveys({
+		const surveys = user ? await user.getSurveys({
 			where: {
 				expires_at: {
 					[Sequelize.Op.gt]: moment.utc().toISOString()
@@ -28,7 +28,7 @@ module.exports = class MetricsService extends ContextualModule {
 				},
 				public: true,
 			},
-		})
+		}) : []
 
 		if (surveys.length > 0) {
 			meta.surveys = surveys.map(s => s.id)
diff --git a/src/http/controllers/api/content.js b/src/http/controllers/api/content.js
index c688964594d169f2f912cdcb31ca3d810cb1eb33..1f126df9f4a691a6499f3268974f72223aad764c 100644
--- a/src/http/controllers/api/content.js
+++ b/src/http/controllers/api/content.js
@@ -144,7 +144,5 @@ exports.getWithin = async ctx => {
 		toDate.toISOString(),
 	)
 
-
-	console.log(metrics)
 	ctx.body = { metrics }
 }
diff --git a/src/http/controllers/api/oauth.js b/src/http/controllers/api/oauth.js
index 643b2cdb93007e935918f21d4f42b00966f240f8..d8c99c11d8723f39c24d90f44d1f94c29acf5019 100644
--- a/src/http/controllers/api/oauth.js
+++ b/src/http/controllers/api/oauth.js
@@ -7,10 +7,11 @@ exports.createClient = async ctx => {
 		throw new HttpError(403, 'You must be logged in to create an OAuth client')
 	}
 
-	const client = await OAuthClient.generateClient(user.id)
+	const { name, description } = ctx.request.body
+	const client = await OAuthClient.generateClient(user.id, { name, description })
 
 	ctx.body = {
-		client: client.toOAuthInterface(),
+		client: client.toJSON(),
 	}
 }
 exports.listClients = async ctx => {
@@ -22,7 +23,7 @@ exports.listClients = async ctx => {
 	const clients = await user.getOAuthClients()
 
 	ctx.body = {
-		clients: clients.map(c => c.toOAuthInterface()),
+		clients: clients.map(c => c.toJSON()),
 	}
 }
 
@@ -50,7 +51,7 @@ exports.addClientRedirect = async ctx => {
 		await client.save()
 	}
 
-	ctx.body = { client: client.toOAuthInterface() }
+	ctx.body = { client: client.toJSON() }
 }
 
 exports.removeClientRedirect = async ctx => {
@@ -77,5 +78,5 @@ exports.removeClientRedirect = async ctx => {
 
 		await client.save()
 	}
-	ctx.body = { client: client.toOAuthInterface() }
+	ctx.body = { client: client.toJSON() }
 }
diff --git a/src/http/controllers/api/v2/surveys.js b/src/http/controllers/api/v2/surveys.js
index 8a4251a025d3892e1f18b7cebcd222691e839b76..07127a5e9b9db3bed7fb359bd2f9ac3ed251092f 100644
--- a/src/http/controllers/api/v2/surveys.js
+++ b/src/http/controllers/api/v2/surveys.js
@@ -5,6 +5,17 @@ const HttpError = require("../../../../core/errors/HttpError");
 const {Survey, SurveyUser, User, sequelize} = require('database/models')
 const {QueryTypes} = require('sequelize')
 
+exports.get = async ctx => {
+	const {survey} = ctx.models
+	if (survey) {
+		ctx.body = {
+			survey: survey.asOwnSurvey()
+		}
+	} else {
+		throw new NotFoundError('Survey')
+	}
+}
+
 exports.list = async ctx => {
 	const {Sequelize, Survey} = require('database/models')
 	const user = await ctx.services['core.auth'].getUser()
@@ -28,7 +39,6 @@ exports.list = async ctx => {
 			public: true,
 		},
 		include: includes
-	}, {
 	})
 
 	ctx.body = {
@@ -70,8 +80,6 @@ exports.join = async ctx => {
 	const {survey} = ctx.models
 	const {properties = {}} = ctx.request.body
 
-	console.log(survey.properties, properties)
-
 	if (Object.values(survey.properties).length !== Object.values(properties).length) {
 		throw new HttpError(400, `Must provide values for all properties. Expecting: [${Object.keys(survey.properties).join(', ')}]; Found [${Object.keys(properties).join(', ')}] `)
 	}
@@ -144,8 +152,6 @@ exports.leave = async ctx => {
 		type: QueryTypes.DELETE
 	})
 
-	console.log(result)
-
 	ctx.body = {
 		success: true
 	}
diff --git a/src/http/controllers/auth.js b/src/http/controllers/auth.js
index 2b5b170be0d90deb20076e6a206873f4e7536293..1284e3b269e41f6d0745c92c0a958b705642467f 100644
--- a/src/http/controllers/auth.js
+++ b/src/http/controllers/auth.js
@@ -1,5 +1,6 @@
 const crypto = require('core/utils/crypto')
 const moment = require('moment')
+const {createRedirectedUrl} = require("../../core/utils/urls");
 
 exports.login = async ctx => {
 	const { email, password } = ctx.request.body
@@ -12,25 +13,42 @@ exports.logout = async ctx => {
 	return ctx.redirect('/')
 }
 
-exports.handleLoginRedirect = async ctx => {
-	const query = ctx.request.query
-
-	if (query.login_state || query.auth_state) {
-		const { login_state, auth_state } = ctx.request.query
-		const state = login_state ?? auth_state
-
-		const values = JSON.parse(await crypto.decrypt(state))
-
-		if (values.redirect === 'authorize') {
-			return ctx.redirect(
-				`/auth/authorize?auth_state=${state}`,
-			)
-		} else {
-			return ctx.redirect('/')
-		}
-	} else {
-		return ctx.redirect('/')
+exports.showLogin = async ctx => {
+	const data = {}
+	const state = new URLSearchParams()
+	if (ctx.query.login_state) {
+		state.set('login_state',  ctx.query.login_state)
+	}
+	if (ctx.query.auth_state) {
+		state.set('auth_state', ctx.query.auth_state)
 	}
+
+	data.state_string = `?${ state.toString() }`
+	return ctx.render('auth/login', data)
+}
+
+exports.handleLoginRedirect = async ctx => {
+	const redirectTo = await createRedirectedUrl(ctx)
+	return ctx.redirect(redirectTo)
+
+	// const query = ctx.request.query
+	//
+	// if (query.login_state || query.auth_state) {
+	// 	const { login_state, auth_state } = ctx.request.query
+	// 	const state = login_state ?? auth_state
+	//
+	// 	const values = JSON.parse(await crypto.decrypt(state))
+	//
+	// 	if (values.redirect === 'authorize') {
+	// 		return ctx.redirect(
+	// 			`/auth/authorize?auth_state=${state}`,
+	// 		)
+	// 	} else {
+	// 		return ctx.redirect('/')
+	// 	}
+	// } else {
+	// 	return ctx.redirect('/')
+	// }
 }
 
 const resetErrorMessages = {
diff --git a/src/http/middleware/RedirectToLogin.js b/src/http/middleware/RedirectToLogin.js
new file mode 100644
index 0000000000000000000000000000000000000000..52925552876724d82cceb4ec395d6885ef022c83
--- /dev/null
+++ b/src/http/middleware/RedirectToLogin.js
@@ -0,0 +1,12 @@
+const {createRedirectState} = require("../../core/utils/urls");
+
+module.exports = async (ctx, next) => {
+	const user = await ctx.services['core.auth'].getUser()
+
+	if (user) {
+		return await next()
+	}
+
+	const redirectState = await createRedirectState(ctx)
+	return ctx.redirect(`/login?login_state=${ redirectState }`)
+}
diff --git a/src/http/params/survey.js b/src/http/params/survey.js
index 0545c818110c7b4e00b6f5a3842aa187f7206b21..92ded5d8e02be21c1f5280ff39c08f028dc2ec2e 100644
--- a/src/http/params/survey.js
+++ b/src/http/params/survey.js
@@ -1,7 +1,37 @@
-const { Survey } = require('database/models')
+const { Survey, SurveyUser, Sequelize} = require('database/models')
+const moment = require("moment");
 
 module.exports = async (id, ctx, next) => {
 	ctx.models = ctx.models ?? {}
-	ctx.models.survey = await Survey.findByPk(id)
+
+	const user = await ctx.services['core.auth'].getUser()
+
+	let includes = user ? [{
+		model: SurveyUser,
+		required: false,
+		where: {
+			user_id: user.id,
+		}
+	}] : []
+
+	console.log("INCLUDES", includes)
+
+
+	ctx.models.survey = await Survey.findOne({
+		where: {
+			id,
+			expires_at: {
+				[Sequelize.Op.gt]: moment.utc().toISOString()
+			},
+			published_at: {
+				[Sequelize.Op.lt]: moment.utc().toISOString()
+			},
+			public: true,
+		},
+		include: includes
+	})
+
+	console.log(ctx.models.survey)
+
 	return await next()
 }
diff --git a/src/http/routers/routes_v2.js b/src/http/routers/routes_v2.js
index be47d9492453703a9cf3ee9eb1d108644266dce0..fc6db7be393b145036e43450caeab180fad6e7b6 100644
--- a/src/http/routers/routes_v2.js
+++ b/src/http/routers/routes_v2.js
@@ -53,6 +53,7 @@ router.put('/uploads/:upload_id/:property', noop)
 router.param('survey', param('survey'))
 
 router.get('/surveys', controller('api/v2/surveys', 'list'))
+router.get('/surveys/:survey', controller('api/v2/surveys', 'get'))
 router.post('/surveys/:survey/membership', controller('api/v2/surveys', 'join'))
 router.delete('/surveys/:survey/membership', controller('api/v2/surveys', 'leave'))
 if (config('app.dev')) {
diff --git a/src/http/routes.js b/src/http/routes.js
index f88661129bf8c783e9d09654db50b1619b94a48d..f268250e40587a65367a59ed96462775360e14dd 100644
--- a/src/http/routes.js
+++ b/src/http/routes.js
@@ -15,6 +15,7 @@ const includes = require('http/middleware/ParseIncludes')
 const profiling = require('http/middleware/Profiler')
 const loaders = require('http/middleware/MountLoaders')
 const userGate = require('http/middleware/RequiresAuth')
+const authRedirect = require('http/middleware/RedirectToLogin')
 const device = require('http/middleware/DeviceProperties').extractDevice
 
 const v2 = require('./routers/routes_v2')
@@ -47,26 +48,14 @@ web.use(device)
 web.use(well_known.allowedMethods())
 web.use(well_known.routes())
 
-web.get('/login', ctx => {
-	const data = {}
-	const state = new URLSearchParams()
-	if (ctx.query.login_state) {
-		state.set('login_state',  ctx.query.login_state)
-	}
-	if (ctx.query.auth_state) {
-		state.set('auth_state', ctx.query.auth_state)
-	}
-
-	data.state_string = `?${ state.toString() }`
-	return ctx.render('auth/login', data)
-})
-web.get('/logout', controller('auth', 'logout'))
+web.get('/login', controller('auth', 'showLogin'))
 web.post('/login', controller('auth', 'login'))
+web.get('/logout', controller('auth', 'logout'))
 
 web.get('/reset-password', controller('auth', 'resetPassword'))
 web.post('/reset-password', controller('auth', 'handleResetPassword'))
 
-web.get('/auth/authorize', AuthServer.authorize)
+web.get('/auth/authorize', authRedirect, AuthServer.authorize)
 web.post('/auth/authorize', AuthServer.authorize)
 web.post('/auth/token', AuthServer.token)
 env('FS_DRIVER', 'local') === 'local' &&
diff --git a/views/auth/accept-oauth.hbs b/views/auth/accept-oauth.hbs
index fcc291fc304ef4e10cef8677e07291980f76c300..7b1433a8d91cb71cc65b70ae4ec36144187c2563 100644
--- a/views/auth/accept-oauth.hbs
+++ b/views/auth/accept-oauth.hbs
@@ -16,27 +16,40 @@
 	</style>
 </head>
 <body>
-	<div class="max-w-lg rounded overflow-hidden bg-white shadow">
-		{{#if client.name }}<h1 class="p-2 text-xl">{{ client.name }}</h1>{{/if}}
-		{{#if client.description }}<p class="p-2">{{ client.description }}</p>{{/if}}
-		<p class="p-2 border-t border-b border-solid border-gray-400">
+<div class="bg-jetsam column center-content">
+	<div class="row space-1 content">
+		<img src="/logo.png" width="75px">
+		<h1 class="font-brand text head light">Jetsam</h1>
+	</div>
+	<div class="content focus">
+		{{#if client.name }}<h1 class="text lg">{{ client.name }} wants to access your Jetsam account</h1>{{/if}}
+		{{#if client.description }}<p class="vertical-block text md">{{ client.description }}</p>{{/if}}
+		<p class="vertical-block text md italic">
 			{{#if client.name}}{{client.name}}{{else}}An application{{/if}} is requesting access to the following
-			permissions. Please carefully read what each does before accepting or rejecting this request. Only accept this
+			permissions. Please carefully read what each does before accepting or rejecting this request. Only accept
+			this
 			request for access if you trust the application.
 		</p>
-		<ul class="p-2">
-		{{#each scopes}}
-			<li><strong>{{ this.name }}</strong> - {{ this.description }}</li>
-		{{/each}}
+		<ul class="vertical-block">
+			{{#each scopes}}
+				<li class="vertical-block"><strong class="text md bold">{{ this.name }}</strong> - {{ this.description }}</li>
+			{{/each}}
 		</ul>
-		<div class="flex justify-center">
-			<form class="flex-1 flex flex-col justify-center align-stretch" method="post" action="/auth/authorize?action=accept&auth_state={{redirect}}">
-				<button type="submit" class="bg-green-500 hover:bg-green-600 text-white shadow hover:shadow-md active:shadow">Accept</button>
+		<div class="row space-4 center-content">
+			<form method="post"
+				  action="/auth/authorize?action=accept&auth_state={{redirect}}">
+			<button type="submit"
+					class="button success">Accept
+			</button>
 			</form>
-			<form class="flex-1 flex flex-col justify-center align-stretch" method="post" action="/auth/authorize?action=deny&auth_state={{redirect}}">
-				<button type="submit" class="bg-red-500 hover:bg-red-600 text-white shadow hover:shadow-md active:shadow">Reject</button>
+			<form method="post"
+				  action="/auth/authorize?action=deny&auth_state={{redirect}}">
+			<button type="submit" class="button danger">
+				Reject
+			</button>
 			</form>
 		</div>
 	</div>
-	</body>
-</html>
\ No newline at end of file
+</div>
+</body>
+</html>