Skip to content
This repository has been archived by the owner on Dec 15, 2022. It is now read-only.

Commit

Permalink
Merge pull request #87 from atom/authentication-2
Browse files Browse the repository at this point in the history
Authentication
  • Loading branch information
Antonio Scandurra committed Oct 2, 2017
2 parents 2b61e22 + 0984b3a commit 2339084
Show file tree
Hide file tree
Showing 16 changed files with 713 additions and 77 deletions.
128 changes: 128 additions & 0 deletions lib/credential-cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
const {execFile} = require('child_process')
const keytar = require('keytar')

const SERVICE_NAME = 'atom-tachyon'

class CredentialCache {
async get (key) {
const strategy = await this.getStrategy()
return strategy.get(SERVICE_NAME, key)
}

async set (key, value) {
const strategy = await this.getStrategy()
return strategy.set(SERVICE_NAME, key, value)
}

async delete (key) {
const strategy = await this.getStrategy()
return strategy.delete(SERVICE_NAME, key)
}

async getStrategy () {
if (!this.strategy) {
if (await KeytarStrategy.isValid()) {
this.strategy = new KeytarStrategy()
} else if (SecurityBinaryStrategy.isValid()) {
this.strategy = new SecurityBinaryStrategy()
} else {
console.warn('Falling back to storing credentials in memory. Auth tokens will only be stored for the lifetime of the current window.')
this.strategy = new InMemoryStrategy()
}
}

return this.strategy
}
}

class KeytarStrategy {
static async isValid () {
try {
await keytar.setPassword('atom-test-service', 'test-key', 'test-value')
const value = await keytar.getPassword('atom-test-service', 'test-key')
keytar.deletePassword('atom-test-service', 'test-key')
return value === 'test-value'
} catch (err) {
return false
}
}

get (service, key) {
return keytar.getPassword(service, key)
}

set (service, key, value) {
return keytar.setPassword(service, key, value)
}

delete (service, key) {
return keytar.deletePassword(service, key)
}
}

class SecurityBinaryStrategy {
static isValid () {
return process.platform === 'darwin'
}

async get (service, key) {
try {
const value = await this.execSecurityBinary(['find-generic-password', '-s', service, '-a', key, '-w'])
return value.trim() || null
} catch (error) {
return null
}
}

set (service, key, value) {
return this.execSecurityBinary(['add-generic-password', '-s', service, '-a', key, '-w', value, '-U'])
}

delete (service, key) {
return this.execSecurityBinary(['delete-generic-password', '-s', service, '-a', key])
}

execSecurityBinary (args) {
return new Promise((resolve, reject) => {
execFile('security', args, (error, stdout) => {
if (error) { return reject(error) }
return resolve(stdout)
})
})
}
}

class InMemoryStrategy {
constructor () {
this.credentials = new Map()
}

get (service, key) {
const valuesByKey = this.credentials.get(service)
if (valuesByKey) {
return Promise.resolve(valuesByKey.get(key))
} else {
return Promise.resolve(null)
}
}

set (service, key, value) {
let valuesByKey = this.credentials.get(service)
if (!valuesByKey) {
valuesByKey = new Map()
this.credentials.set(service, valuesByKey)
}

valuesByKey.set(key, value)
return Promise.resolve()
}

delete (service, key) {
const valuesByKey = this.credentials.get(service)
if (valuesByKey) valuesByKey.delete(key)
return Promise.resolve()
}
}

Object.assign(CredentialCache, {KeytarStrategy, SecurityBinaryStrategy, InMemoryStrategy})
module.exports = CredentialCache
59 changes: 59 additions & 0 deletions lib/github-auth-token-provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const LoginDialog = require('./login-dialog')

