This repository has been archived by the owner on Dec 15, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 320
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #87 from atom/authentication-2
Authentication
- Loading branch information
Showing
16 changed files
with
713 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.