diff --git a/src/database/models/AccessToken.js b/src/database/models/AccessToken.js
index ec8c8c2ac6639e9aeab922b129fa058ecbe82757..16d060d2e89dc972683d1ff3cca6c20a551a7ed6 100644
--- a/src/database/models/AccessToken.js
+++ b/src/database/models/AccessToken.js
@@ -19,6 +19,19 @@ class AccessToken extends BaseModel {
 		}
 	}
 
+	async toOAuthInterface() {
+		const client = await this.getOAuthClient()
+		const user = await this.getUser()
+
+		return {
+			accessToken: this.token,
+			accessTokenExpiresAt: this.expires_at,
+			scope: this.scope,
+			client:  client.toOAuthInterface(),
+			user,
+		}
+	}
+
 	toJSON() {
 		const user = this.user ? {user: this.user} : {}
 		return {
diff --git a/src/database/models/RefreshToken.js b/src/database/models/RefreshToken.js
index eb537328cebe72cf34bcb930cc1aff017ed25c77..214edc4471682109bfbc73f4d93531b9fc1429fa 100644
--- a/src/database/models/RefreshToken.js
+++ b/src/database/models/RefreshToken.js
@@ -19,6 +19,19 @@ class RefreshToken extends BaseModel {
 		}
 	}
 
+	async toOAuthInterface() {
+		const client = await this.getOAuthClient()
+		const user = await this.getUser()
+
+		return {
+			refreshToken: this.token,
+			refreshTokenExpiresAt: this.expires_at,
+			scope: this.scope,
+			client:  client.toOAuthInterface(),
+			user,
+		}
+	}
+
 	toJSON() {
 		const user = this.user ? { user: this.user } : { }
 		return {
diff --git a/src/database/models/User.js b/src/database/models/User.js
index 26231af8c74b74683aa0cfe55f481634065b421a..c6ec41b92775b1ea9c635f10cf2749afdeef9156 100644
--- a/src/database/models/User.js
+++ b/src/database/models/User.js
@@ -109,13 +109,14 @@ class User extends BaseModel {
 		return await crypto.encrypt(JSON.stringify({ session: this.id }))
 	}
 
-	async asJWTToken() {
+	async asJWTToken(extras = {}) {
 		const { sign } = require('core/utils/jwt')
 		return await sign({
 			session: {
 				id: this.id,
 				roles: ['overseer', 'user'],
 			},
+			...extras,
 		})
 	}
 
diff --git a/src/domain/auth/AuthServer.js b/src/domain/auth/AuthServer.js
index 07ae5e3edecfad80b6fe5de81f5db6c65a343989..d0b21a7ddac9a6d8c6fdc376d05682ff9edd5b5a 100644
--- a/src/domain/auth/AuthServer.js
+++ b/src/domain/auth/AuthServer.js
@@ -15,18 +15,10 @@ function createTokenPair(user, client, access, refresh = {}) {
 	}
 }
 
-async function createClientEncryptedToken(client, user, scope) {
-	const payload = {
-		client_id: client.id,
-		user_id: user.id,
-		scope: scope,
-	}
-
-	if (client.secret == null) {
-		return crypto.encrypt(payload)
-	}
-
-	return await crypto.encryptWith(Buffer.from(client.secret, 'hex'), JSON.stringify(payload))
+async function createClientEncryptedToken(client, userModel, scope) {
+	const user = await User.findByPk(userModel.id)
+	const payload = user.asJWTToken({ cid: client.id, scope })
+	return payload
 }
 
 const model = {
@@ -50,8 +42,9 @@ const model = {
 	getAccessToken(token) {
 		return AccessToken.findOne({ include: [ { model: User }, { model: OAuthClient } ], where: { token: { [Op.eq]: token } } })
 	},
-	getRefreshToken(token) {
-		return RefreshToken.findOne({ include: [ { model: User }, { model: OAuthClient } ], where: { token: { [Op.eq]: token } } })
+	async getRefreshToken(token) {
+		const t = await RefreshToken.findOne({ include: [ { model: User }, { model: OAuthClient } ], where: { token: { [Op.eq]: token } } })
+		return await t?.toOAuthInterface()
 	},
 
 	getUser: async function getAuthUser(email, password) {
@@ -188,6 +181,8 @@ class KoaOAuthServer {
 				}
 			}
 
+			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 }`)
@@ -259,7 +254,7 @@ const scopeDescriptionMap = {
 		description: 'Full access to your account, including the ability to create, update and delete any user information, metrics and files.'
 	}
 }
-function describeScopeRequest(scope) {
+function describeScopeRequest(scope = '*') {
 	const scopes = scope.split(' ')
 	return scopes.map(s => scopeDescriptionMap[s])
 }
diff --git a/src/http/controllers/api/oauth.js b/src/http/controllers/api/oauth.js
index 6091b5aa72b7cb4d11daf2b2228dae4bf72f6084..a82997e69c096dae35aea9a1de61fe1524156c1f 100644
--- a/src/http/controllers/api/oauth.js
+++ b/src/http/controllers/api/oauth.js
@@ -24,4 +24,55 @@ exports.listClients = async ctx => {
 	ctx.body = {
 		clients: clients.map(c => c.toOAuthInterface())
 	}
+}
+
+exports.addClientRedirect = async ctx => {
+	const client = ctx.models?.oauthClient
+	const user = await ctx.services['core.auth'].getUser()
+
+	if (client?.owner_id !== user.id) {
+		throw new HttpError(403, 'You do not have permission to modify this oauth client')
+	}
+
+	const uri = ctx.request.body?.uri
+
+	if (!uri?.trim()) {
+		throw new HttpError(400, 'Must provide a URI to add to the client')
+	}
+
+	const uris = new Set(client.redirect_uris)
+	if (!uris.has(uri)) {
+		client.redirect_uris = [
+			...client.redirect_uris,
+			uri,
+		]
+
+		await client.save()
+	}
+
+	ctx.body = { client: client.toOAuthInterface() }
+}
+
+exports.removeClientRedirect = async ctx => {
+	const client = ctx.models?.oauthClient
+	const user = await ctx.services['core.auth'].getUser()
+
+	if (client.owner_id !== user.id) {
+		throw new HttpError(403, 'You do not have permission to modify this oauth client')
+	}
+
+	const uri = ctx.request.body?.uri
+
+	if (!uri?.trim()) {
+		throw new HttpError(400, 'Must provide a URI to add to the client')
+	}
+
+	const uris = new Set(client.redirect_uris)
+	if (uris.has(uri)) {
+		uris.delete(uri)
+		client.redirect_uris = Array.from(uris)
+
+		await client.save()
+	}
+	ctx.body = { client: client.toOAuthInterface() }
 }
\ No newline at end of file
diff --git a/src/http/middleware/Profiler.js b/src/http/middleware/Profiler.js
index 6c76f074fe75e4a55eac5bc3810ae39502abf885..d9b1e37c028b5b68789666d492ecbc86425ba6fd 100644
--- a/src/http/middleware/Profiler.js
+++ b/src/http/middleware/Profiler.js
@@ -43,6 +43,18 @@ module.exports = async (ctx, next) => {
 	} finally {
 		t.setName(`[${ ctx.method }] ${ ctx._matchedRouteName ?? ctx._matchedRoute ?? ctx.path }`)
 		t.setHttpStatus(ctx.status)
-		threadContext.stopTransaction()
+		const user = ctx.services['core.auth']._user
+		Sentry.configureScope(scope => {
+			if (user) {
+				scope.setUser({
+					name: user.name,
+					email: user.email,
+					id: user.id,
+					ip_address: ctx.ip,
+				})
+			}
+
+			threadContext.stopTransaction()
+		})
 	}
 }
\ No newline at end of file
diff --git a/src/http/params/oauth_client.js b/src/http/params/oauth_client.js
new file mode 100644
index 0000000000000000000000000000000000000000..ed386d809639ac9ce6a3df5d3db0e32029615ab3
--- /dev/null
+++ b/src/http/params/oauth_client.js
@@ -0,0 +1,7 @@
+const { OAuthClient } = require('database/models')
+
+module.exports = async (id, ctx, next) => {
+	ctx.models = ctx.models ?? {}
+	ctx.models.oauthClient = await OAuthClient.findByPk(id)
+	return await next()
+}
\ No newline at end of file
diff --git a/src/http/routes.js b/src/http/routes.js
index ae0e221c2a05c8f405c8eacc6590ade07802b7f4..c57794a95edd4b2c8a6b84ee235954de96f5d076 100644
--- a/src/http/routes.js
+++ b/src/http/routes.js
@@ -1,4 +1,6 @@
 const controller = (name, method) => require(`./controllers/${ name }`)[method]
+const param = (name) => require(`./params/${ name }`)
+
 const AuthServer = require('domain/auth/AuthServer')
 const { env, config } = require('bootstrap')
 
@@ -103,8 +105,12 @@ function mount(api) {
 	api.post('/auth/reset-token', controller('api/auth', 'triggerPasswordReset'))
 	api.post('/auth/reset-password', controller('api/auth', 'handlePasswordReset'))
 
+	api.param('oauthClientId', param('oauth_client'))
+
 	api.get('/oauth/clients', controller('api/oauth', 'listClients'))
 	api.post('/oauth/clients', controller('api/oauth', 'createClient'))
+	api.post('/oauth/clients/:oauthClientId/redirects', controller('api/oauth', 'addClientRedirect'))
+	api.delete('/oauth/clients/:oauthClientId/redirects', controller('api/oauth', 'removeClientRedirect'))
 
 	api.get('/self', controller('api/user', 'self'))
 	api.get('/self/bundles', controller('api/app', 'getBundles'))