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 +3 -0
- data/LICENSE +20 -0
- data/README.md +6 -0
- data/Rakefile +11 -0
- data/lib/auth/client.rb +11 -0
- data/lib/auth/exceptions.rb +8 -0
- data/lib/auth/helpers.rb +54 -0
- data/lib/auth/sentry.rb +24 -0
- data/lib/auth/server/views/authorize.erb +26 -0
- data/lib/auth/server.rb +234 -0
- data/lib/auth/version.rb +3 -0
- data/lib/auth.rb +190 -0
- data/test/auth_test.rb +140 -0
- data/test/redis-test.conf +115 -0
- data/test/server_test.rb +178 -0
- data/test/test_helper.rb +44 -0
- metadata +154 -0
data/CHANGELOG
ADDED
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
data/Rakefile
ADDED
data/lib/auth/client.rb
ADDED
@@ -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
|
data/lib/auth/helpers.rb
ADDED
@@ -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
|
data/lib/auth/sentry.rb
ADDED
@@ -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>
|
data/lib/auth/server.rb
ADDED
@@ -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
|
data/lib/auth/version.rb
ADDED
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
|
data/test/server_test.rb
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
@@ -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
|
+
|