Skip to content

Commit 0d1cd1d

Browse files
committed
feat: add organizations whitelist to GitHub OAuth
Currently CodiMD does not support limiting access of GitHub OAuth users based on their organization membership. This is a very useful functionality for teams that want to limit write access to their notes. I've implemented a crude solution to this problem, which most probably requires some adjusments to make it better. I'm not sure if this implementation is kosher, but it definitely works on my deployment. Open to suggestions on how I can improve it. Signed-off-by: Jakub Sokołowski <[email protected]>
1 parent 3b1e270 commit 0d1cd1d

File tree

6 files changed

+58
-4
lines changed

6 files changed

+58
-4
lines changed

app.json

+8
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@
8080
"description": "GitHub API client secret",
8181
"required": false
8282
},
83+
"CMD_GITHUB_ORGANIZATIONS": {
84+
"description": "GitHub whitelist of orgs",
85+
"required": false
86+
},
87+
"CMD_GITHUB_SCOPES": {
88+
"description": "GitHub OAuth API scopes",
89+
"required": false
90+
},
8391
"CMD_BITBUCKET_CLIENTID": {
8492
"description": "Bitbucket API client id",
8593
"required": false

config.json.example

+2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
"github": {
5555
"clientID": "change this",
5656
"clientSecret": "change this"
57+
"organizations": ["names of github organizations allowed, optional"],
58+
"scopes": ["defaults to 'read:user' scope for auth user"],
5759
},
5860
"gitlab": {
5961
"baseURL": "change this",

lib/auth/github/index.js

+34-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
'use strict'
22

33
const Router = require('express').Router
4+
const request = require('request')
45
const passport = require('passport')
56
const GithubStrategy = require('passport-github').Strategy
7+
const { InternalOAuthError } = require('passport-oauth2')
68
const config = require('../../config')
79
const response = require('../../response')
810
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
911
const { URL } = require('url')
12+
const { promisify } = require('util')
13+
14+
const rp = promisify(request)
1015

1116
const githubAuth = module.exports = Router()
1217

@@ -15,20 +20,47 @@ function githubUrl (path) {
1520
}
1621

1722
passport.use(new GithubStrategy({
23+
scope: (config.github.organizations ?
24+
config.github.scopes.concat(['read:org']) : config.github.scope),
1825
clientID: config.github.clientID,
1926
clientSecret: config.github.clientSecret,
2027
callbackURL: config.serverURL + '/auth/github/callback',
2128
authorizationURL: githubUrl('login/oauth/authorize'),
2229
tokenURL: githubUrl('login/oauth/access_token'),
2330
userProfileURL: githubUrl('api/v3/user')
24-
}, passportGeneralCallback))
31+
}, async (accessToken, refreshToken, profile, done) => {
32+
if (!config.github.organizations) {
33+
return passportGeneralCallback(accessToken, refreshToken, profile, done)
34+
}
35+
const { statusCode, body: data } = await rp({
36+
url: `https://api.github.com/user/orgs`,
37+
method: 'GET', json: true, timeout: 2000,
38+
headers: {
39+
'Authorization': `token ${accessToken}`,
40+
'User-Agent': 'nodejs-http'
41+
}
42+
})
43+
if (statusCode != 200) {
44+
return done(InternalOAuthError(
45+
`Failed to query organizations for user: ${profile.username}`
46+
))
47+
}
48+
const orgs = data.map(({login}) => login)
49+
for (const org of orgs) {
50+
if (config.github.organizations.includes(org)) {
51+
return passportGeneralCallback(accessToken, refreshToken, profile, done)
52+
}
53+
}
54+
return done(InternalOAuthError(
55+
`User orgs not whitelisted: ${profile.username} (${orgs.join(',')})`
56+
))
57+
}))
2558

2659
githubAuth.get('/auth/github', function (req, res, next) {
2760
setReturnToFromReferer(req)
2861
passport.authenticate('github')(req, res, next)
2962
})
3063

31-
// github auth callback
3264
githubAuth.get('/auth/github/callback',
3365
passport.authenticate('github', {
3466
successReturnToOrRedirect: config.serverURL + '/',

lib/config/default.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ module.exports = {
115115
github: {
116116
enterpriseURL: undefined, // if you use github.com, not need to specify
117117
clientID: undefined,
118-
clientSecret: undefined
118+
clientSecret: undefined,
119+
organizations: [],
120+
scopes: ['read:user']
119121
},
120122
gitlab: {
121123
baseURL: undefined,

lib/config/environment.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ module.exports = {
6969
github: {
7070
enterpriseURL: process.env.CMD_GITHUB_ENTERPRISE_URL,
7171
clientID: process.env.CMD_GITHUB_CLIENTID,
72-
clientSecret: process.env.CMD_GITHUB_CLIENTSECRET
72+
clientSecret: process.env.CMD_GITHUB_CLIENTSECRET,
73+
organizations: toArrayConfig(process.env.CMD_GITHUB_ORGANIZATIONS),
74+
scopes: toArrayConfig(process.env.CMD_GITHUB_SCOPES),
7375
},
7476
bitbucket: {
7577
clientID: process.env.CMD_BITBUCKET_CLIENTID,

scalingo.json

+8
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@
8080
"description": "GitHub API client secret",
8181
"required": false
8282
},
83+
"CMD_GITHUB_ORGANIZATIONS": {
84+
"description": "GitHub whitelist of orgs",
85+
"required": false
86+
},
87+
"CMD_GITHUB_SCOPES": {
88+
"description": "GitHub OAuth API scopes",
89+
"required": false
90+
},
8391
"CMD_BITBUCKET_CLIENTID": {
8492
"description": "Bitbucket API client id",
8593
"required": false

0 commit comments

Comments
 (0)