diff --git a/package.json b/package.json
index 0b4914873f059239e076489802d4ba95c4fda230..ea44ce1bbefa4bd76e3fdc2b950fe966c8220d2a 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
     "watch": "NODE_PATH=src DEBUG=server:* nodemon server --ignore './client/src' --ignore './certs' --ignore 'google-storage.json'",
     "watch:queue": "NODE_PATH=src QUEUE_ACTION=consumer DEBUG=server:* nodemon worker --ignore './client/src' --ignore './certs' --ignore 'google-storage.json'",
     "exec:env": "docker-compose -p jetenv up",
+	"exec:ngrok": "ngrok http 7123 --hostname trash.4l2.uk",
     "test": "NODE_ENV=testing NODE_PATH=src node scripts/jest.js",
     "start": "NODE_PATH=src node server",
     "cmd": "NODE_PATH=src node run",
diff --git a/src/database/models/User.js b/src/database/models/User.js
index c6ec41b92775b1ea9c635f10cf2749afdeef9156..ea4037e5a23b380a32d6e93d23371bcc6481ce05 100644
--- a/src/database/models/User.js
+++ b/src/database/models/User.js
@@ -111,15 +111,24 @@ class User extends BaseModel {
 
 	async asJWTToken(extras = {}) {
 		const { sign } = require('core/utils/jwt')
+		const roles = await this.getAuthRoles()
 		return await sign({
 			session: {
 				id: this.id,
-				roles: ['overseer', 'user'],
+				roles,
 			},
 			...extras,
 		})
 	}
 
+	async getAuthRoles() {
+		const roles = ['user']
+		if (this.email === 'contact@louiscap.co') {
+			roles.unshift('overseer')
+		}
+		return roles
+	}
+
 	async checkPassword(password) {
 		const crypto = require('core/utils/crypto')
 		if (this.password == null) {
diff --git a/src/domain/auth/AuthServer.js b/src/domain/auth/AuthServer.js
index b237b6d248186e63e37bd660a53701773f11600f..39573c83a0de51d69775d2c2c82222e3e1df4086 100644
--- a/src/domain/auth/AuthServer.js
+++ b/src/domain/auth/AuthServer.js
@@ -52,7 +52,7 @@ const model = {
 			where: { email: { [Op.eq]: email } },
 		}, { })
 		if (user != null) {
-			const valid = await crypto.verify(user.password, password)
+			const valid = await user.checkPassword(password)
 
 			if (valid) {
 				return user
@@ -170,66 +170,26 @@ class KoaOAuthServer {
 		// authorize to get code
 
 		this.authorize = async ctx => {
-			const user = await ctx.services['core.auth'].getUser()
+			const OAuthFlow = require('./OAuthFlow')
+			const flow = await OAuthFlow.initialiseFlow(ctx)
+			const {
+				user,
+				redirect,
+			} = flow
 
-			let query = ctx.request.query
-
-			if (ctx.request.query.auth_state) {
-				query = JSON.parse(await crypto.decrypt(ctx.request.query.auth_state))
-				if (ctx.request.query.action && query.query) {
-					query = query.query
-				}
-			}
-
-			console.log(ctx.request.query, query)
-
-			const authState = await crypto.encrypt(JSON.stringify({ redirect: 'authorize', query: ctx.request.query }))
 			if (!user) {
-				return ctx.redirect(`/login?login_state=${ authState }`)
+				return ctx.redirect(`/login?auth_state=${ redirect }`)
 			} else if (ctx.method === 'GET') {
-				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(scopes)
-
-				return ctx.render('auth/accept-oauth', {
-					user,
-					client,
-					scopes,
-					authState,
-				})
+				return await OAuthFlow.showOAuthConsent(ctx, flow)
 			} else {
 				if (ctx.request.query.action === 'deny') {
-					const { redirect_uri } = query
-					const redirect = new URL(redirect_uri, 'http://localhost')
-					const search = new URLSearchParams(redirect.searchParams)
-
-					search.set('error', 'access_denied')
-					search.set('error_description', 'The user has denied the requested permissions')
-					redirect.search = search.toString()
-
-					ctx.set('Location', redirect.toString())
-					ctx.status = 302
-					return
-				}
-				if (!query.state) {
-					query.state = crypto.insecureHexString(32)
-				}
-				const { req, res } = this.transformContext(ctx, { query })
-				await authServer.authorize(req, res, { authenticateHandler: { handle() { return user } }})
-				for (const [name, value] of Object.entries(res.headers)) {
-					ctx.response.set(name, value)
+					return await OAuthFlow.handleConsentRejection(ctx, flow)
 				}
-				ctx.response.status = res.status
+				return await OAuthFlow.handleConsentAcceptance(ctx, flow, this)
 			}
 		}
 
 		this.authenticate = async ctx => {
-
 			const { req, res } = this.transformContext(ctx)
 			await authServer.authenticate(req, res)
 
@@ -243,48 +203,12 @@ class KoaOAuthServer {
 			for (const [name, value] of Object.entries(res.headers)) {
 				ctx.response.set(name, value)
 			}
+
 			ctx.response.status = res.status
 			ctx.response.body = res.body
 		}
 	}
 }
 
-const scopeDescriptionMap = {
-	'*': {
-		icon: 'admin',
-		name: 'Full Access',
-		description: 'Full access to your account, including the ability to create, update and delete any user information, metrics and files.'
-	},
-	'metrics:create': {
-		name: 'Create Metrics',
-		description: 'The ability to add data metrics linked to your account. Remember that connected apps that create metrics will know your location!',
-	},
-	'files:upload': {
-		name: 'Upload Files',
-		description: 'The ability to upload images linked to your account',
-	},
-	'files:read': {
-		name: 'Read Files',
-		description: 'The ability to see and download images that you\'ve uploaded to your account',
-	},
-	'profile:read': {
-		name: 'Read Profile',
-		description: 'The ability to see any information associated with your user profile. This includes your name and email address',
-	},
-	'profile:write': {
-		name: 'Modify Profile',
-		description: 'The ability to edit any information associated with your user profile. This includes your name',
-	},
-	'profile:stats': {
-		name: 'Profile Stats',
-		description: 'The ability to see information about your account stats, including your points and citizen scientist level',
-	},
-
-}
-function describeScopeRequest(scope = '*') {
-	const scopes = scope.split(' ')
-	return scopes.map(s => scopeDescriptionMap[s])
-}
-
 module.exports = new KoaOAuthServer(new OAuthServer({ model }))
 module.exports.KoaOauthServer = KoaOAuthServer
diff --git a/src/domain/auth/OAuthFlow.js b/src/domain/auth/OAuthFlow.js
new file mode 100644
index 0000000000000000000000000000000000000000..b2d369efa8dbc145a2dc1660994699e0a4130e75
--- /dev/null
+++ b/src/domain/auth/OAuthFlow.js
@@ -0,0 +1,124 @@
+const { OAuthClient } = require('database/models')
+const HttpError = require('core/errors/HttpError')
+const crypto = require('core/utils/crypto')
+
+/**
+ * Retrieve the correct query value and format auth state for an oauth request
+ * @param ctx
+ * @return {Promise<{redirect: *, query: ({auth_state}|*), action, state: null, user: *}>}
+ */
+exports.initialiseFlow = async ctx => {
+	const user = await ctx.services['core.auth'].getUser()
+
+	let baseQuery = ctx.request.query
+	let queryState = null
+
+	console.log(baseQuery)
+
+	if (baseQuery.auth_state) {
+		queryState = JSON.parse(await crypto.decrypt(baseQuery.auth_state))
+		if (queryState.query) {
+			queryState = queryState.query
+		}
+	}
+
+	const action = baseQuery.action
+
+	const redirectState = await crypto.encrypt(JSON.stringify({ redirect: 'authorize', query: baseQuery }))
+
+	return {
+		user,
+		action,
+		query: baseQuery,
+		state: queryState,
+		redirect: redirectState,
+	}
+}
+
+exports.showOAuthConsent = async (ctx, queryState) => {
+	const {
+		user,
+		query,
+		redirect,
+	} = queryState
+
+	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)
+
+	return ctx.render('auth/accept-oauth', {
+		user,
+		client,
+		scopes,
+		redirect,
+	})
+}
+
+exports.handleConsentRejection = async (ctx, flow) => {
+	const { redirect_uri } = flow.query
+	const redirect = new URL(redirect_uri, 'http://localhost')
+	const search = new URLSearchParams(redirect.searchParams)
+
+	search.set('error', 'access_denied')
+	search.set('error_description', 'The user has denied the requested permissions')
+	redirect.search = search.toString()
+
+	ctx.set('Location', redirect.toString())
+	ctx.status = 302
+	ctx.body = null
+}
+
+exports.handleConsentAcceptance = async (ctx, flow, server) => {
+	const { state: queryState } = flow
+	if (!queryState.state) {
+		queryState.state = crypto.insecureHexString(32)
+	}
+
+	const { req, res } = server.transformContext(ctx, { query: queryState })
+	await server.getAuthServer().authorize(req, res, { authenticateHandler: { handle() { return flow.user } }})
+	for (const [name, value] of Object.entries(res.headers)) {
+		ctx.response.set(name, value)
+	}
+	ctx.response.status = res.status
+}
+
+const scopeDescriptionMap = {
+	'*': {
+		icon: 'admin',
+		name: 'Full Access',
+		description: 'Full access to your account, including the ability to create, update and delete any user information, metrics and files.'
+	},
+	'metrics:create': {
+		name: 'Create Metrics',
+		description: 'The ability to add data metrics linked to your account. Remember that connected apps that create metrics will know your location!',
+	},
+	'files:upload': {
+		name: 'Upload Files',
+		description: 'The ability to upload images linked to your account',
+	},
+	'files:read': {
+		name: 'Read Files',
+		description: 'The ability to see and download images that you\'ve uploaded to your account',
+	},
+	'profile:read': {
+		name: 'Read Profile',
+		description: 'The ability to see any information associated with your user profile. This includes your name and email address',
+	},
+	'profile:write': {
+		name: 'Modify Profile',
+		description: 'The ability to edit any information associated with your user profile. This includes your name',
+	},
+	'profile:stats': {
+		name: 'Profile Stats',
+		description: 'The ability to see information about your account stats, including your points and citizen scientist level',
+	},
+
+}
+function describeScopeRequest(scope = '*') {
+	const scopes = scope.split(' ')
+	return scopes.map(s => scopeDescriptionMap[s])
+		.filter(Boolean)
+}
diff --git a/views/auth/accept-oauth.hbs b/views/auth/accept-oauth.hbs
index 85bc1d6ed9830e2af5ab855cb8f83e325ab62084..5b73a73422048a9ef8a18dfd3032cd876192a5ee 100644
--- a/views/auth/accept-oauth.hbs
+++ b/views/auth/accept-oauth.hbs
@@ -21,10 +21,10 @@
 		{{/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={{authState}}">
+			<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>
 			</form>
-			<form class="flex-1 flex flex-col justify-center align-stretch" method="post" action="/auth/authorize?action=deny&auth_state={{authState}}">
+			<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>
 		</div>