Skip to content
Snippets Groups Projects
Verified Commit f9dd6ed2 authored by Louis's avatar Louis :fire:
Browse files

Refactor auth server methods into OAuthFlow file

parent 6d9bee80
No related branches found
No related tags found
No related merge requests found
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
"watch": "NODE_PATH=src DEBUG=server:* nodemon server --ignore './client/src' --ignore './certs' --ignore 'google-storage.json'", "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'", "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: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", "test": "NODE_ENV=testing NODE_PATH=src node scripts/jest.js",
"start": "NODE_PATH=src node server", "start": "NODE_PATH=src node server",
"cmd": "NODE_PATH=src node run", "cmd": "NODE_PATH=src node run",
......
...@@ -111,15 +111,24 @@ class User extends BaseModel { ...@@ -111,15 +111,24 @@ class User extends BaseModel {
async asJWTToken(extras = {}) { async asJWTToken(extras = {}) {
const { sign } = require('core/utils/jwt') const { sign } = require('core/utils/jwt')
const roles = await this.getAuthRoles()
return await sign({ return await sign({
session: { session: {
id: this.id, id: this.id,
roles: ['overseer', 'user'], roles,
}, },
...extras, ...extras,
}) })
} }
async getAuthRoles() {
const roles = ['user']
if (this.email === 'contact@louiscap.co') {
roles.unshift('overseer')
}
return roles
}
async checkPassword(password) { async checkPassword(password) {
const crypto = require('core/utils/crypto') const crypto = require('core/utils/crypto')
if (this.password == null) { if (this.password == null) {
......
...@@ -52,7 +52,7 @@ const model = { ...@@ -52,7 +52,7 @@ const model = {
where: { email: { [Op.eq]: email } }, where: { email: { [Op.eq]: email } },
}, { }) }, { })
if (user != null) { if (user != null) {
const valid = await crypto.verify(user.password, password) const valid = await user.checkPassword(password)
if (valid) { if (valid) {
return user return user
...@@ -170,66 +170,26 @@ class KoaOAuthServer { ...@@ -170,66 +170,26 @@ class KoaOAuthServer {
// authorize to get code // authorize to get code
this.authorize = async ctx => { 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) { if (!user) {
return ctx.redirect(`/login?login_state=${ authState }`) return ctx.redirect(`/login?auth_state=${ redirect }`)
} else if (ctx.method === 'GET') { } else if (ctx.method === 'GET') {
const client = await OAuthClient.findOne({ where: { id: query.client_id }}) return await OAuthFlow.showOAuthConsent(ctx, flow)
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,
})
} else { } else {
if (ctx.request.query.action === 'deny') { if (ctx.request.query.action === 'deny') {
const { redirect_uri } = query return await OAuthFlow.handleConsentRejection(ctx, flow)
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)
} }
ctx.response.status = res.status return await OAuthFlow.handleConsentAcceptance(ctx, flow, this)
} }
} }
this.authenticate = async ctx => { this.authenticate = async ctx => {
const { req, res } = this.transformContext(ctx) const { req, res } = this.transformContext(ctx)
await authServer.authenticate(req, res) await authServer.authenticate(req, res)
...@@ -243,48 +203,12 @@ class KoaOAuthServer { ...@@ -243,48 +203,12 @@ class KoaOAuthServer {
for (const [name, value] of Object.entries(res.headers)) { for (const [name, value] of Object.entries(res.headers)) {
ctx.response.set(name, value) ctx.response.set(name, value)
} }
ctx.response.status = res.status ctx.response.status = res.status
ctx.response.body = res.body 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 = new KoaOAuthServer(new OAuthServer({ model }))
module.exports.KoaOauthServer = KoaOAuthServer module.exports.KoaOauthServer = KoaOAuthServer
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)
}
...@@ -21,10 +21,10 @@ ...@@ -21,10 +21,10 @@
{{/each}} {{/each}}
</ul> </ul>
<div class="flex justify-center"> <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> <button type="submit" class="bg-green-500 hover:bg-green-600 text-white shadow hover:shadow-md active:shadow">Accept</button>
</form> </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> <button type="submit" class="bg-red-500 hover:bg-red-600 text-white shadow hover:shadow-md active:shadow">Reject</button>
</form> </form>
</div> </div>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment