auth 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+