module.exports =
class GithubAuthTokenProvider {
constructor ({credentialCache, commandRegistry, workspace}) {
this.credentialCache = credentialCache
this.commandRegistry = commandRegistry
this.workspace = workspace
this.tokenIsInvalid = false
}

async getToken (canPrompt) {
const previousTokenWasInvalid = this.tokenIsInvalid
this.tokenIsInvalid = false

let token
if (!previousTokenWasInvalid) {
token = await this.credentialCache.get('oauth-token')
}

if (token) {
return token
} else if (canPrompt) {
const token = await this.showLoginDialog(previousTokenWasInvalid)
return token
} else {
return null
}
}

async didInvalidateToken () {
this.tokenIsInvalid = true
await this.credentialCache.delete('oauth-token')
}

showLoginDialog (previousTokenWasInvalid) {
return new Promise((resolve) => {
const loginDialog = new LoginDialog({
commandRegistry: this.commandRegistry,
tokenIsInvalid: previousTokenWasInvalid,
didConfirm: async (token) => {
resolve(token)

modalPanel.destroy()
loginDialog.dispose()
await this.credentialCache.set('oauth-token', token)
},
didCancel: () => {
resolve(null)

modalPanel.destroy()
loginDialog.dispose()
}
})
const modalPanel = this.workspace.addModalPanel({item: loginDialog, className: 'realtime-LoginPanel'})
loginDialog.focus()
})
}
}
14 changes: 14 additions & 0 deletions lib/guest-portal-binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class GuestPortalBinding {
async initialize () {
try {
this.portal = await this.client.joinPortal(this.portalId)
if (!this.portal) return false

this.statusBarTile = this.addStatusBarIndicatorForPortal(this.portal, {isHost: false})
this.workspace.observeActivePaneItem(this.didChangeActivePaneItem.bind(this))
this.portal.setDelegate(this)
Expand All @@ -46,6 +48,18 @@ class GuestPortalBinding {
this.emitDidDispose()
}

siteDidJoin (siteId) {
const {login: hostLogin} = this.portal.getSiteIdentity(1)
const {login: siteLogin} = this.portal.getSiteIdentity(siteId)
this.notificationManager.addInfo(`@${siteLogin} has joined @${hostLogin}'s portal`)
}

siteDidLeave (siteId) {
const {login: hostLogin} = this.portal.getSiteIdentity(1)
const {login: siteLogin} = this.portal.getSiteIdentity(siteId)
this.notificationManager.addInfo(`@${siteLogin} has left @${hostLogin}'s portal`)
}

didChangeActivePaneItem (paneItem) {
if (this.statusBarTile) {
const item = this.statusBarTile.getItem()
Expand Down
12 changes: 12 additions & 0 deletions lib/host-portal-binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class HostPortalBinding {
async initialize () {
try {
this.portal = await this.client.createPortal()
if (!this.portal) return false

this.portal.setDelegate(this)
this.disposables.add(this.workspace.observeActiveTextEditor(
this.didChangeActiveTextEditor.bind(this)
Expand Down Expand Up @@ -58,6 +60,16 @@ class HostPortalBinding {
this.portal.dispose()
}

siteDidJoin (siteId) {
const {login} = this.portal.getSiteIdentity(siteId)
this.notificationManager.addInfo(`@${login} has joined your portal`)
}

siteDidLeave (siteId) {
const {login} = this.portal.getSiteIdentity(siteId)
this.notificationManager.addInfo(`@${login} has left your portal`)
}

async didChangeActiveTextEditor (editor) {
if (editor == null) {
await this.portal.setActiveEditorProxy(null)
Expand Down
86 changes: 86 additions & 0 deletions lib/login-dialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const {CompositeDisposable, Disposable, TextEditor} = require('atom')
const etch = require('etch')
const $ = etch.dom

module.exports =
class LoginDialog {
constructor (props) {
this.props = props
etch.initialize(this)
this.refs.editor.element.addEventListener('blur', this.didBlur.bind(this))
this.disposables = new CompositeDisposable()
this.disposables.add(this.props.commandRegistry.add(this.element, {
'core:confirm': this.confirm.bind(this),
'core:cancel': this.cancel.bind(this)
}))
}

dispose () {
this.disposables.dispose()
}

focus () {
this.refs.editor.element.focus()
}

didBlur ({relatedTarget}) {
if (this.element !== relatedTarget && !this.element.contains(relatedTarget)) {
this.cancel()
}
}

render () {
const errorMessage = this.props.tokenIsInvalid
? $.p({className: 'error-messages'}, 'That token does not appear to be valid.')
: null

return $.div({className: 'realtime-LoginDialog', tabIndex: -1, on: {blur: this.didBlur}},
$.h1(null, 'Log in with ', $.span({className: 'realtime-LoginDialog-GitHubLogo'})),
$.p(null,
'Visit ',
$.a({href: 'https://tachyon.atom.io/login', className: 'text-info'}, 'tachyon.atom.io/login'),
' to generate an authentication token and paste it below:'
),
errorMessage,

$(TextEditor, {ref: 'editor', mini: true, placeholderText: 'Enter your token...'}),
$.div(null,
$.button(
{
type: 'button',
className: 'btn inline-block-tight',
onClick: this.cancel
},
'Cancel'
),
$.button(
{
ref: 'loginButton',
type: 'button',
className: 'btn btn-primary inline-block-tight',
onClick: this.confirm
},
'Login'
)
)
)
}

update (props) {
Object.assign(this.props, props)
etch.update(this)
}

confirm () {
const token = this.refs.editor.getText()
if (token.length === 40) {
this.props.didConfirm(token)
} else {
this.update({tokenIsInvalid: true})
}
}

cancel () {
this.props.didCancel()
}
}
20 changes: 16 additions & 4 deletions lib/real-time-package.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const path = require('path')
const {shell} = require('electron')
const {CompositeDisposable} = require('atom')
const {RealTimeClient, Errors} = require('@atom/real-time-client')
const BufferBinding = require('./buffer-binding')
Expand All @@ -8,13 +9,16 @@ const GuestPortalBindingRegistry = require('./guest-portal-binding-registry')
const GuestPortalBinding = require('./guest-portal-binding')
const JoinPortalDialog = require('./join-portal-dialog')
const PortalStatusBarIndicator = require('./portal-status-bar-indicator')
const GithubAuthTokenProvider = require('./github-auth-token-provider')
const CredentialCache = require('./credential-cache')
const LoginDialog = require('./login-dialog')

module.exports =
class RealTimePackage {
constructor (options) {
const {
workspace, notificationManager, commandRegistry, tooltipManager, clipboard,
restGateway, pubSubGateway, pusherKey, baseURL
authTokenProvider, restGateway, pubSubGateway, pusherKey, baseURL
} = options

this.workspace = workspace
Expand All @@ -26,11 +30,18 @@ class RealTimePackage {
this.pubSubGateway = pubSubGateway
this.pusherKey = pusherKey
this.baseURL = baseURL
this.authTokenProvider = authTokenProvider || new GithubAuthTokenProvider({
workspace,
commandRegistry,
credentialCache: new CredentialCache(),
openURL: shell.openExternal.bind(shell)
})
this.client = new RealTimeClient({
pusherKey: this.pusherKey,
baseURL: this.baseURL,
restGateway: this.restGateway,
pubSubGateway: this.pubSubGateway
pubSubGateway: this.pubSubGateway,
authTokenProvider: this.authTokenProvider
})
this.client.onConnectionError(this.handleConnectionError.bind(this))
this.hostPortalBinding = null
Expand Down Expand Up @@ -76,8 +87,9 @@ class RealTimePackage {
notificationManager: this.notificationManager,
addStatusBarIndicatorForPortal: this.addStatusBarIndicatorForPortal.bind(this)
})
await this.hostPortalBinding.initialize()
return this.hostPortalBinding.portal
if (await this.hostPortalBinding.initialize()) {
return this.hostPortalBinding.portal
}
}
}

Expand Down

0 comments on commit 2339084

Please sign in to comment.