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.2'

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 cookie_domain param gives a domain name to use for the cookie. If unspecified, cookies will be set without specifying a domain. Per RFC 2965, this should cause user agents to default to the effective request-host.



22
23
24
25
26
27
28
29
30
# File 'lib/rack/auth/cookie.rb', line 22

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

Class Method Details



160
161
162
# File 'lib/rack/auth/cookie.rb', line 160

def self.cookie_name
  @@cookie_name
end


190
191
192
193
194
195
196
# File 'lib/rack/auth/cookie.rb', line 190

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

.create_auth_token(env) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/rack/auth/cookie.rb', line 164

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


198
199
200
201
202
203
204
205
# File 'lib/rack/auth/cookie.rb', line 198

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

.generate_hmac(data) ⇒ Object



207
208
209
# File 'lib/rack/auth/cookie.rb', line 207

def self.generate_hmac(data)
  OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, @@secret, data)
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.



46
47
48
49
50
51
52
53
54
55
56
57
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
# File 'lib/rack/auth/cookie.rb', line 46

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



101
102
103
104
105
106
107
108
109
110
111
112
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
# File 'lib/rack/auth/cookie.rb', line 101

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)
      
      headers["Set-Cookie"] << cookie
    end
    
    # If authentication failed earlier, tell the client to clear the cookie
    if env['AUTH_FAIL']
      cookie = self.class.create_clear_cookie(env)
      
      headers["Set-Cookie"] << cookie
    end
  end
  
  [status, headers, body]
end


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 140

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