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

Commit 6f373fb

Browse files
committed
https://trello.com/c/s3jFLzfJ - support for github auth
1 parent 2c685ac commit 6f373fb

25 files changed

+557
-323
lines changed

api_server/lib/oauth/oauth_module.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ class OAuthModule extends APIServerModule {
116116
return !!this.oauthConfig.tokenFromFragment;
117117
}
118118

119+
// does this provider support signup?
120+
supportsSignup () {
121+
return this.oauthConfig.supportsSignup;
122+
}
123+
119124
// extract the access token from a fragment sent to the browser
120125
extractTokenFromFragment (options) {
121126
// special allowance for token in the fragment, which we can't access,

api_server/modules/github_auth/github_auth.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
'use strict';
44

55
const OAuthModule = require(process.env.CS_API_TOP + '/lib/oauth/oauth_module.js');
6+
const GithubAuthorizer = require('./github_authorizer');
67

78
const OAUTH_CONFIG = {
89
provider: 'github',
@@ -11,9 +12,10 @@ const OAUTH_CONFIG = {
1112
authPath: 'login/oauth/authorize',
1213
tokenPath: 'login/oauth/access_token',
1314
exchangeFormat: 'query',
14-
scopes: 'repo,read:user',
15+
scopes: 'repo,read:user,user:email',
1516
noGrantType: true,
16-
hasIssues: true
17+
hasIssues: true,
18+
supportsSignup: true
1719
};
1820

1921
class GithubAuth extends OAuthModule {
@@ -22,6 +24,15 @@ class GithubAuth extends OAuthModule {
2224
super(config);
2325
this.oauthConfig = OAUTH_CONFIG;
2426
}
27+
28+
// match the given github identity to a CodeStream identity
29+
async getUserIdentity (options) {
30+
const authorizer = new GithubAuthorizer({ options });
31+
return await authorizer.getGithubIdentity(
32+
options.accessToken,
33+
options.providerInfo
34+
);
35+
}
2536
}
2637

2738
module.exports = GithubAuth;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// provide a class to handle authorizing credentials for the github provider
2+
3+
'use strict';
4+
5+
const { Octokit } = require('@octokit/rest');
6+
const RandomString = require('randomstring');
7+
const ProviderErrors = require(process.env.CS_API_TOP + '/modules/providers/errors');
8+
9+
class GithubAuthorizer {
10+
11+
constructor (options) {
12+
Object.assign(this, options);
13+
this.request = options.options.request;
14+
this.request.errorHandler.add(ProviderErrors);
15+
}
16+
17+
// return identifying information associated with the fetched access token
18+
async getGithubIdentity (accessToken, providerInfo) {
19+
this.accessToken = accessToken;
20+
this.providerInfo = this.providerInfo || providerInfo;
21+
this.octokit = new Octokit({ auth: accessToken });
22+
const userData = await this.githubApiRequest('users', 'getAuthenticated');
23+
let emailData = await this.githubApiRequest('users', 'listEmails');
24+
this.request.log('user data: ' + JSON.stringify(userData, undefined, 5));
25+
this.request.log('email data: ' + JSON.stringify(emailData, undefined, 5));
26+
emailData = emailData instanceof Array ? emailData : null;
27+
if (!userData || !emailData) {
28+
throw this.request.errorHandler.error('noIdentityMatch');
29+
}
30+
31+
const primaryEmail = emailData.find(e => e.primary);
32+
return {
33+
userId: userData.id,
34+
accessToken,
35+
username: userData.login,
36+
fullName: userData.name,
37+
email: primaryEmail.email
38+
};
39+
}
40+
41+
// make a github api request
42+
async githubApiRequest(module, method) {
43+
if (this.accessToken === 'invalid-token') { // for testing
44+
throw this.request.errorHandler.error('invalidProviderCredentials', { reason: 'invalid token' });
45+
}
46+
const mockCode = (
47+
this.providerInfo &&
48+
this.providerInfo.code &&
49+
this.providerInfo.code.match(/^mock.*-(.+)-(.+)$/)
50+
);
51+
if (mockCode && mockCode.length >= 3) {
52+
if (method === 'getAuthenticated') {
53+
return this._mockIdentity(mockCode[1]);
54+
}
55+
else if (method === 'listEmails') {
56+
return this._mockEmails();
57+
}
58+
}
59+
try {
60+
return (await this.octokit[module][method]()).data;
61+
}
62+
catch (error) {
63+
throw this.request.errorHandler.error('invalidProviderCredentials', { reason: error.message });
64+
}
65+
}
66+
67+
_mockIdentity (mockUserId) {
68+
return {
69+
id: mockUserId,
70+
login: RandomString.generate(8),
71+
name: `${RandomString.generate(8)} ${RandomString.generate(8)}`
72+
};
73+
}
74+
75+
_mockEmails () {
76+
return [{
77+
primary: true,
78+
email: this.providerInfo.mockEmail || `${RandomString.generate(8)}@${RandomString.generate(8)}.com`
79+
}];
80+
}
81+
}
82+
83+
module.exports = GithubAuthorizer;

api_server/modules/github_enterprise_auth/github_enterprise_auth.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const OAUTH_CONFIG = {
1010
authPath: 'login/oauth/authorize',
1111
tokenPath: 'login/oauth/access_token',
1212
exchangeFormat: 'query',
13-
scopes: 'repo,read:user',
13+
scopes: 'repo,read:user,user:email',
1414
noGrantType: true,
1515
hasIssues: true,
1616
forEnterprise: true,

api_server/modules/providers/provider_deauth_request.js

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,42 +46,63 @@ class ProviderDeauthRequest extends RestfulRequest {
4646
const teamId = this.request.body.teamId.toLowerCase();
4747
const provider = this.request.params.provider.toLowerCase();
4848
let { host, subId } = this.request.body;
49-
let key = `providerInfo.${teamId}.${provider}`;
49+
let userKey = `providerInfo.${provider}`;
50+
let teamKey = `providerInfo.${teamId}.${provider}`;
5051
if (host) {
5152
host = host.toLowerCase().replace(/\./g, '*');
52-
key += `.hosts.${host}`;
53+
userKey += `.hosts.${host}`;
54+
teamKey += `.hosts.${host}`;
5355
}
5456
if (subId) {
55-
key += `.multiple.${subId}`;
57+
userKey += `.multiple.${subId}`;
58+
teamKey += `.multiple.${subId}`;
5659
}
57-
const existingProviderInfo = this.user.getProviderInfo(provider, teamId);
60+
61+
const existingUserProviderInfo = this.user.getProviderInfo(provider);
5862
if (
5963
!host &&
60-
existingProviderInfo &&
61-
existingProviderInfo.hosts &&
62-
Object.keys(existingProviderInfo.hosts).length > 0
64+
existingUserProviderInfo &&
65+
existingUserProviderInfo.hosts &&
66+
Object.keys(existingUserProviderInfo.hosts).length > 0
6367
) {
6468
// if we have enterprise hosts for this provider, don't stomp on them
65-
key += '.accessToken';
69+
userKey += '.accessToken';
6670
}
71+
72+
const existingTeamProviderInfo = this.user.getProviderInfo(provider, teamId);
73+
if (
74+
!host &&
75+
existingTeamProviderInfo &&
76+
existingTeamProviderInfo.hosts &&
77+
Object.keys(existingTeamProviderInfo.hosts).length > 0
78+
) {
79+
// if we have enterprise hosts for this provider, don't stomp on them
80+
teamKey += '.accessToken';
81+
}
82+
6783
const op = {
6884
$unset: {
69-
[key]: true
85+
[userKey]: true,
86+
[teamKey]: true
7087
},
7188
$set: {
7289
modifiedAt: Date.now()
7390
}
7491
};
92+
7593
// this is really only for "sharing model" chat providers, which will provide a subId
76-
if (subId && existingProviderInfo && existingProviderInfo.multiple) {
77-
const serviceAuth = this.api.services[`${provider}Auth`];
78-
if (serviceAuth) {
79-
const providerUserId = await serviceAuth.getUserId(existingProviderInfo.multiple[subId]);
80-
if (providerUserId) {
81-
const identity = `${provider}::${providerUserId}`;
82-
if ((this.user.get('providerIdentities') || []).find(id => id === identity)) {
83-
op.$pull = { providerIdentities: identity };
84-
}
94+
const serviceAuth = this.api.services[`${provider}Auth`];
95+
if (
96+
serviceAuth &&
97+
subId &&
98+
existingTeamProviderInfo &&
99+
existingTeamProviderInfo.multiple
100+
) {
101+
const providerUserId = await serviceAuth.getUserId(existingTeamProviderInfo.multiple[subId]);
102+
if (providerUserId) {
103+
const identity = `${provider}::${providerUserId}`;
104+
if ((this.user.get('providerIdentities') || []).find(id => id === identity)) {
105+
op.$pull = { providerIdentities: identity };
85106
}
86107
}
87108
}

0 commit comments

Comments
 (0)