• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

suculent / thinx-device-api / #252645614

08 Nov 2023 06:04PM UTC coverage: 71.784% (+66.5%) from 5.266%
#252645614

push

suculent
Merge branch 'main'

* main: (49 commits)
  maintenance release v1.9.2451
  removed services/broker submodule before re-adding
  removed lua-iinspect
  log leak fix
  log cleanup, redeploy after changing expired Rollbar Project Access Tokens
  submodule sync
  version bump, log cleanup and base image update after fixing GitHub OAuth
  dependency updates, fixing github login (has code but different object structure)
  test passes, but parsing fails
  spec fix for staging
  fails in tests, because code is B
  removed json
  fixes
  gpg
  debugging broken github-oauth login
  task renamed
  recent test passed, build stable, adding debug logging only for next refactoring steps
  fix for potent. unlinked github login addRoutes
  recent test passed, build stable, adding debug logging only for next refactoring steps
  github fix
  ...

# Conflicts:
#	package-lock.json
#	package.json

1795 of 3438 branches covered (0.0%)

Branch coverage included in aggregate %.

20 of 51 new or added lines in 6 files covered. (39.22%)

182 existing lines in 5 files now uncovered.

8165 of 10437 relevant lines covered (78.23%)

7.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

28.4
/lib/router.github.js
1
// /api/v2/oauth/github
2

3
let RSAKey = require("../lib/thinx/rsakey.js");
1✔
4
let rsakey = new RSAKey();
1✔
5
let GitHub = require("../lib/thinx/github.js");
1✔
6

7
let envi = require("../_envi.json");
1✔
8
let owner = envi.oid;
1✔
9

10
const Globals = require("./thinx/globals");
1✔
11
const prefix = Globals.prefix();
1✔
12
const Database = require("../lib/thinx/database.js");
1✔
13
let db_uri = new Database().uri();
1✔
14
let userlib = require("nano")(db_uri).use(prefix + "managed_users"); // lgtm [js/unused-local-variable]
1✔
15

16
const github_ocfg = Globals.github_ocfg();
1✔
17
const https = require('https');
1✔
18
const sha256 = require("sha256");
1✔
19

20
const app_config = Globals.app_config();
1✔
21

22
let AuditLog = require("../lib/thinx/audit"); let alog = new AuditLog();
1✔
23

24
const Util = require("./thinx/util");
1✔
25

26
//
27
// OAuth2 for GitHub
28
//
29

