Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Auth Command Set

161 changes: 161 additions & 0 deletions auth/authCheck.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
const nimbella = require('@nimbella/sdk')
const kv = nimbella.redis()
const baseUrl = 'https://github.com'
const authorizePath = '/login/oauth/authorize'
const scope = 'user:email,read:org'
const allowSignup = 'false'
// const authorizationUrl = `${baseUrl}${authorizePath}?client_id=${process.env.OAUTH_CLIENT_ID}&scope=${scope}&allow_signup=${allowSignup}`
const sessionExpiration = 1 * 60 // ttl in seconds
const https = require('https')
const { URL } = require('url')


// Generates error response. This is not intended for slack and usually indicates
// an invalid request.
function generateErrorObject(message, statusCode) {
return {
statusCode: 400 || statusCode,
body: {
error: message
}
}
}

// When the request contains an access token, it is the result of an asynchronous
// call back. This will indicate that the handler should rehydrate the state then
// call the command handler. When the handler completes, we must post the result
// back using the saved webhook.
//
// This function will attempt to save the access token for a predetermined amount
// of time so that it may be reused without forcing the same user to re-authenticate.
async function rehydrate(access_token, state) {
return kv
.getAsync(state)
.then(JSON.parse)
.then(async state => {
if (state && state.savedArgs) {
try {
// remember the access token for a some duration
const userid = state.savedArgs.params.__client.user_id
await kv.setexAsync(userid, sessionExpiration, JSON.stringify({ access_token }))
} catch (e) {
console.error('could not save access token', e)
}

return state.savedArgs
} else {
return Promise.reject('Your session has expired')
}
})
}

// For the user identified in the request, check if there is a previously saved
// access token. If so, return it. Otherwise return undefined and allow handler
// to initiate a new oauth flow.
async function previouslyAuthorized(event) {
try {
const userid = event.params.__client.user_id
return kv
.getAsync(userid)
.then(JSON.parse)
.then(_ => {
if (_ && _.access_token) {
return _.access_token
} else return undefined
})
} catch (e) {
console.error('Invalid request (missing expected properties)')
return undefined
}
}

async function postResult(webhook, result) {
const url = new URL(webhook)
return new Promise(function (resolve, reject) {
const data = JSON.stringify(result)
https.request({
method: 'POST',
host: url.host,
path: url.pathname,
headers: {
'Content-Type': 'application/json',
'Content-Length': data.length
}
}, (resp) => {
let buffer = ''

resp.on('data', (chunk) => {
buffer += chunk
})

resp.on('end', () => {
try {
resolve({ body: buffer })
} catch (e) {
console.error(e, buffer)
reject(generateErrorObject('Unexpected response'))
}
})
}).on('error', reject)
.write(data)
.end()
})
}

function validEvent(event) {
return event.params
&& event.params.__client
&& event.params.__client.user_id
&& event.params.__client.response_url
&& event.params.__client.name === 'slack'
}

const authCheck = async (event, _command) => {
try {
// check if this is an asynchronous call back from an oauth flow
// in which case the state and access_tokens are available in the event
if (event.state && event.access_token) {
// rehydrate, then execute the command with the access token
const savedArgs = await rehydrate(event.access_token, event.state)
const result = await _command(event.access_token, savedArgs.params, savedArgs.commandText, savedArgs.__secrets || {})
const webhook = savedArgs.params.__client.response_url
return postResult(webhook, result)
} else if (validEvent(event)) {
// this is a synchronous inbound event from slack, check if there is an
// access token available for the slack user who initiated this request
const access_token = await previouslyAuthorized(event)
if (access_token) {
// previously authorized, execute the command and return the result
return _command(access_token, event.params, event.commandText, event.__secrets || {})
} else {
// kick off authorization flow
const state = process.env.__OW_ACTIVATION_ID
return kv
.setexAsync(state, sessionExpiration, JSON.stringify({savedArgs: event, callback: `${process.env.__OW_ACTION_NAME}`}))
.then(result => {
if (result === 'OK') {
return {
response_type: 'ephemeral',
text: `You need to authenticate to perform this operation. Please click this <${authorizationUrl}&state=${state}|link> to continue.`
}
} else {
console.log('kv set failed with result:', result)
return {
response_type: 'ephemeral',
text: `This operation requires you to authenticate but could not create a session for you.\n` +
`If you are authorized to inspect the activation logs, check them for details or contact your Commander admin.`
}
}
})
}
} else {
console.error('Invalid request', event)
return generateErrorObject('Invalid request')
}
} catch (error) {
console.error(error)
return generateErrorObject('Unexpected error')
}
}

module.exports = authCheck
34 changes: 34 additions & 0 deletions auth/commands.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
commands:
auth:
description: Add, Remove and List Identity Providers, Specify which provider to use for how long.
parameters:
- name: action
- name: entity
optional: true
options:
- name: n
value: provider_name
- name: a
value: auth_url
- name: b
value: base_url
- name: c
value: callback_url
- name: d
value: duration
- name: g
value: grant_type
- name: i
value: client_id
- name: p
value: scope
- name: s
value: client_secret
- name: t
value: access_token_url
limits:
logs: 10
callback:
description: callback for oauth flow.
annotations:
provide-api-key: true
24 changes: 24 additions & 0 deletions auth/encrypt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const crypto = require('crypto');
const algorithm = 'aes-256-cbc';
const iv = crypto.randomBytes(16);

function encrypt(text, key) {
let cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return { iv: iv.toString('hex'), encryptedData: encrypted.toString('hex') };
}

function decrypt(text, key) {
let iv = Buffer.from(text.iv, 'hex');
let encryptedText = Buffer.from(text.encryptedData, 'hex');
let decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), iv);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
}

module.exports ={
encrypt,
decrypt
}
3 changes: 3 additions & 0 deletions auth/packages/auth/auth/.include
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
../../../encrypt.js
../../../authCheck.js
index.js
Loading