Permalink
Browse files

First checkin.

  • Loading branch information...
0 parents commit aa6ea2b040da40712f617b53e9bf045d509c4ae3 @assaf committed Apr 18, 2012
Showing with 322 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +21 −0 MIT-LICENSE
  3. +55 −0 lib/logger.coffee
  4. +179 −0 lib/server.coffee
  5. +37 −0 package.json
  6. +11 −0 server.js
  7. +15 −0 test.sh
@@ -0,0 +1,4 @@
+node_modules
+log
+tmp
+npm-debug.log
@@ -0,0 +1,21 @@
+Copyright (c) 2012 Assaf Arkin
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
@@ -0,0 +1,55 @@
+# Logging. Exports the application's default logger.
+#
+# The logger supports the logging methods debug, info, warning, error and alert.
+
+
+Path = require("path")
+Winston = require("winston")
+TTY = require("tty")
+# Vendor Winston Syslog handler with a patch
+# require "../vendor/winston-syslog"
+
+
+node_env = process.env.NODE_ENV.toLowerCase()
+debug = !!process.env.DEBUG
+colorize = TTY.isatty(process.stdout.fd)
+
+# Use syslog logging levels.
+Winston.setLevels Winston.config.syslog.levels
+
+# Use debug level in development or when DEBUG environment variable is set.
+if node_env == "development" || debug
+ level = "debug"
+else
+ level = "info"
+
+
+# Log to file based on the current environment.
+filename = Path.resolve(__dirname, "../log/#{node_env}.log")
+Winston.remove Winston.transports.Console
+Winston.add Winston.transports.File,
+ filename: filename
+ level: level
+ json: false
+ timestamp: true
+
+
+# Log to console in development, and in test when DEBUG environment variable is
+# set. Use Syslog in production, if configured.
+switch node_env
+ when "development"
+ # Log to file and console.
+ Winston.add Winston.transports.Console, level: level, colorize: colorize
+ when "test"
+ # Log to console only when running with DEBUG
+ if debug
+ Winston.add Winston.transports.Console, level: level, colorize: colorize
+ when "production"
+ if process.env.SYSLOG_HOST
+ # Use Syslog
+ Winston.add Winston.transports.Syslog,
+ host: process.env.SYSLOG_HOST
+ port: parseInt(process.env.SYSLOG_PORT, 10) || 514
+
+
+module.exports = Winston
@@ -0,0 +1,179 @@
+process.env.NODE_ENV ||= "development"
+
+
+Connect = require("connect")
+Cookies = require("cookies")
+QS = require("querystring")
+Keygrip = require("keygrip")
+Request = require("request")
+URL = require("url")
+logger = require("./logger")
+
+{ HttpProxy } = require("http-proxy")
+
+###
+var options = {
+ https: {
+ key: fs.readFileSync('path/to/your/key.pem', 'utf8'),
+ cert: fs.readFileSync('path/to/your/cert.pem', 'utf8')
+ }
+};
+###
+
+
+proxy = new HttpProxy(
+ target:
+ host: "localhost"
+ port: 3000
+)
+
+
+# These are the HTTP headers we send to the back-end resource when a user it
+# authenticated. The values are the corresponding Github user property.
+HEADERS =
+ "x-github-login": "login"
+ "x-github-name": "name"
+ "x-github-gravatar": "gravatar_id"
+ "x-github-token": "token"
+
+# If set, only authorize members of this team
+team_id = process.env.GITHUB_TEAM_ID
+# If set, only authorize specified logins
+logins = process.env.GITHUB_LOGINS?.split(/,\s*/)
+
+
+server = Connect.createServer()
+
+# Log all requests.
+server.use (req, res, next)->
+ start = Date.now()
+ end_fn = res.end
+ res.end = ->
+ remote_addr = req.socket && (req.socket.remoteAddress || (req.socket.socket && req.socket.socket.remoteAddress))
+ ua = req.headers["user-agent"] || "-"
+ logger.info "#{remote_addr} - \"#{req.method} #{req.originalUrl} HTTP/#{req.httpVersionMajor}.#{req.httpVersionMinor}\" #{res.statusCode} \"#{ua}\" - #{Date.now() - start} ms"
+ res.end = end_fn
+ end_fn.apply(res, arguments)
+ next()
+
+
+keys = null
+server.use Cookies.connect(new Keygrip(keys))
+
+# Get user form cookie and set request headers (passed to proxy)
+server.use (req, res, next)->
+ if cookie = req.cookies.get("user", signed: true)
+ req._user = JSON.parse(cookie)
+ next()
+
+
+# OAuth callback
+server.use (req, res, next)->
+ uri = URL.parse(req.url)
+ if uri.pathname == "/oauth/callback"
+
+ query = QS.parse(uri.search.slice(1))
+ # Exchange OAuth code for access token
+ params =
+ url: "https://github.com/login/oauth/access_token"
+ json:
+ code: query.code
+ client_id: process.env.GITHUB_CLIENT_ID
+ client_secret: process.env.GITHUB_CLIENT_SECRET
+ Request.post params, (error, response, json)->
+ return next(error) if error
+ # If we got OAuth error, just show it.
+ if json && json.error
+ logger.warning json.error
+ #req.flash "error", json.error
+ res.redirect "/"
+ return
+
+ # Get the user name and gravatar ID, so we can display those.
+ token = json.access_token
+ url = "https://api.github.com/user?access_token=#{token}"
+ Request.get url, (error, response, body)->
+ return next(error) if error
+ { login, name, gravatar_id } = JSON.parse(body)
+ user = # we only care for these fields
+ name: name
+ login: login
+ gravatar_id: gravatar_id
+
+ if team_id
+ # Easiest way to determine if user is member of a team:
+ # "In order to list members in a team, the authenticated user must be a member of the team."
+ # -- http://developer.github.com/v3/orgs/teams/
+ url = "https://api.github.com/teams/#{team_id}/members?access_token=#{token}"
+ Request.get url, (error, response, body)->
+ return next(error) if error
+ if response.statusCode == 200
+ members = JSON.parse(body).map((m)-> m.login)
+ if members && members.indexOf(login) >= 0
+ log_in(user)
+ else
+ fail(user)
+ else if logins
+ # Authorization based on Github login
+ if logins.indexOf(login) >= 0
+ log_in(user)
+ else
+ fail(user)
+ else
+ # Default is to deny all
+ fail(user)
+
+ log_in = (user)->
+ logger.info "#{user.login} logged in successfully"
+ logger.debug "Logged in", user
+ # Set the user cookie for the session
+ res._user = user
+ res.cookies.set "user", JSON.stringify(user), signed: true
+ # We use this to redirect back to where we came from
+ res.setHeader "Location", query.return_to || "/"
+ res.statusCode = 303
+ res.end("Redirecting you back to application")
+
+ fail = (user)->
+ logger.warning "Access denied for", user
+ req.flash "error", "You are not authorized to access this application"
+ # Can't redirect back to protected resource, only place to go is home
+ res.redirect "/"
+ else
+ next()
+
+
+# Protect resource by requiring authentication
+server.use (req, res, next)->
+ if req._user
+ next()
+ else
+ # Pass return_to parameter to callback
+ redirect_uri = "http://#{req.headers.host}/oauth/callback?return_to=#{req.url}"
+ logger.info redirect_uri
+ # You need 'repo' scope to list team members
+ scope = "repo" if team_id
+ url = "https://github.com/login/oauth/authorize?" +
+ QS.stringify(client_id: process.env.GITHUB_CLIENT_ID, redirect_uri: redirect_uri, scope: scope)
+ # This takes us to Github
+ res.setHeader "Location", url
+ res.statusCode = 303 # Follow URL with a GET request
+ res.end("Authentication required, you are being redirected")
+
+
+# Proxy request to back-end application
+server.use (req, res)->
+ if user = req._user
+ # Send all headers corresponding to user object
+ for header, prop of HEADERS
+ req.headers[header] = user[prop]
+ else
+ # Make sure these headers are not filled up by client
+ for header of req.headers
+ if /^x-github/i.test(header)
+ delete req.headers[header]
+ # Forward
+ proxy.proxyRequest(req, res)
+
+
+server.listen 8000
@@ -0,0 +1,37 @@
+{ "name": "github-connect",
+ "version": "0.1.0",
+ "description": "Use Github OAuth for access control",
+ "author": "Assaf Arkin <assaf@labnotes.org> (http://labnotes.org/)",
+ "main": "index",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "engines": {
+ "node": ">= 0.6.0"
+ },
+ "dependencies": {
+ "async": "~0.1.18",
+ "coffee-script": "~1.3.1",
+ "cookies": "~0.2.2",
+ "connect": "~1.8.6",
+ "express": "~2.5.9",
+ "http-proxy": "~0.8.0",
+ "keygrip": "~0.2.0",
+ "request": "~2.9.200",
+ "winston": "~0.5.11"
+ },
+ "devDependencies": {
+ },
+ "repository": {
+ "type": "git",
+ "url": "http://github.com/assaf/github-connect"
+ },
+ "bugs": {
+ "url": "http://github.com/assaf/github-connect/issues"
+ },
+ "licenses": [
+ { "type": "MIT",
+ "url": "http://github.com/assaf/github-connect/blob/master/MIT-LICENSE"
+ }
+ ]
+}
@@ -0,0 +1,11 @@
+// Up workers load this file, and via this file server.coffee and the rest of
+// the application (JS and CS files).
+var coffee = require("coffee-script");
+var File = require("fs");
+if (!require.extensions[".coffee"]) {
+ require.extensions[".coffee"] = function (module, filename) {
+ var source = coffee.compile(File.readFileSync(filename, "utf8"));
+ return module._compile(source, filename);
+ };
+}
+module.exports = require(__dirname + "/lib/server.coffee")
15 test.sh
@@ -0,0 +1,15 @@
+# This is an example script for running the Vanity.js server.
+#
+# It contains Github client ID and secret that you can use to authenticate
+# against a test server running on localhost:3000.
+export GITHUB_CLIENT_ID=8fa9b2a82cb28fb664a4
+export GITHUB_CLIENT_SECRET=204093f4739fbe8e9b07cfa16b5cfd70fca5bf66
+# Only accept Github users that are members of this team
+export GITHUB_TEAM_ID=
+# Only accept Github users with these logins; change this if your username is
+# not the same as your Github login (e.g. alice,bob)
+export GITHUB_LOGINS=${USER}
+# API access token.
+export COOKIE_KEYS=9c7516780b8bc00b523c565bb20980ee0865dcfc
+
+node server.js

0 comments on commit aa6ea2b

Please sign in to comment.