Permalink
Browse files

Initial commit.

  • Loading branch information...
0 parents commit 8ae8f1f5b08ff0f25805ece7580914afd735fa94 @niho niho committed with Niklas Holmgren Mar 24, 2011
Showing with 1,116 additions and 0 deletions.
  1. +2 −0 .gitignore
  2. +1 −0 .rvmrc
  3. +3 −0 CHANGELOG
  4. +3 −0 Gemfile
  5. +20 −0 LICENSE
  6. +6 −0 README.md
  7. +11 −0 Rakefile
  8. +31 −0 auth.gemspec
  9. +11 −0 config.ru
  10. +1 −0 init.rb
  11. +190 −0 lib/auth.rb
  12. +11 −0 lib/auth/client.rb
  13. +8 −0 lib/auth/exceptions.rb
  14. +54 −0 lib/auth/helpers.rb
  15. +24 −0 lib/auth/sentry.rb
  16. +234 −0 lib/auth/server.rb
  17. +26 −0 lib/auth/server/views/authorize.erb
  18. +3 −0 lib/auth/version.rb
  19. +140 −0 test/auth_test.rb
  20. +115 −0 test/redis-test.conf
  21. +178 −0 test/server_test.rb
  22. +44 −0 test/test_helper.rb
@@ -0,0 +1,2 @@
+*.gem
+Gemfile.lock
@@ -0,0 +1 @@
+rvm 1.9.2@auth --create
@@ -0,0 +1,3 @@
+*1.0.0*
+
+* First public release
@@ -0,0 +1,3 @@
+source 'http://rubygems.org/'
+
+gemspec
@@ -0,0 +1,20 @@
+Copyright (c) 2010, 2011 Niklas Holmgren, Sutajio
+
+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,6 @@
+Auth
+====
+
+A high performance OAuth2 authorization server using Sinatra and Redis,
+inspired by Resque. Can be run both as a standalone server or as a rack
+middleware.
@@ -0,0 +1,11 @@
+require 'rake/testtask'
+
+$LOAD_PATH.unshift 'lib'
+
+task :default => [:test]
+
+task :test do
+ Dir.glob('test/**/*_test.rb').each do |file|
+ require File.expand_path(file)
+ end
+end
@@ -0,0 +1,31 @@
+$LOAD_PATH.unshift 'lib'
+require 'auth/version'
+
+Gem::Specification.new do |s|
+ s.name = 'auth'
+ s.version = Auth::Version
+ s.summary = 'Auth is a Redis-backed high performance OAuth2 authorization server.'
+ s.description = 'A high performance OAuth2 authorization server using Sinatra and Redis, inspired by Resque. Can be run both as a standalone server or as a rack middleware.'
+
+ s.author = 'Niklas Holmgren'
+ s.email = 'niklas@sutajio.se'
+ s.homepage = 'http://github.com/sutajio/auth/'
+
+ s.files = Dir['README', 'LICENSE', 'CHANGELOG', 'Gemfile', 'Gemfile.lock', 'init.rb', 'config.ru', 'Rakefile', 'test/**/*', 'lib/**/{*,.[a-z]*}']
+ s.require_path = 'lib'
+
+ s.files = %w( README.md Rakefile LICENSE CHANGELOG )
+ s.files += Dir.glob("lib/**/*")
+ s.files += Dir.glob("test/**/*")
+ s.files += Dir.glob("tasks/**/*")
+
+ s.extra_rdoc_files = [ "LICENSE", "README.md" ]
+ s.rdoc_options = ["--charset=UTF-8"]
+
+ s.add_dependency('rack-contrib', '~> 1.0.0')
+ s.add_dependency('sinatra', '~> 1.0.0')
+ s.add_dependency('redis', '~> 2.0.0')
+ s.add_dependency('redis-namespace', '~> 0.8.0')
+
+ s.add_development_dependency('rack-test', '~> 0.5.6')
+end
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+
+$LOAD_PATH.unshift ::File.expand_path(::File.dirname(__FILE__) + '/lib')
+require 'auth/server'
+
+if ENV['REDISTOGO_URL'] || ENV['REDIS_URL']
+ Auth.redis = ENV['REDISTOGO_URL'] || ENV['REDIS_URL']
+end
+
+use Rack::ShowExceptions
+run Auth::Server.new
@@ -0,0 +1 @@
+require 'auth'
@@ -0,0 +1,190 @@
+require 'redis'
+require 'redis/namespace'
+require 'json'
+
+ENV['AUTH_HASH_ALGORITHM'] ||= 'sha256'
+ENV['AUTH_TOKEN_TTL'] ||= '3600'
+
+require 'auth/version'
+require 'auth/exceptions'
+require 'auth/helpers'
+require 'auth/client'
+require 'auth/sentry'
+
+module Auth
+ include Helpers
+ extend self
+
+ # Accepts:
+ # 1. A 'hostname:port' string
+ # 2. A 'hostname:port:db' string (to select the Redis db)
+ # 3. A 'hostname:port/namespace' string (to set the Redis namespace)
+ # 4. A redis URL string 'redis://host:port'
+ # 5. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`,
+ # or `Redis::Namespace`.
+ def redis=(server)
+ if server.respond_to? :split
+ if server =~ /redis\:\/\//
+ redis = Redis.connect(:url => server)
+ else
+ server, namespace = server.split('/', 2)
+ host, port, db = server.split(':')
+ redis = Redis.new(:host => host, :port => port,
+ :thread_safe => true, :db => db)
+ end
+ namespace ||= :auth
+ @redis = Redis::Namespace.new(namespace, :redis => redis)
+ elsif server.respond_to? :namespace=
+ @redis = server
+ else
+ @redis = Redis::Namespace.new(:auth, :redis => server)
+ end
+ end
+
+ # Returns the current Redis connection. If none has been created, will
+ # create a new one.
+ def redis
+ return @redis if @redis
+ self.redis = 'localhost:6379'
+ self.redis
+ end
+
+ #
+ # Accounts
+ #
+
+ def register_account(username, password)
+ raise if username.nil? || username == ''
+ raise if password.nil? || password == ''
+ unless redis.exists("account:#{username}")
+ hash = ENV['AUTH_HASH_ALGORITHM']
+ salt = generate_secret
+ crypted_password = encrypt_password(password, salt, hash)
+ redis.hset("account:#{username}", 'crypted_password', crypted_password)
+ redis.hset("account:#{username}", 'password_hash', hash)
+ redis.hset("account:#{username}", 'password_salt', salt)
+ return true
+ else
+ return false
+ end
+ end
+
+ def authenticate_account(username, password)
+ account = redis.hgetall("account:#{username}")
+ if account['crypted_password']
+ crypted_password = encrypt_password(password,
+ account['password_salt'],
+ account['password_hash'])
+ if crypted_password == account['crypted_password']
+ return true
+ else
+ return false
+ end
+ else
+ return false
+ end
+ end
+
+ def change_password(username, old_password, new_password)
+ if authenticate_account(username, old_password)
+ hash = ENV['AUTH_HASH_ALGORITHM']
+ salt = generate_secret
+ crypted_password = encrypt_password(new_password, salt, hash)
+ redis.hset("account:#{username}", 'crypted_password', crypted_password)
+ redis.hset("account:#{username}", 'password_hash', hash)
+ redis.hset("account:#{username}", 'password_salt', salt)
+ end
+ end
+
+ def remove_account(username)
+ redis.del("account:#{username}")
+ end
+
+ #
+ # Clients
+ #
+
+ def register_client(client_id, name, redirect_uri)
+ raise if client_id.nil? || client_id == ''
+ raise if name.nil? || name == ''
+ raise if redirect_uri.nil? || redirect_uri == ''
+ unless redis.exists("client:#{client_id}")
+ secret = generate_secret
+ client = { :id => client_id,
+ :secret => secret,
+ :name => name,
+ :redirect_uri => redirect_uri }
+ client.each do |key,val|
+ redis.hset("client:#{client_id}", key, val)
+ end
+ return Client.new(client)
+ end
+ end
+
+ def authenticate_client(client_id, client_secret = nil)
+ client = redis.hgetall("client:#{client_id}")
+ if client_secret
+ return client['id'] && client['secret'] == client_secret ? Client.new(client) : false
+ else
+ return client['id'] ? Client.new(client) : false
+ end
+ end
+
+ def remove_client(client_id)
+ redis.del("client:#{client_id}")
+ end
+
+ #
+ # Authorization codes
+ #
+
+ def issue_code(account_id, client_id, redirect_uri, scopes = nil)
+ code = generate_secret
+ redis.set("code:#{client_id}:#{redirect_uri}:#{code}:account", account_id)
+ decode_scopes(scopes).each do |scope|
+ redis.sadd("code:#{client_id}:#{redirect_uri}:#{code}:scopes", scope)
+ end
+ redis.expire("code:#{client_id}:#{redirect_uri}:#{code}:account", 3600)
+ redis.expire("code:#{client_id}:#{redirect_uri}:#{code}:scopes", 3600)
+ return code
+ end
+
+ def validate_code(code, client_id, redirect_uri)
+ account_id = redis.get("code:#{client_id}:#{redirect_uri}:#{code}:account")
+ scopes = redis.smembers("code:#{client_id}:#{redirect_uri}:#{code}:scopes")
+ if account_id
+ return account_id, encode_scopes(scopes)
+ else
+ return false
+ end
+ end
+
+ #
+ # Access tokens
+ #
+
+ def issue_token(account_id, scopes = nil, ttl = nil)
+ token = generate_secret
+ redis.set("token:#{token}:account", account_id)
+ decode_scopes(scopes).each do |scope|
+ redis.sadd("token:#{token}:scopes", scope)
+ end
+ if ttl
+ redis.expire("token:#{token}:account", ttl)
+ redis.expire("token:#{token}:scopes", ttl)
+ end
+ return token
+ end
+
+ def validate_token(token, scopes = nil)
+ account_id = redis.get("token:#{token}:account")
+ if account_id &&
+ decode_scopes(scopes).all? {|scope|
+ redis.sismember("token:#{token}:scopes", scope) }
+ return account_id
+ else
+ return false
+ end
+ end
+
+end
@@ -0,0 +1,11 @@
+module Auth
+ class Client
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def method_missing(method)
+ @attributes[method.to_s] || @attributes[method.to_sym]
+ end
+ end
+end
@@ -0,0 +1,8 @@
+module Auth
+ class AuthException < RuntimeError; end
+ class InvalidRequest < AuthException; end
+ class UnauthorizedClient < AuthException; end
+ class AccessDenied < AuthException; end
+ class UnsupportedResponseType < AuthException; end
+ class InvalidScope < AuthException; end
+end
@@ -0,0 +1,54 @@
+require 'base64'
+require 'digest/sha2'
+
+module Auth
+ module Helpers
+
+ # Generate a unique cryptographically secure secret
+ def generate_secret
+ Base64.encode64(
+ Digest::SHA256.digest("#{Time.now}-#{rand}")
+ ).gsub('/','x').gsub('+','y').gsub('=','').strip
+ end
+
+ # Obfuscate a password using a salt and a cryptographic hash function
+ def encrypt_password(password, salt, hash)
+ case hash.to_s
+ when 'sha256'
+ Digest::SHA256.hexdigest("#{password}-#{salt}")
+ else
+ raise 'Unsupported hash algorithm'
+ end
+ end
+
+ # Given a Ruby object, returns a string suitable for storage in a
+ # queue.
+ def encode(object)
+ object.to_json
+ end
+
+ # Given a string, returns a Ruby object.
+ def decode(object)
+ return unless object
+ begin
+ JSON.parse(object)
+ rescue JSON::ParserError
+ end
+ end
+
+ # Decode a space delimited string of security scopes and return an array
+ def decode_scopes(scopes)
+ if scopes.is_a?(Array)
+ scopes.map {|s| s.to_s.strip }
+ else
+ scopes.to_s.split(' ').map {|s| s.strip }
+ end
+ end
+
+ def encode_scopes(*scopes)
+ scopes = scopes.flatten.compact
+ scopes.map {|s| s.to_s.strip.gsub(' ','_') }.sort.join(' ')
+ end
+
+ end
+end
@@ -0,0 +1,24 @@
+module Auth
+ class Sentry
+ class User
+ def initialize(id); @id = id; end
+ def id; @id; end
+ end
+
+ def initialize(request)
+ @request = request
+ end
+
+ def authenticate!
+ if Auth.authenticate_account(@request.params['username'], @request.params['password'])
+ @user_id = @request.params['username']
+ else
+ raise AuthException, 'Invalid username or password'
+ end
+ end
+
+ def user
+ @user_id ? User.new(@user_id) : nil
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit 8ae8f1f

Please sign in to comment.