Class: Rack::Auth::Cookie

Inherits:
Object
  • Object
show all
Defined in:
lib/rack/auth/cookie.rb

Constant Summary collapse

VERSION =

The version of the rack-auth-cookie library.

'0.7.6'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, options = {}) ⇒ Cookie

Creates a new Rack::Auth::Cookie object.

The cookie_name param gives the name of the cookie used to authenticate the requestor. The default is ‘auth_token’.

The domain_tree_depth param is useful for associating a cookie with an ancestor of the domain where an application is currently hosted. The value indicates the number of domain components to strip off the left side of the fully qualified domain associated with each request when determining the domain to use for the cookie.

The share_cookie_with_subdomains param will result in a “.” appended to the left side of the domain value sent in Set-Cookie response headers. Per RFC 2965, this should cause user agents to include the cookie in requests not only to the associated domain but also to all its subdomains.

For instance, if an application is hosted at “blog.example.com”, setting domain_tree_depth to 1 and share_cookie_with_subdomains to true will result in a domain value of “.example.com” in Set-Cookie headers, meaning “use a cookie that will be visible to example.com and all subdomains of example.com”



33
34
35
36
37
38
39
40
41
42
# File 'lib/rack/auth/cookie.rb', line 33

def initialize(app, options = {})
  @app = app
  @@secret = options[:secret]
  @@cookie_name = options[:cookie_name] || "auth_token"
  @@domain_tree_depth = options[:domain_tree_depth] || nil
  @@share_with_subdomains = options[:share_with_subdomains] || false
  @@idle_timeout = options[:idle_timeout] || 3600
  @@max_lifetime = options[:max_lifetime] || 36000
  @@env = {}
end

Class Method Details



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/rack/auth/cookie.rb', line 244

def self.cookie_domain(env)
  result = host(env)
  
  if @@domain_tree_depth != nil
    components = result.split('.')
    components.slice!(0, @@domain_tree_depth)
    result = components.join('.')
  end
  
  if @@share_with_subdomains
    result = "." + result
  end
  
  result
end


180
181
182
# File 'lib/rack/auth/cookie.rb', line 180

def self.cookie_name
  @@cookie_name
end


210
211
212
213
214
215
216
# File 'lib/rack/auth/cookie.rb', line 210

def self.create_auth_cookie(env)
  cookie_value = create_auth_token(env)
  cookie = "#{@@cookie_name}=#{URI.escape(cookie_value)}; "
  cookie += "domain=#{cookie_domain(env)}; "
  cookie += "path=/; "
  cookie += "HttpOnly; "
end

.create_auth_token(env) ⇒ Object



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/rack/auth/cookie.rb', line 184

def self.create_auth_token(env)
  # Copy relevant auth info for storage in a token
  auth_info = Hash.new
  
  auth_info['AUTH_USER'] = env['AUTH_USER']
  
  auth_info['AUTH_TYPE'] = env['AUTH_TYPE'] || "Unknown"
  auth_info['AUTH_TYPE_USER'] = env['AUTH_TYPE_USER'] || env['AUTH_USER']
  
  # Expecting env['AUTH_DATETIME'] to hold an instance of Time
  if env['AUTH_DATETIME']
    auth_info['AUTH_DATETIME'] = env['AUTH_DATETIME'].to_i
  else
    auth_info['AUTH_DATETIME'] = Time.now.utc.to_i
  end
  
  auth_info['AUTH_EXPIRE_DATETIME'] = Time.now.utc.to_i + @@idle_timeout
  
  # Pack the auth_info hash for cookie storage
  json_data = auth_info.to_json
  packed_data = [json_data].pack('m*')
  
  # Add a digest value to cookie_data to prevent tampering
  "#{packed_data}--#{generate_hmac(packed_data)}"
end


218
219
220
221
222
223
224
225
# File 'lib/rack/auth/cookie.rb', line 218

def self.create_clear_cookie(env)
  cookie_value = ""
  cookie = "#{@@cookie_name}=; "
  cookie += "domain=#{cookie_domain(env)}; "
  cookie += "path=/; "
  cookie += "expires=Thu, 01-Jan-1970 00:00:00 GMT; "
  cookie += "HttpOnly; "
end

.generate_hmac(data) ⇒ Object



227
228
229
# File 'lib/rack/auth/cookie.rb', line 227

def self.generate_hmac(data)
  OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, @@secret, data)
end

.host(env) ⇒ Object



240
241
242
# File 'lib/rack/auth/cookie.rb', line 240

def self.host(env)
  raw_host_with_port(env).sub(/:\d+$/, '')
end

.raw_host_with_port(env) ⇒ Object



231
232
233
234
235
236
237
238
# File 'lib/rack/auth/cookie.rb', line 231