30
module.exports = function (app) {
1✔
31

32
    /*
33
    * OAuth 2 with GitHub
34
    */
35

36
    let user = app.owner;
15✔
37

38
    let githubOAuth;
39

40
    if (typeof (process.env.GITHUB_CLIENT_SECRET) !== "undefined" && process.env.GITHUB_CLIENT_SECRET !== null) {
15!
41
        try {
15✔
42
            let specs = {
15✔
43
                githubClient: process.env.GITHUB_CLIENT_ID,
44
                githubSecret: process.env.GITHUB_CLIENT_SECRET,
45
                baseURL: github_ocfg.base_url, // should be rather gotten from global config!
46
                loginURI: '/api/oauth/github',
47
                callbackURI: '/api/oauth/github/callback',
48
                scope: 'user'
49
            };
50
            githubOAuth = require('./thinx/oauth-github.js')(specs);
15✔
51
        } catch (e) {
52
            console.log(`[debug] [oauth] [github] github_ocfg init error: ${e}`);
×
53
        }
54
    }
55

56
    function validateGithubUser(response, token, userWrapper) {
57

58
        let owner_id = userWrapper.owner; // must not be nil
×
59

60
        // Check user and make note on user login
61
        userlib.get(userWrapper.owner, (error, udoc) => {
×
62

63
            // Error case covers creating new user/managing deleted account
64
            if (error) {
×
65

66
                if (error.toString().indexOf("Error: deleted") !== -1) {
×
67
                    console.log("🔨 [debug] [oauth] [check] user document deleted");
×
68
                    response.redirect(
×
69
                        app_config.public_url + '/error.html?success=failed&title=Sorry&reason=' +
70
                        encodeURI('Account document deleted.')
71
                    );
72
                    return;
×
73
                }
74

75
                // May exist, but be deleted. Can be cleared using Filtered Replication Handler "del"
76
                if (typeof (udoc) !== "undefined" && udoc !== null) {
×
77
                    if ((typeof (udoc.deleted) !== "undefined") && udoc.deleted === true) {
×
78
                        console.log("🔨 [debug] [oauth] [check] user account marked as deleted");
×
79
                        response.redirect(
×
80
                            app_config.public_url + '/error.html?success=failed&title=Sorry&reason=' +
81
                            encodeURI('Account deleted.')
82
                        );
83
                        return;
×
84
                    }
85
                } else {
86
                    console.log("No such owner, should create one...");
×
87
                }
88

89
                // No such owner, create without activation.
90
                this.user.create(userWrapper, false, response, (/* res, success, status*/) => {
×
91

92
                    console.log("[OID:" + owner_id + "] [NEW_SESSION] [oauth] 2485:");
×
93

94
                    alog.log(owner_id, "OAuth User created. ");
×
95

96
                    app.redis_client.v4.set(token, JSON.stringify(userWrapper));
×
97
                    app.redis_client.v4.expire(token, 30);
×
98

99
                    const courl = app_config.public_url + "/auth.html?t=" + token + "&g=true"; // require GDPR consent
×
100

101
                    console.log("Redirecting to login (2)", courl);
×
102
                    response.redirect(courl); // for successful login, this must be a response to /oauth/<idp>/callback 
×
103
                });
104
                return;
×
105
            }
106

107
            user.trackUserLogin(owner_id);
×
108

109
            console.log("validateGithubUser", { token }, { userWrapper });
×
110
            app.redis_client.v4.set(token, JSON.stringify(userWrapper));
×
111
            app.redis_client.v4.expire(token, 3600);
×
112

113
            let gdpr = false;
×
114
            if (typeof (udoc) !== "undefined" && udoc !== null) {
×
115
                if (typeof (udoc.info) !== "undefined") {
×
116
                    if (typeof (udoc.gdpr_consent) !== "undefined" && udoc.gdpr_consent === true) {
×
117
                        gdpr = true;
×
118
                    }
119
                }
120
            }
121

122
            const ourl = app_config.public_url + "/auth.html?t=" + token + "&g=" + gdpr; // require GDPR consent
×
123
            response.redirect(ourl);
×
124
        }); // userlib.get
125
    }
126

127
    function githubLogin(access_token, hdata, res, original_response) {
128
        let token = "ghat:" + access_token;
×
129
        let owner_id, given_name, family_name, email;
130
    
131
        if ((typeof (hdata.name) !== "undefined") && hdata.name !== null) {
×
132
            let in_name_array = hdata.name.split(" ");
×
133
            given_name = in_name_array[0];
×
134
            family_name = in_name_array[in_name_array.count - 1];
×
135
        } else {
136
            family_name = hdata.login;
×
137
            given_name = hdata.login;
×
138
            console.log("🔨 [debug] [github] [token] Warning: no name in GitHub access token response, using login: ", { hdata }); // logs personal data in case the user has no name!
×
139
        }
140
        email = hdata.email || hdata.login;
×
141
    
142
        try {
×
143
            owner_id = sha256(prefix + email);
×
144
        } catch (e) {
145
            console.log("☣️ [error] [github] [token] error parsing e-mail: " + e + " email: " + email);
×
146
            return res.redirect(app_config.public_url + '/error.html?success=failed&title=Sorry&reason=Missing%20e-mail.');
×
147
        }
148
        validateGithubUser(original_response, token, {
×
149
            first_name: given_name,
150
            last_name: family_name,
151
            email: email,
152
            owner: owner_id,
153
            username: owner_id
154
        });
155
    }
156

157
    function secureGithubCallbacks(original_response, callback) {
158

159
        if (typeof (githubOAuth) === "undefined") {
2!
160
            console.log("[critical] [githubOAuth] undefined on secure! attempting to fix...");
×
161

162
            try {
×
163
                let specs = {
×
164
                    githubClient: process.env.GITHUB_CLIENT_ID,
165
                    githubSecret: process.env.GITHUB_CLIENT_SECRET,
166
                    baseURL: github_ocfg.base_url, // should be rather gotten from global config!
167
                    loginURI: '/api/oauth/github',
168
                    callbackURI: '/api/oauth/github/callback',
169
                    scope: 'user'
170
                };
171
                githubOAuth = require('./thinx/oauth-github.js')(specs);
×
172

173
            } catch (e) {
174
                console.log(`[debug] [oauth] [github] github_ocfg init error: ${e}`);
×
175
            }
176
        }
177

178
        // configure callbacks for Emitter events
179

180
        githubOAuth.on('error', (err) => {
2✔
181
            console.error('[debug] [oauth] [github] there was a login error', err);
3✔
182
            if (process.env.ENVIRONMENT == "test")
3!
183
                if (typeof (original_response) !== "undefined") original_response.end("test-ok");
3!
184
        });
185

186
        githubOAuth.on('token', (access_token, /* resp, _res, req */) => {
2✔
187

NEW
188
            if (!Util.isDefined(access_token)) {
×
NEW
189
                original_response.status(401).end();
×
NEW
190
                console.log("[github] oauth_response missing (test or intrusion)");
×
191
                return;
×
192
            }
193

NEW
194
            if ((!access_token) || (access_token.indexOf("bad_verification") !== -1)) {
×
195
                console.log("🔨 [debug] [github] [token] No token, exiting.");
×
196
                original_response.status(401).end();
×
197
                return;
×
198
            }
199

200
            const requestOptions = {
×
201
                host: 'api.github.com',
202
                port: 443,
203
                path: '/user',
204
                headers: {
205
                    'User-Agent': 'THiNX', // Application name from GitHub / Settings / Developer Settings 
206
                    'Authorization': 'token ' + access_token, 
207
                    'Accept': 'application/vnd.github+json' 
208
                } 
209
            };
210

211
            https.get(requestOptions, (res) => {
×
212
                let data = '';
×
213
                res.on('data', (chunk) => {
×
214
                    data += chunk;
×
215
                });
216
                res.on('end', () => {
×
217
                    githubLogin(access_token, JSON.parse(data), res, original_response);
×
218
                });
219
            });
220
        });
221

222
        callback(); // async completes the secureGithubCallbacks()
2✔
223
    }
224

225
    // Initial page redirecting to OAuth2 provider
226
    app.get('/api/oauth/github', function (req, res) {
15✔
227
        if (typeof (req.session) !== "undefined") {
1!
228
            console.log("🔨 [debug] GET /api/oauth/github will destroy old session...");
1✔
229
            req.session.destroy();
1✔
230
        }
231
        if (typeof (githubOAuth) !== "undefined") {
1!
232
            console.log("🔨 [debug] GET /api/oauth/github calling githubOAuth.login");
1✔
233
            githubOAuth.login(req, res);
1✔
234
        } else {
235
            res.status(400).end();
×
236
        }
237
    });
238

239
    // Callback service parsing the authorization token and asking for the access token
240
    app.get('/api/oauth/github/callback', function (req, res) {
15✔
241
        // save original response to callbacks in this code path... when callback is called, response is used to reply (except for error)
242
        secureGithubCallbacks(res, () => {
2✔
243
            githubOAuth.callback(req, res, (err) => {
2✔
244
                console.log("[spec] GitHub OAuth result", err);
×
245
            });
246
        });
247
    });
248

249
    app.post('/api/github/token', (req, res) => {
15✔
250
        let token = req.body.token;
×
251
        rsakey.list(owner, (success, list) => {
×
252
            console.log("RSAKey list success:", success);
×
253
            if (list.length == 0) return console.log("No RSA Keys to add from", list);
×
254
            let key = list[0];
×
255
            let pubkey = key.pubkey;
×
256
            console.log("GitHub adding pub key:", pubkey);
×
257
            GitHub.addPublicKey(token, pubkey, (result) => {
×
258
                res.status(200).end(result);
×
259
            });
260
        });
261
    });
262
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc