Permalink
Browse files

Start migration of storing user auth tokens to client cookie + delaye…

…d jobs. (#5)

* Update readme and add example env vars loader file.

* Fix config files. Store auth token as signed cookie.

* First pass at storing token in cookies and delayed_jobs, with fallback to database stored token if needed (for now). Needs testing/tests.

* Fix some loading of tokens.

* Clean up some code.

* Only use user.token from database if user is already logged in and cookie isnt set.
  • Loading branch information...
zackgilbert committed Aug 15, 2018
1 parent 2b22b7e commit 4143fc7d62d0325ce72d560b94fdbff237db651d
View
@@ -22,7 +22,7 @@ by DelayedJob.
Current Status
--------------
4sweep is unmaintained as of March 2015.
4sweep was unmaintained as of March 2015, but in July of 2018, 4sweep was open sourced and has become primarily maintained by Foursquare. While Foursquare will make periodic security and maintenance updates, the 4sweep/Foursquare community is highly encouraged to fork and submit pull requests with new features.
Explorer Features
@@ -48,9 +48,9 @@ Flag Features
Configuration and setup
-----------------------
4sweep is currently built for Rails 3.2 and uses Bootstrap 2.0. You will need
4sweep is currently built for Rails 3.2 (Ruby 2.0.0) and uses Bootstrap 2.0. You will need
to install all required gems. It relies on a database supported by ActiveRecord,
and has only been tested with MySQL 5.5.
and has only been tested with MySQL 5.5/5.6.
Additionally, you will need to install PEG.js, a JavaScript parser generator
library. The easiest way to do this is via npm:
@@ -68,29 +68,31 @@ PEG.js 0.8.0
You only need PEG.js in your development environment. It is used as part of the
Rails asset pipeline to generate a javascript parser.
API credentials
----
4sweep needs you to specify a database in ``config/database.yml``.
You will need to search globally for all instances of "REPLACE_ME".
4sweep depends on several external services. IN ``config/application.yml``,
you will need to specify the following:
```yaml
development:
# Your Foursquare API keys:
app_id: ""
app_secret: ""
callback_url: ""
ENV Variable Storage
----
# Optional, to support the Rake task of generating and publishing map icons:
aws_key: ""
aws_secret: ""
s3_bucket: ""
4sweep now uses ENV variables to store sensitive config variables (like api keys, database credentials, etc). Feel free to use whatever method of storing these ENV variables works best for you, but if a `config/app_environment_variables.rb` file is present, it will be loaded. An example of what that file might look like (and the variables currently stored) can be found in the `config/app_environment_variables-example.rb` file.
# Optional, for Cloudwatch monitoring of 4sweep in production
cloudwatch_key: ""
cloudwatch_secret: ""
Most recent variables used:
```
ENV['APP_SECRET'] = 'REPLACE_ME'
ENV['DB_ADAPTER'] = 'mysql2'
ENV['DB_DATABASE'] = 'REPLACE_ME'
ENV['DB_USERNAME'] = 'REPLACE_ME'
ENV['DB_PASSWORD'] = 'REPLACE_ME'
ENV['DB_HOST'] = 'localhost'
ENV['FOURSQUARE_CLIENT_ID'] = 'REPLACE_ME'
ENV['FOURSQUARE_CLIENT_SECRET'] = 'REPLACE_ME'
ENV['OAUTH_CALLBACK'] = 'http://localhost:3000/session/callback'
ENV['GOOGLE_MAPS_KEY'] = 'REPLACE_ME'
ENV['AWS_KEY'] = 'REPLACE_ME'
ENV['AWS_SECRET'] = 'REPLACE_ME'
ENV['AWS_S3_BUCKET'] = 'REPLACE_ME'
ENV['CLOUDWATCH_KEY'] = 'REPLACE_ME'
ENV['CLOUDWATCH_SECRET'] = 'REPLACE_ME'
ENV['ROLLBAR_ACCESS_TOKEN'] = 'REPLACE_ME'
```
@@ -6,6 +6,9 @@ class ApplicationController < ActionController::Base
def set_foursquare_user
@current_user = get_current_user
# make sure we always set the user's access token from the cookie, instead of db:
@current_user.access_token = cookies.signed[:access_token] if cookies.signed[:access_token].present?
@current_user
end
def api_version
@@ -44,11 +47,10 @@ def foursquare_userless
private
def require_user
if cookies[:access_token] == nil
if cookies.signed[:access_token] == nil
redirect_to :controller => :session, :action => :new
return false
end
session[:access_token] = cookies[:access_token]
@current_user = current_user
if @current_user.nil?
redirect_to :controller => :session, :action => :new
@@ -58,17 +60,23 @@ def require_user
end
def get_current_user
return nil if cookies[:access_token].blank?
return nil if cookies.signed[:access_token].blank?
begin
foursquare = Foursquare2::Client.new(:oauth_token => cookies[:access_token], :connection_middleware => [Faraday::Response::Logger, FaradayMiddleware::Instrumentation], :api_version => api_version)
@current_user ||= User.find_by_token(cookies[:access_token])
foursquare = Foursquare2::Client.new(:oauth_token => cookies.signed[:access_token], :connection_middleware => [Faraday::Response::Logger, FaradayMiddleware::Instrumentation], :api_version => api_version)
@current_user ||= User.find_by_uid(foursquare.user('self').id)
# TODO: remove when reomving user.token
# ensure existing users that are logged in have the user's database stored token set in the cookie:
if @current_user.token.present? && !cookies.signed[:access_token].present?
cookies.permanent.signed[:access_token] = @current_user.token
end
# set the user access token (so it uses that instead of the database stored):
@current_user.access_token = cookies.signed[:access_token]
rescue Foursquare2::APIError
cookies[:access_token] = nil
session[:access_token] = nil
redirect_to :controller => :session, :action=>:new
cookies.signed[:access_token] = nil
redirect_to :controller => :session, :action => :new
end
@current_user
end
@@ -100,7 +100,7 @@ def newcount
def resubmit
processflags do |flag|
flag.resolved_details = nil
flag.queue_for_submit(Time.zone.now)
flag.queue_for_submit(@current_user.oauth_token, Time.zone.now)
flag
end
end
@@ -143,7 +143,7 @@ def create
flag.user = @current_user
flag.save!
if params[:runimmediately] && params[:runimmediately] != 'false' or flag["scheduled_at"]
flag.queue_for_submit(5.minutes.from_now)
flag.queue_for_submit(@current_user.oauth_token, 5.minutes.from_now)
end
flags << flag
end
@@ -155,7 +155,7 @@ def create
def run
processflags do |flag|
flag.queue_for_submit(Time.now)
flag.queue_for_submit(@current_user.oauth_token, Time.now)
end
end
@@ -14,23 +14,23 @@ def callback
if code
# set up new oauth2 client, get token
begin
token = oauth_client.auth_code.get_token(code, :redirect_uri => Settings.callback_url)
cookies.permanent[:access_token] = token.token
token = oauth_client.auth_code.get_token(code, :redirect_uri => Settings.callback_url)
cookies.permanent.signed[:access_token] = token.token
rescue OAuth2::Error => e
flash[:notice] = "Login Failure: " + e.message
end
end
# Now that we have an access token, let's see if we have a user for this person:
foursquare = Foursquare2::Client.new(:oauth_token => cookies[:access_token], :connection_middleware => [Faraday::Response::Logger, FaradayMiddleware::Instrumentation], :api_version => '20140107')
foursquare = Foursquare2::Client.new(:oauth_token => cookies.signed[:access_token], :connection_middleware => [Faraday::Response::Logger, FaradayMiddleware::Instrumentation], :api_version => '20140107')
foursquare_user = foursquare.user('self')
user = User.find_by_uid(foursquare_user.id)
if user
@current_user = user
# TODO: token will be going byebye soon
@current_user[:token] = cookies[:access_token]
# let's clear their user cache, it seems to be causing problems:
@current_user.user_cache = nil
@@ -40,16 +40,25 @@ def callback
@current_user = User.create(
:uid => foursquare_user.id,
:name => "#{foursquare_user.firstName} #{foursquare_user.lastName}".strip,
:token => cookies[:access_token],
:enabled => true)
end
redirect_to :controller => :explorer, :action => :explore
end
def new
@current_user ||= User.find_by_token(cookies[:access_token])
redirect_to :controller => :explorer, :action => :explore if @current_user
if !@current_user && cookies.signed[:access_token]
# Now that we have an access token, let's see if we have a user for this person:
foursquare = Foursquare2::Client.new(:oauth_token => cookies.signed[:access_token], :connection_middleware => [Faraday::Response::Logger, FaradayMiddleware::Instrumentation], :api_version => '20140107')
foursquare_user = foursquare.user('self')
@current_user = User.find_by_uid(foursquare_user.id)
end
if @current_user
redirect_to :controller => :explorer, :action => :explore
return
end
@authorize_url = oauth_client.auth_code.authorize_url(:redirect_uri => Settings.callback_url)
end
@@ -61,8 +70,7 @@ def error
end
def logout
cookies[:access_token] = nil
session[:access_token] = nil
cookies.signed[:access_token] = nil
redirect_to :action => :new
end
View
@@ -3,23 +3,28 @@ class Flag < ActiveRecord::Base
attr_accessible :created_at, :primaryName, :venueId,
:status, :type, :user, :problem,
:comment, :scheduled_at, :venues_details
attr_accessor :access_token
serialize :venues_details, JSON
validates_presence_of :venueId, :on => :create, :message => "can't be blank"
# validates_inclusion_of :status, :in => %w( new resolved submitted ), :message => "extension %s is not included in the list"
validates_presence_of :user_id, :on => :create, :message => "can't be blank"
HOME_CAT_ID = '4bf58dd8d48988d103941735';
def user_token
# TODO: remove the user.oauth_token and ONLY use self.access_token
@token ||= ( self.access_token.present? ) ? self.access_token : user.oauth_token
end
def client
user.foursquare_client
@user_client ||= Foursquare2::Client.new(:oauth_token => user_token, :connection_middleware => [Faraday::Response::Logger, FaradayMiddleware::Instrumentation], :api_version => '20140825')
end
def userless_client
@userless_client ||= Foursquare2::Client.new(:client_id => Settings.app_id, :client_secret => Settings.app_secret, :connection_middleware => [Faraday::Response::Logger, FaradayMiddleware::Instrumentation], :api_version => '20140825')
end
def queue_for_submit(delayed_time = Time.now, queue = 'submit')
def queue_for_submit(token, delayed_time = Time.now, queue = 'submit')
if self.job_id
begin
# Delete job if it is already present
@@ -55,15 +60,16 @@ def queue_for_submit(delayed_time = Time.now, queue = 'submit')
priority = 20
end
job = Delayed::Job.enqueue(SubmitFlagJob.new(self), :priority => priority, :run_at =>delayed_time, :queue => queue)
job = Delayed::Job.enqueue(SubmitFlagJob.new(self, token), :priority => priority, :run_at => delayed_time, :queue => queue)
self.job_id = job.id
save
end
def submit
def submit(delayed_token)
if status == 'canceled' || status == 'resolved'
return
end
self.access_token = delayed_token
begin
result = submithelper
rescue Foursquare2::APIError => e
@@ -177,6 +183,6 @@ def details
end
def as_json(options = {})
super options.merge(:methods => [:friendly_name, :flag_type, :details])
super options.merge(:methods => [:friendly_name, :flag_type, :details, :user_token])
end
end
@@ -1,7 +1,7 @@
class SubmitFlagJob < Struct.new(:flag)
class SubmitFlagJob < Struct.new(:flag, :token)
def perform
flag.submit
flag.submit(token)
end
def success(job)
@@ -36,7 +36,7 @@ def reschedule_at(time_now, attempts)
flags_of_type = flag.user.flags.where(:status => "queued").where("type NOT IN (?)", ["PhotoFlag", "TipFlag"]).count
rate_limit = 5000
end
# If this fails due to rate_limit_exceeded, figure out how long it will take to process all flags
# If this fails due to rate_limit_exceeded, figure out how long it will take to process all flags
# of this type and randomly assign a time in that window. It's hacky and brittle, but better than
# every job retrying at the same time.
# TODO: make this reschedule all likely to fail flags, not just this one
View
@@ -1,12 +1,19 @@
class User < ActiveRecord::Base
attr_accessible :enabled, :level, :name, :token, :uid
# TODO: remove token
attr_accessible :enabled, :level, :name, :uid, :token
attr_accessor :access_token
has_many :flags
serialize :user_cache, JSON
MAX_USER_AGE = 1.hour
def oauth_token
# TODO: remove the self.token and ONLY use self.access_token
@token ||= ( self.access_token.present? ) ? self.access_token : self.token
end
def foursquare_client
foursquare ||= Foursquare2::Client.new(:oauth_token => token, :connection_middleware => [Faraday::Response::Logger, FaradayMiddleware::Instrumentation], :api_version => '20140825')
foursquare ||= Foursquare2::Client.new(:oauth_token => oauth_token, :connection_middleware => [Faraday::Response::Logger, FaradayMiddleware::Instrumentation], :api_version => '20140825')
end
def foursquare_user
@@ -9,7 +9,7 @@
<script type="text/javascript">
var lat = <%= @lat %>
var lng = <%= @lng %>
var token = '<%= cookies[:access_token] %>'
var token = '<%= cookies.signed[:access_token] %>'
var sulevel = '<%= @current_user.level %>'
var categories = <%=raw @categories %>
var client_id = '<%= @client_id %>'
@@ -0,0 +1,20 @@
# Use this file to load ENV variables in development mode:
# https://stackoverflow.com/questions/4911607/is-it-possible-to-set-env-variables-for-rails-development-environment-in-my-code/11765775#11765775
ENV['APP_SECRET'] = 'REPLACE_ME'
ENV['DB_ADAPTER'] = 'mysql2'
ENV['DB_DATABASE'] = 'REPLACE_ME'
ENV['DB_USERNAME'] = 'REPLACE_ME'
ENV['DB_PASSWORD'] = 'REPLACE_ME'
ENV['DB_HOST'] = 'localhost'
ENV['FOURSQUARE_CLIENT_ID'] = 'REPLACE_ME'
ENV['FOURSQUARE_CLIENT_SECRET'] = 'REPLACE_ME'
ENV['OAUTH_CALLBACK'] = 'http://localhost:3000/session/callback'
ENV['GOOGLE_MAPS_KEY'] = 'REPLACE_ME'
ENV['AWS_KEY'] = 'REPLACE_ME'
ENV['AWS_SECRET'] = 'REPLACE_ME'
ENV['AWS_S3_BUCKET'] = 'REPLACE_ME'
ENV['CLOUDWATCH_KEY'] = 'REPLACE_ME'
ENV['CLOUDWATCH_SECRET'] = 'REPLACE_ME'
ENV['ROLLBAR_ACCESS_TOKEN'] = 'REPLACE_ME'
View
@@ -1,10 +1,4 @@
defaults: &defaults
development:
test:
production:
app_id: "<%= ENV['FOURSQUARE_CLIENT_ID'] %>"
app_secret: "<%= ENV['FOURSQUARE_CLIENT_SECRET'] %>"
callback_url: "<%= ENV['OAUTH_CALLBACK'] %>"
@@ -14,3 +8,12 @@ production:
cloudwatch_secret: "<%= ENV['CLOUDWATCH_SECRET'] %>"
google_maps_key: "<%= ENV['GOOGLE_MAPS_KEY'] %>"
s3_bucket: "<%= ENV['AWS_S3_BUCKET'] %>"
development:
<<: *defaults
test:
<<: *defaults
production:
<<: *defaults
View
@@ -4,6 +4,11 @@
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
development:
adapter: "<%= ENV['DB_ADAPTER'] %>"
database: "<%= ENV['DB_DATABASE'] %>"
username: "<%= ENV['DB_USERNAME'] %>"
password: "<%= ENV['DB_PASSWORD'] %>"
host: "<%= ENV['DB_HOST'] %>"
production:
adapter: "<%= ENV['DB_ADAPTER'] %>"
@@ -1,6 +1,6 @@
# Be sure to restart your server when you modify this file.
Foursweep::Application.config.session_store :cookie_store, :key => '_foursweep_session'
Foursweep::Application.config.session_store :cookie_store, :key => '_foursweep_session', secure: Rails.env.production?
# Use the database for sessions instead of the cookie-based default,
# which shouldn't be used to store highly confidential information

0 comments on commit 4143fc7

Please sign in to comment.