def self.raw_host_with_port(env)
  if forwarded = env["HTTP_X_FORWARDED_HOST"]
    forwarded.split(/,\s?/).last
  else
    env['HTTP_HOST'] ||
      "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
  end
end

Instance Method Details

#call(env) ⇒ Object

The call method we’ve defined first checks to see if AUTH_USER or AUTH_FAIL are set in the environment. If either is set, we assume that the request has already either passed or failed authentication and move on.

If neither is set, we check for the cookie with the name we’ve been configured to use. If present, we attempt to authenticate the user using the cookie. If successful then AUTH_USER is set to the username.

If unsuccessful then AUTH_USER is not set and AUTH_FAIL is set to an appropriate error message.

It is then up to the application to check for the presence of AUTH_USER and/or AUTH_FAIL and act as necessary.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/rack/auth/cookie.rb', line 58

def call(env)
  request = Rack::Request.new(env)
  auth_fail = false
  
  # Only authenticate if there's a cookie in the request named @@cookie_name
  unless request.cookies.has_key?(@@cookie_name)
    return finish(@app, env)
  end
  
  # Get the data from the cookie
  begin
    cookie_value = request.cookies[@@cookie_name]
    hash_data = read_cookie(cookie_value)
  rescue Exception => e
    auth_fail = e.message
  end
  
  # Do not authenticate if either one of these is set
  # This check is done late so that we'll have already
  # checked the cookie
  if env['AUTH_USER'] || env['AUTH_FAIL']
    return finish(@app, env, cookie_value)
  end
  
  if !auth_fail
    auth_datetime = Time.at(hash_data['AUTH_DATETIME']).utc
    auth_expire_datetime = Time.at(hash_data['AUTH_EXPIRE_DATETIME']).utc
    
    if auth_datetime + @@max_lifetime < Time.now.utc
      auth_fail = "You have been signed out since you signed in more than #{@@max_lifetime/3600} hours ago"
    end
    
    if auth_expire_datetime < Time.now.utc
      auth_fail = "You have been signed out due to inactivity"
    end
  end
  
  if auth_fail
    env['AUTH_FAIL'] = auth_fail
  else
    # Put the values from the hash into the environment
    env['AUTH_USER'] = hash_data['AUTH_USER']
    
    env['AUTH_TYPE'] = hash_data['AUTH_TYPE']
    env['AUTH_TYPE_USER'] = hash_data['AUTH_TYPE_USER']
    
    env['AUTH_TYPE_THIS_REQUEST'] = "Cookie"
    
    env['AUTH_DATETIME'] = auth_datetime
    env['AUTH_EXPIRE_DATETIME'] = auth_expire_datetime
  end
  
  finish(@app, env, cookie_value)
end

#finish(app, env, cookie_value_from_request = nil) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/rack/auth/cookie.rb', line 113

def finish(app, env, cookie_value_from_request = nil)
  status, headers, body = @app.call(env)
  
  # Assume our cookie isn't in the response unless/until we find it
  response_cookie = false
  
  if headers.has_key?("Set-Cookie")
    set_cookie = headers["Set-Cookie"]
    set_cookie_pieces = set_cookie.split(";")
    
    # TODO: parse cookies from header and find @@cookie_name
    set_cookie_pieces.each_with_index do |piece, index|
      if piece[@@cookie_name]
        response_cookie = true
      end
    end
  end
  
  # If the application isn't making any changes to the cookie, we can modify it
  if cookie_value_from_request && !response_cookie
    
    # If authentication succeeded earlier, send back a new token
    if env['AUTH_USER']
      cookie = self.class.create_auth_cookie(env)
      
      if headers["Set-Cookie"]
        headers["Set-Cookie"] << cookie
      else
        headers["Set-Cookie"] = cookie
      end
    end
    
    # If authentication failed earlier, tell the client to clear the cookie
    if env['AUTH_FAIL']
      cookie = self.class.create_clear_cookie(env)
      
      if headers["Set-Cookie"]
        headers["Set-Cookie"] << cookie
      else
        headers["Set-Cookie"] = cookie
      end
    end
  end
  
  [status, headers, body]
end


160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/rack/auth/cookie.rb', line 160

def read_cookie(cookie_value)
  # Separate the cookie data and the digest
  raw_data, digest = cookie_value.split("--")
  
  # Check for evidence of tampering
  unless digest == self.class.generate_hmac(raw_data)
    raise "Invalid cookie digest!"
  end
  
  # Unpack the cookie data back to a hash
  begin
    unpacked_data = raw_data.unpack("m*").first
    hash_data = JSON.parse(unpacked_data)
  rescue
    raise "Unable to read cookie!"
  end
  
  hash_data
end