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 <jakub@status.im>
This commit is contained in:
Jakub Sokołowski 2021-08-20 11:38:08 +02:00
parent 3b1e270952
commit 0d1cd1d2f0
No known key found for this signature in database
GPG Key ID: 4EF064D0E6D63020
6 changed files with 58 additions and 4 deletions

View File

@ -80,6 +80,14 @@
"description": "GitHub API client secret", "description": "GitHub API client secret",
"required": false "required": false
}, },
"CMD_GITHUB_ORGANIZATIONS": {
"description": "GitHub whitelist of orgs",
"required": false
},
"CMD_GITHUB_SCOPES": {
"description": "GitHub OAuth API scopes",
"required": false
},
"CMD_BITBUCKET_CLIENTID": { "CMD_BITBUCKET_CLIENTID": {
"description": "Bitbucket API client id", "description": "Bitbucket API client id",
"required": false "required": false

View File

@ -54,6 +54,8 @@
"github": { "github": {
"clientID": "change this", "clientID": "change this",
"clientSecret": "change this" "clientSecret": "change this"
"organizations": ["names of github organizations allowed, optional"],
"scopes": ["defaults to 'read:user' scope for auth user"],
}, },
"gitlab": { "gitlab": {
"baseURL": "change this", "baseURL": "change this",

View File

@ -1,12 +1,17 @@
'use strict' 'use strict'
const Router = require('express').Router const Router = require('express').Router
const request = require('request')
const passport = require('passport') const passport = require('passport')
const GithubStrategy = require('passport-github').Strategy const GithubStrategy = require('passport-github').Strategy
const { InternalOAuthError } = require('passport-oauth2')
const config = require('../../config') const config = require('../../config')
const response = require('../../response') const response = require('../../response')
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
const { URL } = require('url') const { URL } = require('url')
const { promisify } = require('util')
const rp = promisify(request)
const githubAuth = module.exports = Router() const githubAuth = module.exports = Router()
@ -15,20 +20,47 @@ function githubUrl (path) {
} }
passport.use(new GithubStrategy({ passport.use(new GithubStrategy({
scope: (config.github.organizations ?
config.github.scopes.concat(['read:org']) : config.github.scope),
clientID: config.github.clientID, clientID: config.github.clientID,
clientSecret: config.github.clientSecret, clientSecret: config.github.clientSecret,
callbackURL: config.serverURL + '/auth/github/callback', callbackURL: config.serverURL + '/auth/github/callback',
authorizationURL: githubUrl('login/oauth/authorize'), authorizationURL: githubUrl('login/oauth/authorize'),
tokenURL: githubUrl('login/oauth/access_token'), tokenURL: githubUrl('login/oauth/access_token'),
userProfileURL: githubUrl('api/v3/user') userProfileURL: githubUrl('api/v3/user')
}, passportGeneralCallback)) }, async (accessToken, refreshToken, profile, done) => {
if (!config.github.organizations) {
return passportGeneralCallback(accessToken, refreshToken, profile, done)
}
const { statusCode, body: data } = await rp({
url: `https://api.github.com/user/orgs`,
method: 'GET', json: true, timeout: 2000,
headers: {
'Authorization': `token ${accessToken}`,
'User-Agent': 'nodejs-http'
}
})
if (statusCode != 200) {
return done(InternalOAuthError(
`Failed to query organizations for user: ${profile.username}`
))
}
const orgs = data.map(({login}) => login)
for (const org of orgs) {
if (config.github.organizations.includes(org)) {
return passportGeneralCallback(accessToken, refreshToken, profile, done)
}
}
return done(InternalOAuthError(
`User orgs not whitelisted: ${profile.username} (${orgs.join(',')})`
))
}))
githubAuth.get('/auth/github', function (req, res, next) { githubAuth.get('/auth/github', function (req, res, next) {
setReturnToFromReferer(req) setReturnToFromReferer(req)
passport.authenticate('github')(req, res, next) passport.authenticate('github')(req, res, next)
}) })
// github auth callback
githubAuth.get('/auth/github/callback', githubAuth.get('/auth/github/callback',
passport.authenticate('github', { passport.authenticate('github', {
successReturnToOrRedirect: config.serverURL + '/', successReturnToOrRedirect: config.serverURL + '/',

View File

@ -115,7 +115,9 @@ module.exports = {
github: { github: {
enterpriseURL: undefined, // if you use github.com, not need to specify enterpriseURL: undefined, // if you use github.com, not need to specify
clientID: undefined, clientID: undefined,
clientSecret: undefined clientSecret: undefined,
organizations: [],
scopes: ['read:user']
}, },
gitlab: { gitlab: {
baseURL: undefined, baseURL: undefined,

View File

@ -69,7 +69,9 @@ module.exports = {
github: { github: {
enterpriseURL: process.env.CMD_GITHUB_ENTERPRISE_URL, enterpriseURL: process.env.CMD_GITHUB_ENTERPRISE_URL,
clientID: process.env.CMD_GITHUB_CLIENTID, clientID: process.env.CMD_GITHUB_CLIENTID,
clientSecret: process.env.CMD_GITHUB_CLIENTSECRET clientSecret: process.env.CMD_GITHUB_CLIENTSECRET,
organizations: toArrayConfig(process.env.CMD_GITHUB_ORGANIZATIONS),
scopes: toArrayConfig(process.env.CMD_GITHUB_SCOPES),
}, },
bitbucket: { bitbucket: {
clientID: process.env.CMD_BITBUCKET_CLIENTID, clientID: process.env.CMD_BITBUCKET_CLIENTID,

View File

@ -80,6 +80,14 @@
"description": "GitHub API client secret", "description": "GitHub API client secret",
"required": false "required": false
}, },
"CMD_GITHUB_ORGANIZATIONS": {
"description": "GitHub whitelist of orgs",
"required": false
},
"CMD_GITHUB_SCOPES": {
"description": "GitHub OAuth API scopes",
"required": false
},
"CMD_BITBUCKET_CLIENTID": { "CMD_BITBUCKET_CLIENTID": {
"description": "Bitbucket API client id", "description": "Bitbucket API client id",
"required": false "required": false