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

Commit b7eab9c

Browse files
authored
Merge pull request #1871 from atom/aw/insufficient-token-loop
Cache token scope check failures
2 parents c2da601 + e8f41f1 commit b7eab9c

File tree

3 files changed

+65
-11
lines changed

3 files changed

+65
-11
lines changed

lib/models/github-login-model.js

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import crypto from 'crypto';
22
import {Emitter} from 'event-kit';
33

4-
import {UNAUTHENTICATED, INSUFFICIENT, createStrategy} from '../shared/keytar-strategy';
4+
import {UNAUTHENTICATED, INSUFFICIENT, UNAUTHORIZED, createStrategy} from '../shared/keytar-strategy';
55

66
let instance = null;
77

@@ -21,7 +21,7 @@ export default class GithubLoginModel {
2121
this._Strategy = Strategy;
2222
this._strategy = null;
2323
this.emitter = new Emitter();
24-
this.checked = new Set();
24+
this.checked = new Map();
2525
}
2626

2727
async getStrategy() {
@@ -53,21 +53,33 @@ export default class GithubLoginModel {
5353
hash.update(password);
5454
const fingerprint = hash.digest('base64');
5555

56-
if (!this.checked.has(fingerprint)) {
56+
const outcome = this.checked.get(fingerprint);
57+
if (outcome === UNAUTHENTICATED || outcome === INSUFFICIENT) {
58+
// Cached failure
59+
return outcome;
60+
} else if (!outcome) {
61+
// No cached outcome. Query for scopes.
5762
try {
58-
const scopes = new Set(await this.getScopes(account, password));
63+
const scopes = await this.getScopes(account, password);
64+
if (scopes === UNAUTHORIZED) {
65+
// Password is incorrect. Treat it as though you aren't authenticated at all.
66+
this.checked.set(fingerprint, UNAUTHENTICATED);
67+
return UNAUTHENTICATED;
68+
}
69+
const scopeSet = new Set(scopes);
5970

6071
for (const scope of this.constructor.REQUIRED_SCOPES) {
61-
if (!scopes.has(scope)) {
72+
if (!scopeSet.has(scope)) {
6273
// Token doesn't have enough OAuth scopes, need to reauthenticate
74+
this.checked.set(fingerprint, INSUFFICIENT);
6375
return INSUFFICIENT;
6476
}
6577
}
6678

67-
// We're good
68-
this.checked.add(fingerprint);
79+
// Successfully authenticated and had all required scopes.
80+
this.checked.set(fingerprint, true);
6981
} catch (e) {
70-
// Bad credential most likely
82+
// Most likely a network error. Do not cache the failure.
7183
// eslint-disable-next-line no-console
7284
console.error(`Unable to validate token scopes against ${account}`, e);
7385
return UNAUTHENTICATED;
@@ -90,6 +102,7 @@ export default class GithubLoginModel {
90102
this.didUpdate();
91103
}
92104

105+
/* istanbul ignore next */
93106
async getScopes(host, token) {
94107
if (atom.inSpecMode()) {
95108
if (token === 'good-token') {
@@ -104,6 +117,10 @@ export default class GithubLoginModel {
104117
headers: {Authorization: `bearer ${token}`},
105118
});
106119

120+
if (response.status === 401) {
121+
return UNAUTHORIZED;
122+
}
123+
107124
if (response.status !== 200) {
108125
throw new Error(`Unable to check token for OAuth scopes against ${host}: ${await response.text()}`);
109126
}

lib/shared/keytar-strategy.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@ if (typeof atom === 'undefined') {
1616
};
1717
}
1818

19+
// No token available in your OS keychain.
1920
const UNAUTHENTICATED = Symbol('UNAUTHENTICATED');
2021

22+
// The token in your keychain isn't granted all of the required OAuth scopes.
2123
const INSUFFICIENT = Symbol('INSUFFICIENT');
2224

25+
// The token in your keychain is not accepted by GitHub.
26+
const UNAUTHORIZED = Symbol('UNAUTHORIZED');
27+
2328
class KeytarStrategy {
2429
static get keytar() {
2530
return require('keytar');
@@ -248,6 +253,7 @@ async function createStrategy() {
248253
module.exports = {
249254
UNAUTHENTICATED,
250255
INSUFFICIENT,
256+
UNAUTHORIZED,
251257
KeytarStrategy,
252258
SecurityBinaryStrategy,
253259
InMemoryStrategy,

test/models/github-login-model.test.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
InMemoryStrategy,
66
UNAUTHENTICATED,
77
INSUFFICIENT,
8+
UNAUTHORIZED,
89
} from '../../lib/shared/keytar-strategy';
910

1011
describe('GithubLoginModel', function() {
@@ -47,25 +48,55 @@ describe('GithubLoginModel', function() {
4748
});
4849

4950
it('returns INSUFFICIENT if scopes are present', async function() {
50-
sinon.stub(loginModel, 'getScopes').returns(Promise.resolve(['repo', 'read:org']));
51+
sinon.stub(loginModel, 'getScopes').resolves(['repo', 'read:org']);
5152

5253
assert.strictEqual(await loginModel.getToken('https://api.github.com'), INSUFFICIENT);
5354
});
5455

5556
it('returns the token if at least the required scopes are present', async function() {
56-
sinon.stub(loginModel, 'getScopes').returns(Promise.resolve(['repo', 'read:org', 'user:email', 'extra']));
57+
sinon.stub(loginModel, 'getScopes').resolves(['repo', 'read:org', 'user:email', 'extra']);
5758

5859
assert.strictEqual(await loginModel.getToken('https://api.github.com'), '1234');
5960
});
6061

6162
it('caches checked tokens', async function() {
62-
sinon.stub(loginModel, 'getScopes').returns(Promise.resolve(['repo', 'read:org', 'user:email']));
63+
sinon.stub(loginModel, 'getScopes').resolves(['repo', 'read:org', 'user:email']);
6364

6465
assert.strictEqual(await loginModel.getToken('https://api.github.com'), '1234');
6566
assert.strictEqual(loginModel.getScopes.callCount, 1);
6667

6768
assert.strictEqual(await loginModel.getToken('https://api.github.com'), '1234');
6869
assert.strictEqual(loginModel.getScopes.callCount, 1);
6970
});
71+
72+
it('caches tokens that failed to authenticate correctly', async function() {
73+
sinon.stub(loginModel, 'getScopes').resolves(UNAUTHORIZED);
74+
75+
assert.strictEqual(await loginModel.getToken('https://api.github.com'), UNAUTHENTICATED);
76+
assert.strictEqual(loginModel.getScopes.callCount, 1);
77+
78+
assert.strictEqual(await loginModel.getToken('https://api.github.com'), UNAUTHENTICATED);
79+
assert.strictEqual(loginModel.getScopes.callCount, 1);
80+
});
81+
82+
it('caches tokens that had insufficient scopes', async function() {
83+
sinon.stub(loginModel, 'getScopes').resolves(['repo', 'read:org']);
84+
85+
assert.strictEqual(await loginModel.getToken('https://api.github.com'), INSUFFICIENT);
86+
assert.strictEqual(loginModel.getScopes.callCount, 1);
87+
88+
assert.strictEqual(await loginModel.getToken('https://api.github.com'), INSUFFICIENT);
89+
assert.strictEqual(loginModel.getScopes.callCount, 1);
90+
});
91+
92+
it('does not cache network errors', async function() {
93+
sinon.stub(loginModel, 'getScopes').rejects(new Error('You unplugged your ethernet cable'));
94+
95+
assert.strictEqual(await loginModel.getToken('https://api.github.com'), UNAUTHENTICATED);
96+
assert.strictEqual(loginModel.getScopes.callCount, 1);
97+
98+
assert.strictEqual(await loginModel.getToken('https://api.github.com'), UNAUTHENTICATED);
99+
assert.strictEqual(loginModel.getScopes.callCount, 2);
100+
});
70101
});
71102
});

0 commit comments

Comments
 (0)