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 @@
"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",
......
......@@ -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) {
......
......@@ -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
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 @@
{{/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>
......
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