auth 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,3 @@
1
+ *1.0.0*
2
+
3
+ * First public release
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010, 2011 Niklas Holmgren, Sutajio
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,6 @@
1
+ Auth
2
+ ====
3
+
4
+ A high performance OAuth2 authorization server using Sinatra and Redis,
5
+ inspired by Resque. Can be run both as a standalone server or as a rack
6
+ middleware.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'rake/testtask'
2
+
3
+ $LOAD_PATH.unshift 'lib'
4
+
5
+ task :default => [:test]
6
+
7
+ task :test do
8
+ Dir.glob('test/**/*_test.rb').each do |file|
9
+ require File.expand_path(file)
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Auth
2
+ class Client
3
+ def initialize(attributes)
4
+ @attributes = attributes
5
+ end
6
+
7
+ def method_missing(method)
8
+ @attributes[method.to_s] || @attributes[method.to_sym]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ module Auth
2
+ class AuthException < RuntimeError; end
3
+ class InvalidRequest < AuthException; end
4
+ class UnauthorizedClient < AuthException; end
5
+ class AccessDenied < AuthException; end
6
+ class UnsupportedResponseType < AuthException; end
7
+ class InvalidScope < AuthException; end
8
+ end
@@ -0,0 +1,54 @@
1
+ require 'base64'
2
+ require 'digest/sha2'
3
+
4
+ module Auth
5
+ module Helpers
6
+
7
+ # Generate a unique cryptographically secure secret
8
+ def generate_secret
9
+ Base64.encode64(
10
+ Digest::SHA256.digest("#{Time.now}-#{rand}")
11
+ ).gsub('/','x').gsub('+','y').gsub('=','').strip
12
+ end
13
+
14
+ # Obfuscate a password using a salt and a cryptographic hash function
15
+ def encrypt_password(password, salt, hash)
16
+ case hash.to_s
17
+ when 'sha256'
18
+ Digest::SHA256.hexdigest("#{password}-#{salt}")
19
+ else
20
+ raise 'Unsupported hash algorithm'
21
+ end
22
+ end
23
+
24
+ # Given a Ruby object, returns a string suitable for storage in a
25
+ # queue.
26
+ def encode(object)
27
+ object.to_json
28
+ end
29
+
30
+ # Given a string, returns a Ruby object.
31
+ def decode(object)
32
+ return unless object
33
+ begin
34
+ JSON.parse(object)
35
+ rescue JSON::ParserError
36
+ end
37
+ end
38
+
39
+ # Decode a space delimited string of security scopes and return an array
40
+ def decode_scopes(scopes)
41
+ if scopes.is_a?(Array)
42
+ scopes.map {|s| s.to_s.strip }
43
+ else
44
+ scopes.to_s.split(' ').map {|s| s.strip }
45
+ end
46
+ end
47
+
48
+ def encode_scopes(*scopes)
49
+ scopes = scopes.flatten.compact
50
+ scopes.map {|s| s.to_s.strip.gsub(' ','_') }.sort.join(' ')
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,24 @@
1
+ module Auth
2
+ class Sentry
3
+ class User
4
+ def initialize(id); @id = id; end
5
+ def id; @id; end
6
+ end
7
+
8
+ def initialize(request)
9
+ @request = request
10
+ end
11
+
12
+ def authenticate!
13
+ if Auth.authenticate_account(@request.params['username'], @request.params['password'])
14
+ @user_id = @request.params['username']
15
+ else
16
+ raise AuthException, 'Invalid username or password'
17
+ end
18
+ end
19
+
20
+ def user
21
+ @user_id ? User.new(@user_id) : nil
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Authorize <%= h(@client.name) %> to access your account</title>
5
+ </head>
6
+ <body>
7
+ <div class="dialog secure">
8
+ <h1>Authorize <%= h(@client.name) %> to access your account</h1>
9
+ <p><%= h(@client.name) %> is asking for access to your account.
10
+ We need to make sure it is OK with you.</p>
11
+ <form method="post">
12
+ <% if params[:response_type] %>
13
+ <input type="hidden" name="response_type" value="<%= cgi_escape(params[:response_type]) %>" />
14
+ <% end %>
15
+ <input type="hidden" name="client_id" value="<%= cgi_escape(params[:client_id]) %>" />
16
+ <input type="hidden" name="redirect_uri" value="<%= cgi_escape(params[:redirect_uri] || @client.redirect_uri) %>" />
17
+ <input type="hidden" name="scope" value="<%= cgi_escape(params[:scope]) %>" />
18
+ <input type="hidden" name="state" value="<%= cgi_escape(params[:state]) %>" />
19
+ <button type="submit">Yes, allow access</button>
20
+ <a href="<%= merge_uri_based_on_response_type(@client.redirect_uri, :error => 'access_denied', :error_reason => 'user_denied', :error_description => 'The user denied your request.', :state => params[:state]) %>">
21
+ No thanks, take me back to <%= h(@client.name) %>
22
+ </a>
23
+ </form>
24
+ </div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,234 @@
1
+ require 'rubygems'
2
+ require 'sinatra/base'
3
+ require 'erb'
4
+ require 'cgi'
5
+ require 'uri'
6
+ require 'auth'
7
+
8
+ module Auth
9
+ class Server < Sinatra::Base
10
+ dir = File.dirname(File.expand_path(__FILE__))
11
+
12
+ set :views, "#{dir}/server/views"
13
+ set :public, "#{dir}/server/public"
14
+ set :static, true
15
+
16
+ helpers do
17
+ include Rack::Utils
18
+ alias_method :h, :escape_html
19
+
20
+ def cgi_escape(text)
21
+ URI.escape(CGI.escape(text.to_s), '.').gsub(' ','+')
22
+ end
23
+
24
+ def query_string(parameters, escape = true)
25
+ if escape
26
+ parameters.map{|key,val| val ? "#{cgi_escape(key)}=#{cgi_escape(val)}" : nil }.compact.join('&')
27
+ else
28
+ parameters.map{|key,val| val ? "#{key}=#{val}" : nil }.compact.join('&')
29
+ end
30
+ end
31
+
32
+ def merge_uri_with_query_parameters(uri, parameters = {})
33
+ parameters = query_string(parameters)
34
+ if uri.to_s =~ /\?/
35
+ parameters = "&#{parameters}"
36
+ else
37
+ parameters = "?#{parameters}"
38
+ end
39
+ URI.escape(uri.to_s) + parameters.to_s
40
+ end
41
+
42
+ def merge_uri_with_fragment_parameters(uri, parameters = {})
43
+ parameters = query_string(parameters)
44
+ parameters = "##{parameters}"
45
+ URI.escape(uri.to_s) + parameters.to_s
46
+ end
47
+
48
+ def merge_uri_based_on_response_type(uri, parameters = {})
49
+ case params[:response_type]
50
+ when 'code', nil
51
+ merge_uri_with_query_parameters(uri, parameters)
52
+ when 'token', 'code_and_token'
53
+ merge_uri_with_fragment_parameters(uri, parameters)
54
+ end
55
+ end
56
+
57
+ def sentry
58
+ @sentry ||= request.env['warden'] || request.env['rack.auth'] || Sentry.new(request)
59
+ end
60
+
61
+ def require_client_identification!
62
+ @client = Auth.authenticate_client(params[:client_id])
63
+ halt(403, 'Invalid client identifier') unless @client
64
+ end
65
+
66
+ def require_client_authentication!
67
+ @client = Auth.authenticate_client(params[:client_id], params[:client_secret])
68
+ halt(403, 'Invalid client identifier or client secret') unless @client
69
+ end
70
+
71
+ def validate_redirect_uri!
72
+ params[:redirect_uri] ||= @client.redirect_uri
73
+ if URI(params[:redirect_uri]).host.downcase != URI(@client.redirect_uri).host.downcase
74
+ halt(400, 'Invalid redirect URI')
75
+ end
76
+ rescue URI::InvalidURIError
77
+ halt(400, 'Invalid redirect URI')
78
+ end
79
+ end
80
+
81
+ error AuthException do
82
+ headers['Content-Type'] = 'application/json;charset=utf-8'
83
+ [400, {
84
+ :error => {
85
+ :type => 'OAuthException',
86
+ :message => request.env['sinatra.error'].message
87
+ }
88
+ }.to_json]
89
+ end
90
+
91
+ error UnsupportedResponseType do
92
+ redirect_uri = merge_uri_based_on_response_type(
93
+ params[:redirect_uri],
94
+ :error => 'unsupported_response_type',
95
+ :error_description => request.env['sinatra.error'].message,
96
+ :state => params[:state])
97
+ redirect redirect_uri
98
+ end
99
+
100
+ before do
101
+ headers['Cache-Control'] = 'no-store'
102
+ end
103
+
104
+ ['', '/authorize'].each do |action|
105
+ get action do
106
+ require_client_identification!
107
+ validate_redirect_uri!
108
+ sentry.authenticate!
109
+ unless ['code', 'token', 'code_and_token', nil].include?(params[:response_type])
110
+ raise UnsupportedResponseType,
111
+ 'The authorization server does not support obtaining an ' +
112
+ 'authorization code using this method.'
113
+ end
114
+ erb(:authorize)
115
+ end
116
+ end
117
+
118
+ ['', '/authorize'].each do |action|
119
+ post action do
120
+ require_client_identification!
121
+ validate_redirect_uri!
122
+ sentry.authenticate!
123
+ case params[:response_type]
124
+ when 'code', nil
125
+ authorization_code = Auth.issue_code(sentry.user.id,
126
+ params[:client_id],
127
+ params[:redirect_uri],
128
+ params[:scope])
129
+ redirect_uri = merge_uri_with_query_parameters(
130
+ params[:redirect_uri],
131
+ :code => authorization_code,
132
+ :state => params[:state])
133
+ redirect redirect_uri
134
+ when 'token'
135
+ ttl = ENV['AUTH_TOKEN_TTL'].to_i
136
+ access_token = Auth.issue_token(sentry.user.id, params[:scope], ttl)
137
+ redirect_uri = merge_uri_with_fragment_parameters(
138
+ params[:redirect_uri],
139
+ :access_token => access_token,
140
+ :token_type => 'bearer',
141
+ :expires_in => ttl,
142
+ :expires => ttl, # Facebook compatibility
143
+ :scope => params[:scope],
144
+ :state => params[:state])
145
+ redirect redirect_uri
146
+ when 'code_and_token'
147
+ ttl = ENV['AUTH_TOKEN_TTL'].to_i
148
+ authorization_code = Auth.issue_code(sentry.user.id,
149
+ params[:client_id],
150
+ params[:redirect_uri],
151
+ params[:scope])
152
+ access_token = Auth.issue_token(sentry.user.id, params[:scope], ttl)
153
+ redirect_uri = merge_uri_with_fragment_parameters(
154
+ params[:redirect_uri],
155
+ :code => authorization_code,
156
+ :access_token => access_token,
157
+ :token_type => 'bearer',
158
+ :expires_in => ttl,
159
+ :expires => ttl, # Facebook compatibility
160
+ :scope => params[:scope],
161
+ :state => params[:state])
162
+ redirect redirect_uri
163
+ else
164
+ raise UnsupportedResponseType,
165
+ 'The authorization server does not support obtaining an ' +
166
+ 'authorization code using this method.'
167
+ end
168
+ end
169
+ end
170
+
171
+ ['/token', '/access_token'].each do |action|
172
+ post action do
173
+ require_client_authentication!
174
+ validate_redirect_uri!
175
+ case params[:grant_type]
176
+ when 'authorization_code', nil
177
+ account_id, scopes = Auth.validate_code(
178
+ params[:code], params[:client_id], params[:redirect_uri])
179
+ if account_id
180
+ ttl = ENV['AUTH_TOKEN_TTL'].to_i
181
+ access_token = Auth.issue_token(account_id, scopes, ttl)
182
+ @token = {
183
+ :access_token => access_token,
184
+ :token_type => 'bearer',
185
+ :expires_in => ttl,
186
+ :expires => ttl, # Facebook compatibility
187
+ :scope => scopes
188
+ }
189
+ else
190
+ raise AuthException, 'Invalid authorization code'
191
+ end
192
+ when 'password'
193
+ sentry.authenticate!
194
+ ttl = ENV['AUTH_TOKEN_TTL'].to_i
195
+ access_token = Auth.issue_token(sentry.user.id, params[:scope], ttl)
196
+ @token = {
197
+ :access_token => access_token,
198
+ :token_type => 'bearer',
199
+ :expires_in => ttl,
200
+ :expires => ttl, # Facebook compatibility
201
+ :scope => params[:scope]
202
+ }
203
+ when 'refresh_token'
204
+ raise AuthException, 'Unsupported grant type'
205
+ when 'client_credentials'
206
+ access_token = Auth.issue_token("client:#{@client.id}")
207
+ @token = {
208
+ :access_token => access_token,
209
+ :token_type => 'client'
210
+ }
211
+ else
212
+ raise AuthException, 'Unsupported grant type'
213
+ end
214
+ if request.accept.include?('application/json')
215
+ headers['Content-Type'] = 'application/json;charset=utf-8'
216
+ [200, @token.to_json]
217
+ else
218
+ headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8'
219
+ [200, query_string(@token)]
220
+ end
221
+ end
222
+ end
223
+
224
+ get '/validate' do
225
+ require_client_authentication!
226
+ headers['Content-Type'] = 'text/plain;charset=utf-8'
227
+ if account_id = Auth.validate_token(params[:access_token], params[:scope])
228
+ [200, account_id]
229
+ else
230
+ [403, 'Forbidden']
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,3 @@
1
+ module Auth
2
+ Version = VERSION = '0.0.1'
3
+ end
data/lib/auth.rb ADDED
@@ -0,0 +1,190 @@
1
+ require 'redis'
2
+ require 'redis/namespace'
3
+ require 'json'
4
+
5
+ ENV['AUTH_HASH_ALGORITHM'] ||= 'sha256'
6
+ ENV['AUTH_TOKEN_TTL'] ||= '3600'
7
+
8
+ require 'auth/version'
9
+ require 'auth/exceptions'
10
+ require 'auth/helpers'
11
+ require 'auth/client'
12
+ require 'auth/sentry'
13
+
14
+ module Auth
15
+ include Helpers
16
+ extend self
17
+
18
+ # Accepts:
19
+ # 1. A 'hostname:port' string
20
+ # 2. A 'hostname:port:db' string (to select the Redis db)
21
+ # 3. A 'hostname:port/namespace' string (to set the Redis namespace)
22
+ # 4. A redis URL string 'redis://host:port'
23
+ # 5. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`,
24
+ # or `Redis::Namespace`.
25
+ def redis=(server)
26
+ if server.respond_to? :split
27
+ if server =~ /redis\:\/\//
28
+ redis = Redis.connect(:url => server)
29
+ else
30
+ server, namespace = server.split('/', 2)
31
+ host, port, db = server.split(':')
32
+ redis = Redis.new(:host => host, :port => port,
33
+ :thread_safe => true, :db => db)
34
+ end
35
+ namespace ||= :auth
36
+ @redis = Redis::Namespace.new(namespace, :redis => redis)
37
+ elsif server.respond_to? :namespace=
38
+ @redis = server
39
+ else
40
+ @redis = Redis::Namespace.new(:auth, :redis => server)
41
+ end
42
+ end
43
+
44
+ # Returns the current Redis connection. If none has been created, will
45
+ # create a new one.
46
+ def redis
47
+ return @redis if @redis
48
+ self.redis = 'localhost:6379'
49
+ self.redis
50
+ end
51
+
52
+ #
53
+ # Accounts
54
+ #
55
+
56
+ def register_account(username, password)
57
+ raise if username.nil? || username == ''
58
+ raise if password.nil? || password == ''
59
+ unless redis.exists("account:#{username}")
60
+ hash = ENV['AUTH_HASH_ALGORITHM']
61
+ salt = generate_secret
62
+ crypted_password = encrypt_password(password, salt, hash)
63
+ redis.hset("account:#{username}", 'crypted_password', crypted_password)
64
+ redis.hset("account:#{username}", 'password_hash', hash)
65
+ redis.hset("account:#{username}", 'password_salt', salt)
66
+ return true
67
+ else
68
+ return false
69
+ end
70
+ end
71
+
72
+ def authenticate_account(username, password)
73
+ account = redis.hgetall("account:#{username}")
74
+ if account['crypted_password']
75
+ crypted_password = encrypt_password(password,
76
+ account['password_salt'],
77
+ account['password_hash'])
78
+ if crypted_password == account['crypted_password']
79
+ return true
80
+ else
81
+ return false
82
+ end
83
+ else
84
+ return false
85
+ end
86
+ end
87
+
88
+ def change_password(username, old_password, new_password)
89
+ if authenticate_account(username, old_password)
90
+ hash = ENV['AUTH_HASH_ALGORITHM']
91
+ salt = generate_secret
92
+ crypted_password = encrypt_password(new_password, salt, hash)
93
+ redis.hset("account:#{username}", 'crypted_password', crypted_password)
94
+ redis.hset("account:#{username}", 'password_hash', hash)
95
+ redis.hset("account:#{username}", 'password_salt', salt)
96
+ end
97
+ end
98
+
99
+ def remove_account(username)
100
+ redis.del("account:#{username}")
101
+ end
102
+
103
+ #
104
+ # Clients
105
+ #
106
+
107
+ def register_client(client_id, name, redirect_uri)
108
+ raise if client_id.nil? || client_id == ''
109
+ raise if name.nil? || name == ''
110
+ raise if redirect_uri.nil? || redirect_uri == ''
111
+ unless redis.exists("client:#{client_id}")
112
+ secret = generate_secret
113
+ client = { :id => client_id,
114
+ :secret => secret,
115
+ :name => name,
116
+ :redirect_uri => redirect_uri }
117
+ client.each do |key,val|
118
+ redis.hset("client:#{client_id}", key, val)
119
+ end
120
+ return Client.new(client)
121
+ end
122
+ end
123
+
124
+ def authenticate_client(client_id, client_secret = nil)
125
+ client = redis.hgetall("client:#{client_id}")
126
+ if client_secret
127
+ return client['id'] && client['secret'] == client_secret ? Client.new(client) : false
128
+ else
129
+ return client['id'] ? Client.new(client) : false
130
+ end
131
+ end
132
+
133
+ def remove_client(client_id)
134
+ redis.del("client:#{client_id}")
135
+ end
136
+
137
+ #
138
+ # Authorization codes
139
+ #
140
+
141
+ def issue_code(account_id, client_id, redirect_uri, scopes = nil)
142
+ code = generate_secret
143
+ redis.set("code:#{client_id}:#{redirect_uri}:#{code}:account", account_id)
144
+ decode_scopes(scopes).each do |scope|
145
+ redis.sadd("code:#{client_id}:#{redirect_uri}:#{code}:scopes", scope)
146
+ end
147
+ redis.expire("code:#{client_id}:#{redirect_uri}:#{code}:account", 3600)
148
+ redis.expire("code:#{client_id}:#{redirect_uri}:#{code}:scopes", 3600)
149
+ return code
150
+ end
151
+
152
+ def validate_code(code, client_id, redirect_uri)
153
+ account_id = redis.get("code:#{client_id}:#{redirect_uri}:#{code}:account")
154
+ scopes = redis.smembers("code:#{client_id}:#{redirect_uri}:#{code}:scopes")
155
+ if account_id
156
+ return account_id, encode_scopes(scopes)
157
+ else
158
+ return false
159
+ end
160
+ end
161
+
162
+ #
163
+ # Access tokens
164
+ #
165
+
166
+ def issue_token(account_id, scopes = nil, ttl = nil)
167
+ token = generate_secret
168
+ redis.set("token:#{token}:account", account_id)
169
+ decode_scopes(scopes).each do |scope|
170
+ redis.sadd("token:#{token}:scopes", scope)
171
+ end
172
+ if ttl
173
+ redis.expire("token:#{token}:account", ttl)
174
+ redis.expire("token:#{token}:scopes", ttl)
175
+ end
176
+ return token
177
+ end
178
+
179
+ def validate_token(token, scopes = nil)
180
+ account_id = redis.get("token:#{token}:account")
181
+ if account_id &&
182
+ decode_scopes(scopes).all? {|scope|
183
+ redis.sismember("token:#{token}:scopes", scope) }
184
+ return account_id
185
+ else
186
+ return false
187
+ end
188
+ end
189
+
190
+ end
data/test/auth_test.rb ADDED
@@ -0,0 +1,140 @@
1
+ require File.expand_path('test/test_helper')
2
+
3
+ class AuthTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ Auth.redis.flushall
7
+ end
8
+
9
+ def test_can_set_a_namespace_through_a_url_like_string
10
+ assert Auth.redis
11
+ assert_equal :auth, Auth.redis.namespace
12
+ Auth.redis = 'localhost:9736/namespace'
13
+ assert_equal 'namespace', Auth.redis.namespace
14
+ end
15
+
16
+ def test_can_register_an_account
17
+ assert Auth.register_account('test', 'test')
18
+ end
19
+
20
+ def test_can_only_register_an_account_once
21
+ assert_equal true, Auth.register_account('test', 'test')
22
+ assert_equal false, Auth.register_account('test', 'test')
23
+ end
24
+
25
+ def test_can_authenticate_account
26
+ Auth.register_account('test', 'test')
27
+ assert_equal true, Auth.authenticate_account('test', 'test')
28
+ assert_equal false, Auth.authenticate_account('test', 'wrong')
29
+ assert_equal false, Auth.authenticate_account('wrong', 'wrong')
30
+ assert_equal false, Auth.authenticate_account('wrong', 'test')
31
+ end
32
+
33
+ def test_can_change_password_for_an_account
34
+ Auth.register_account('test', 'test')
35
+ Auth.change_password('test', 'test', '123456')
36
+ assert_equal false, Auth.authenticate_account('test', 'test')
37
+ assert_equal true, Auth.authenticate_account('test', '123456')
38
+ end
39
+
40
+ def test_can_remove_account
41
+ Auth.register_account('test', 'test')
42
+ Auth.remove_account('test')
43
+ assert_equal false, Auth.authenticate_account('test', 'test')
44
+ end
45
+
46
+ def test_can_register_a_client
47
+ client = Auth.register_client('test-client', 'Test client', 'http://example.org/')
48
+ assert_equal 'test-client', client.id
49
+ assert_equal 'Test client', client.name
50
+ assert_equal 'http://example.org/', client.redirect_uri
51
+ assert client.secret
52
+ end
53
+
54
+ def test_can_authenticate_a_client
55
+ client = Auth.register_client('test-client', 'Test client', 'http://example.org/')
56
+ client = Auth.authenticate_client('test-client', client.secret)
57
+ assert_equal 'test-client', client.id
58
+ assert_equal 'Test client', client.name
59
+ assert_equal 'http://example.org/', client.redirect_uri
60
+ assert client.secret
61
+ assert_equal false, Auth.authenticate_client('test-client', 'wrong')
62
+ assert_equal false, Auth.authenticate_client('wrong', 'wrong')
63
+ assert_equal false, Auth.authenticate_client('wrong', client.secret)
64
+ assert_equal false, Auth.authenticate_client('wrong')
65
+ end
66
+
67
+ def test_can_authenticate_a_client_without_a_client_secret
68
+ client = Auth.register_client('test-client', 'Test client', 'http://example.org/')
69
+ client = Auth.authenticate_client('test-client')
70
+ assert_equal 'test-client', client.id
71
+ assert_equal 'Test client', client.name
72
+ assert_equal 'http://example.org/', client.redirect_uri
73
+ assert client.secret
74
+ end
75
+
76
+ def test_can_remove_client
77
+ Auth.register_client('test-client', 'Test client', 'http://example.org/')
78
+ Auth.remove_client('test-client')
79
+ assert_equal false, Auth.authenticate_client('test-client')
80
+ end
81
+
82
+ def test_can_issue_a_token_for_an_account
83
+ assert Auth.issue_token('test-account')
84
+ end
85
+
86
+ def test_can_validate_a_token_and_return_the_associated_account_id
87
+ token = Auth.issue_token('test-account')
88
+ assert_equal 'test-account', Auth.validate_token(token)
89
+ assert_equal false, Auth.validate_token('gibberish')
90
+ end
91
+
92
+ def test_can_issue_a_token_for_a_specified_set_of_scopes
93
+ assert Auth.issue_token('test-account', 'read write offline')
94
+ end
95
+
96
+ def test_can_validate_a_token_with_a_specified_set_of_scopes
97
+ token = Auth.issue_token('test-account', 'read write offline')
98
+ assert_equal 'test-account', Auth.validate_token(token)
99
+ assert_equal 'test-account', Auth.validate_token(token, 'read')
100
+ assert_equal 'test-account', Auth.validate_token(token, 'write offline')
101
+ assert_equal 'test-account', Auth.validate_token(token, 'offline read write')
102
+ assert_equal false, Auth.validate_token('gibberish', 'read')
103
+ assert_equal false, Auth.validate_token(token, 'delete')
104
+ assert_equal false, Auth.validate_token(token, 'read delete')
105
+ end
106
+
107
+ def test_can_issue_a_time_limited_token
108
+ assert Auth.issue_token('test-account', nil, 3600)
109
+ end
110
+
111
+ def test_can_issue_a_refresh_token
112
+ flunk
113
+ end
114
+
115
+ def test_can_redeem_a_refresh_token
116
+ flunk
117
+ end
118
+
119
+ def test_can_issue_an_authorization_code
120
+ assert Auth.issue_code('test-account', 'test-client', 'https://example.com/callback')
121
+ end
122
+
123
+ def test_can_validate_an_authentication_code
124
+ code = Auth.issue_code('test-account', 'test-client', 'https://example.com/callback')
125
+ assert_equal ['test-account', ''], Auth.validate_code(code, 'test-client', 'https://example.com/callback')
126
+ assert_equal false, Auth.validate_code(code, 'wrong-client', 'https://example.com/callback')
127
+ assert_equal false, Auth.validate_code(code, 'test-client', 'https://example.com/wrong-callback')
128
+ end
129
+
130
+ def test_can_issue_an_authorization_code_for_a_specified_set_of_scopes
131
+ assert Auth.issue_code('test-account', 'test-client', 'https://example.com/callback', 'read write offline')
132
+ end
133
+
134
+ def test_can_validate_an_authentication_code_with_a_specified_set_of_scopes
135
+ code = Auth.issue_code('test-account', 'test-client', 'https://example.com/callback', 'read write offline')
136
+ account_id, scopes = Auth.validate_code(code, 'test-client', 'https://example.com/callback')
137
+ assert_equal 'test-account', account_id
138
+ assert_equal 'offline read write', scopes
139
+ end
140
+ end
@@ -0,0 +1,115 @@
1
+ # Redis configuration file example
2
+
3
+ # By default Redis does not run as a daemon. Use 'yes' if you need it.
4
+ # Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
5
+ daemonize yes
6
+
7
+ # When run as a daemon, Redis write a pid file in /var/run/redis.pid by default.
8
+ # You can specify a custom pid file location here.
9
+ pidfile ./test/redis-test.pid
10
+
11
+ # Accept connections on the specified port, default is 6379
12
+ port 9736
13
+
14
+ # If you want you can bind a single interface, if the bind option is not
15
+ # specified all the interfaces will listen for connections.
16
+ #
17
+ # bind 127.0.0.1
18
+
19
+ # Close the connection after a client is idle for N seconds (0 to disable)
20
+ timeout 300
21
+
22
+ # Save the DB on disk:
23
+ #
24
+ # save <seconds> <changes>
25
+ #
26
+ # Will save the DB if both the given number of seconds and the given
27
+ # number of write operations against the DB occurred.
28
+ #
29
+ # In the example below the behaviour will be to save:
30
+ # after 900 sec (15 min) if at least 1 key changed
31
+ # after 300 sec (5 min) if at least 10 keys changed
32
+ # after 60 sec if at least 10000 keys changed
33
+ save 900 1
34
+ save 300 10
35
+ save 60 10000
36
+
37
+ # The filename where to dump the DB
38
+ dbfilename dump.rdb
39
+
40
+ # For default save/load DB in/from the working directory
41
+ # Note that you must specify a directory not a file name.
42
+ dir ./test/
43
+
44
+ # Set server verbosity to 'debug'
45
+ # it can be one of:
46
+ # debug (a lot of information, useful for development/testing)
47
+ # notice (moderately verbose, what you want in production probably)
48
+ # warning (only very important / critical messages are logged)
49
+ loglevel debug
50
+
51
+ # Specify the log file name. Also 'stdout' can be used to force
52
+ # the demon to log on the standard output. Note that if you use standard
53
+ # output for logging but daemonize, logs will be sent to /dev/null
54
+ logfile stdout
55
+
56
+ # Set the number of databases. The default database is DB 0, you can select
57
+ # a different one on a per-connection basis using SELECT <dbid> where
58
+ # dbid is a number between 0 and 'databases'-1
59
+ databases 16
60
+
61
+ ################################# REPLICATION #################################
62
+
63
+ # Master-Slave replication. Use slaveof to make a Redis instance a copy of
64
+ # another Redis server. Note that the configuration is local to the slave
65
+ # so for example it is possible to configure the slave to save the DB with a
66
+ # different interval, or to listen to another port, and so on.
67
+
68
+ # slaveof <masterip> <masterport>
69
+
70
+ ################################## SECURITY ###################################
71
+
72
+ # Require clients to issue AUTH <PASSWORD> before processing any other
73
+ # commands. This might be useful in environments in which you do not trust
74
+ # others with access to the host running redis-server.
75
+ #
76
+ # This should stay commented out for backward compatibility and because most
77
+ # people do not need auth (e.g. they run their own servers).
78
+
79
+ # requirepass foobared
80
+
81
+ ################################### LIMITS ####################################
82
+
83
+ # Set the max number of connected clients at the same time. By default there
84
+ # is no limit, and it's up to the number of file descriptors the Redis process
85
+ # is able to open. The special value '0' means no limts.
86
+ # Once the limit is reached Redis will close all the new connections sending
87
+ # an error 'max number of clients reached'.
88
+
89
+ # maxclients 128
90
+
91
+ # Don't use more memory than the specified amount of bytes.
92
+ # When the memory limit is reached Redis will try to remove keys with an
93
+ # EXPIRE set. It will try to start freeing keys that are going to expire
94
+ # in little time and preserve keys with a longer time to live.
95
+ # Redis will also try to remove objects from free lists if possible.
96
+ #
97
+ # If all this fails, Redis will start to reply with errors to commands
98
+ # that will use more memory, like SET, LPUSH, and so on, and will continue
99
+ # to reply to most read-only commands like GET.
100
+ #
101
+ # WARNING: maxmemory can be a good idea mainly if you want to use Redis as a
102
+ # 'state' server or cache, not as a real DB. When Redis is used as a real
103
+ # database the memory usage will grow over the weeks, it will be obvious if
104
+ # it is going to use too much memory in the long run, and you'll have the time
105
+ # to upgrade. With maxmemory after the limit is reached you'll start to get
106
+ # errors for write operations, and this may even lead to DB inconsistency.
107
+
108
+ # maxmemory <bytes>
109
+
110
+ ############################### ADVANCED CONFIG ###############################
111
+
112
+ # Glue small output buffers together in order to send small replies in a
113
+ # single TCP packet. Uses a bit more CPU but most of the times it is a win
114
+ # in terms of number of queries per second. Use 'yes' if unsure.
115
+ glueoutputbuf yes
@@ -0,0 +1,178 @@
1
+ require File.expand_path('test/test_helper')
2
+ require 'auth/server'
3
+
4
+ class ServerTest < Test::Unit::TestCase
5
+ include Rack::Test::Methods
6
+
7
+ def app
8
+ Auth::Server.new
9
+ end
10
+
11
+ def setup
12
+ Auth.redis.flushall
13
+ Auth.register_account('test', 'test')
14
+ @client = Auth.register_client('test-client', 'Test', 'https://example.com/callback')
15
+ @authorization_code = Auth.issue_code('test-account', @client.id, @client.redirect_uri, 'read write')
16
+ end
17
+
18
+ def test_should_not_allow_invalid_redirect_uri
19
+ get '/authorize', :client_id => @client.id, :redirect_uri => 'invalid uri'
20
+ assert_equal 400, last_response.status
21
+ get '/authorize', :client_id => @client.id, :redirect_uri => 'https://wrong.com/callback'
22
+ assert_equal 400, last_response.status
23
+ get '/authorize', :client_id => @client.id, :redirect_uri => 'https://wrong.example.com/callback'
24
+ assert_equal 400, last_response.status
25
+ end
26
+
27
+ def test_obtaining_end_user_authorization
28
+ get '/authorize',
29
+ :response_type => 'code',
30
+ :client_id => @client.id,
31
+ :redirect_uri => @client.redirect_uri,
32
+ :scope => 'read write',
33
+ :state => 'opaque',
34
+ :username => 'test',
35
+ :password => 'test'
36
+ assert_equal 200, last_response.status
37
+ assert_equal 'text/html;charset=utf-8', last_response.headers['Content-Type']
38
+ assert_equal 'no-store', last_response.headers['Cache-Control']
39
+ assert_match 'code', last_response.body
40
+ assert_match @client.id.to_s, last_response.body
41
+ assert_match 'https%3A%2F%2Fexample%2Ecom%2Fcallback', last_response.body
42
+ assert_match 'read+write', last_response.body
43
+ assert_match 'opaque', last_response.body
44
+ end
45
+
46
+ def test_request_for_authorization_code
47
+ post '/authorize',
48
+ :response_type => 'code',
49
+ :client_id => @client.id,
50
+ :redirect_uri => @client.redirect_uri,
51
+ :scope => 'read write',
52
+ :state => 'opaque',
53
+ :username => 'test',
54
+ :password => 'test'
55
+ assert_equal 302, last_response.status
56
+ assert_equal 'no-store', last_response.headers['Cache-Control']
57
+ location_uri = URI(last_response.headers['Location'])
58
+ assert_equal 'https', location_uri.scheme
59
+ assert_equal 'example.com', location_uri.host
60
+ assert_equal '/callback', location_uri.path
61
+ assert_match /code=[^&]+/, location_uri.query
62
+ assert_match /state=opaque/, location_uri.query
63
+ end
64
+
65
+ def test_request_for_access_token_using_authorization_code
66
+ post '/access_token', {
67
+ :grant_type => 'authorization_code',
68
+ :client_id => @client.id,
69
+ :client_secret => @client.secret,
70
+ :redirect_uri => @client.redirect_uri,
71
+ :code => @authorization_code
72
+ }, 'HTTP_ACCEPT' => 'application/json'
73
+ assert_equal 200, last_response.status
74
+ assert_equal 'application/json;charset=utf-8', last_response.headers['Content-Type']
75
+ assert_equal 'no-store', last_response.headers['Cache-Control']
76
+ token = JSON.parse(last_response.body)
77
+ assert token['access_token']
78
+ assert_equal 'bearer', token['token_type']
79
+ assert_equal 3600, token['expires_in']
80
+ assert_equal 'read write', token['scope']
81
+ end
82
+
83
+ def test_request_for_access_token_using_password
84
+ post '/access_token', {
85
+ :grant_type => 'password',
86
+ :client_id => @client.id,
87
+ :client_secret => @client.secret,
88
+ :redirect_uri => @client.redirect_uri,
89
+ :scope => 'read write',
90
+ :username => 'test',
91
+ :password => 'test'
92
+ }, 'HTTP_ACCEPT' => 'application/json'
93
+ assert_equal 200, last_response.status
94
+ assert_equal 'application/json;charset=utf-8', last_response.headers['Content-Type']
95
+ assert_equal 'no-store', last_response.headers['Cache-Control']
96
+ token = JSON.parse(last_response.body)
97
+ assert token['access_token']
98
+ assert_equal 'bearer', token['token_type']
99
+ assert_equal 3600, token['expires_in']
100
+ assert_equal 'read write', token['scope']
101
+ end
102
+
103
+ def test_request_for_access_token_using_refresh_token
104
+ post '/access_token', {
105
+ :grant_type => 'refresh_token',
106
+ :client_id => @client.id,
107
+ :client_secret => @client.secret,
108
+ :redirect_uri => @client.redirect_uri,
109
+ :refresh_token => '?'
110
+ }, 'HTTP_ACCEPT' => 'application/json'
111
+ assert_equal 200, last_response.status
112
+ assert_equal 'application/json;charset=utf-8', last_response.headers['Content-Type']
113
+ assert_equal 'no-store', last_response.headers['Cache-Control']
114
+ token = JSON.parse(last_response.body)
115
+ assert token['access_token']
116
+ assert_equal 'bearer', token['token_type']
117
+ assert_equal 3600*24, token['expires_in']
118
+ assert_equal 'read write', token['scope']
119
+ end
120
+
121
+ def test_request_for_access_token_using_client_credentials
122
+ post '/access_token', {
123
+ :grant_type => 'client_credentials',
124
+ :client_id => @client.id,
125
+ :client_secret => @client.secret,
126
+ :redirect_uri => @client.redirect_uri
127
+ }, 'HTTP_ACCEPT' => 'application/json'
128
+ assert_equal 200, last_response.status
129
+ assert_equal 'application/json;charset=utf-8', last_response.headers['Content-Type']
130
+ assert_equal 'no-store', last_response.headers['Cache-Control']
131
+ token = JSON.parse(last_response.body)
132
+ assert token['access_token']
133
+ assert_equal 'client', token['token_type']
134
+ end
135
+
136
+ # def test_request_for_both_code_and_token
137
+ # Warden.on_next_request do |warden|
138
+ # post '/test/authorize',
139
+ # :response_type => 'code_and_token',
140
+ # :client_id => 'test-client',
141
+ # :redirect_uri => 'https://example.com/callback',
142
+ # :scope => 'read write',
143
+ # :state => 'opaque'
144
+ # assert_equal 302, last_response.status
145
+ # location_uri = URI(last_response.headers['Location'])
146
+ # assert_equal 'https', location_uri.scheme
147
+ # assert_equal 'example.com', location_uri.host
148
+ # assert_equal '/callback', location_uri.path
149
+ # assert_match /code=/, location_uri.query
150
+ # location_uri_fragment_parts = location_uri.fragment.split('&')
151
+ # assert_equal true, location_uri_fragment_parts.include?('code=')
152
+ # assert_equal true, location_uri_fragment_parts.include?('access_token=')
153
+ # assert_equal true, location_uri_fragment_parts.include?('token_type=')
154
+ # assert_equal true, location_uri_fragment_parts.include?('expires_in=')
155
+ # assert_equal true, location_uri_fragment_parts.include?('scope=')
156
+ # assert_equal true, location_uri_fragment_parts.include?('state=')
157
+ # end
158
+ # end
159
+
160
+ # def test_validate_access_token
161
+ # basic_authorize @client.username, @client.password
162
+ # get '/test/validate', :token => 'xxx', :client_id => 'test', :scope => 'read write'
163
+ # assert_equal 200, last_response.status
164
+ # end
165
+ #
166
+ # def test_validate_expired_access_token
167
+ # basic_authorize @client.username, @client.password
168
+ # get '/test/validate', :token => 'xxx', :client_id => 'test', :scope => 'read write'
169
+ # assert_equal 403, last_response.status
170
+ # end
171
+ #
172
+ # def test_validate_invalid_access_token
173
+ # basic_authorize @client.username, @client.password
174
+ # get '/test/validate', :token => 'invalid', :client_id => 'test', :scope => 'read write'
175
+ # assert_equal 403, last_response.status
176
+ # end
177
+
178
+ end
@@ -0,0 +1,44 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+
3
+ dir = File.dirname(File.expand_path(__FILE__))
4
+ $LOAD_PATH.unshift dir + '/../lib'
5
+
6
+ require 'test/unit'
7
+ require 'rack/test'
8
+ require 'auth'
9
+
10
+ #
11
+ # make sure we can run redis
12
+ #
13
+
14
+ if !system("which redis-server")
15
+ puts '', "** can't find `redis-server` in your path"
16
+ puts "** try running `sudo rake install`"
17
+ abort ''
18
+ end
19
+
20
+
21
+ #
22
+ # start our own redis when the tests start,
23
+ # kill it when they end
24
+ #
25
+
26
+ at_exit do
27
+ next if $!
28
+
29
+ if defined?(MiniTest)
30
+ exit_code = MiniTest::Unit.new.run(ARGV)
31
+ else
32
+ exit_code = Test::Unit::AutoRunner.run
33
+ end
34
+
35
+ pid = `ps -A -o pid,command | grep [r]edis-test`.split(" ")[0]
36
+ puts "Killing test redis server..."
37
+ `rm -f #{dir}/dump.rdb`
38
+ Process.kill("KILL", pid.to_i)
39
+ exit exit_code
40
+ end
41
+
42
+ puts "Starting redis for testing at localhost:9736..."
43
+ `redis-server #{dir}/redis-test.conf`
44
+ Auth.redis = 'localhost:9736'
metadata ADDED
@@ -0,0 +1,154 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: auth
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Niklas Holmgren
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-04-29 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rack-contrib
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 1
30
+ - 0
31
+ - 0
32
+ version: 1.0.0
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: sinatra
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ segments:
44
+ - 1
45
+ - 0
46
+ - 0
47
+ version: 1.0.0
48
+ type: :runtime
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ name: redis
52
+ prerelease: false
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ~>
57
+ - !ruby/object:Gem::Version
58
+ segments:
59
+ - 2
60
+ - 0
61
+ - 0
62
+ version: 2.0.0
63
+ type: :runtime
64
+ version_requirements: *id003
65
+ - !ruby/object:Gem::Dependency
66
+ name: redis-namespace
67
+ prerelease: false
68
+ requirement: &id004 !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ~>
72
+ - !ruby/object:Gem::Version
73
+ segments:
74
+ - 0
75
+ - 8
76
+ - 0
77
+ version: 0.8.0
78
+ type: :runtime
79
+ version_requirements: *id004
80
+ - !ruby/object:Gem::Dependency
81
+ name: rack-test
82
+ prerelease: false
83
+ requirement: &id005 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ~>
87
+ - !ruby/object:Gem::Version
88
+ segments:
89
+ - 0
90
+ - 5
91
+ - 6
92
+ version: 0.5.6
93
+ type: :development
94
+ version_requirements: *id005
95
+ 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.
96
+ email: niklas@sutajio.se
97
+ executables: []
98
+
99
+ extensions: []
100
+
101
+ extra_rdoc_files:
102
+ - LICENSE
103
+ - README.md
104
+ files:
105
+ - README.md
106
+ - Rakefile
107
+ - LICENSE
108
+ - CHANGELOG
109
+ - lib/auth/client.rb
110
+ - lib/auth/exceptions.rb
111
+ - lib/auth/helpers.rb
112
+ - lib/auth/sentry.rb
113
+ - lib/auth/server/views/authorize.erb
114
+ - lib/auth/server.rb
115
+ - lib/auth/version.rb
116
+ - lib/auth.rb
117
+ - test/auth_test.rb
118
+ - test/redis-test.conf
119
+ - test/server_test.rb
120
+ - test/test_helper.rb
121
+ has_rdoc: true
122
+ homepage: http://github.com/sutajio/auth/
123
+ licenses: []
124
+
125
+ post_install_message:
126
+ rdoc_options:
127
+ - --charset=UTF-8
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ none: false
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ segments:
136
+ - 0
137
+ version: "0"
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ none: false
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ segments:
144
+ - 0
145
+ version: "0"
146
+ requirements: []
147
+
148
+ rubyforge_project:
149
+ rubygems_version: 1.3.7
150
+ signing_key:
151
+ specification_version: 3
152
+ summary: Auth is a Redis-backed high performance OAuth2 authorization server.
153
+ test_files: []
154
+