I recently became interested in stats and analysis of Dota 2, partially fueled by the stats overlays presented during streamed broadcasts of The International 3, a Dota 2 tournament with a prize pool of nearly $2.9 million USD. To get started with making a stats website (such as datdota or dotabuff), you need to use the Steam WebAPI and (optionally) allow users to login to your website using Steam OpenID. This guide covers the process I followed in getting started using Steam OpenID and the Steam WebAPI with Ruby on Rails, followed with deployment to Heroku.
You can find this walkthrough’s resulting repo on GitHub: https://github.com/grschafer/dota2rails/tree/steam_openid
Getting an API Key
To use Steam OpenID or the Steam WebAPI, you’ll need to get an API Key.
-
As mentioned in above forum post, get an API key from http://steamcommunity.com/dev/apikey.
Making the Rails Site
For an introduction to making a first Rails app, check out this tutorial. I have included all of the necessary steps below, but in less detail than the afore-linked tutorial.
-
Start a new Rails site (I called mine dota2rails). The ‘$’ in code snippets indicates that that line is entered at the command prompt. Edit: Making the new project with the
--skip-active-record
switch avoids some of the database/config-related errors that I manually fix in other steps throughout this walkthrough.$ rails new dota2rails
-
Let’s grab a gem to handle the OpenID authentication for us.
-
To use the omniauth-steam gem, add the following to the bottom of the Gemfile:
# Gemfile … # use Steam OpenID Omniauth gem for authenticating Steam users gem 'omniauth-steam'
-
Install omniauth-steam and all of its dependencies (multi_json, omniauth-openid, omniauth, hashie, rack-openid, ruby-openid) by running the following:
$ bundle install
-
-
As instructed in the documentation for omniauth and omniauth-steam, we need to configure the Rails middleware to use OmniAuth::Builder, so let’s create a new file
config/initializers/omniauth.rb
and add the following lines to it:# config/initializers/omniauth.rb Rails.application.config.middleware.use OmniAuth::Builder do provider :steam, ENV["STEAM_WEB_API_KEY"] end
-
We need to provide that API_KEY to the environment somewhere. There are a few different options for managing Rails environment variables – I’ll be using the figaro gem:
-
Add the following to the bottom of the Gemfile:
# Gemfile … # gem for managing environment variables (steam webapi key) gem 'figaro'
-
Run
bundle install
to install figaro. Next, generate the figaro config file and edit.gitignore
(to avoid committing the config file to the repo) by running the following command:$ rails generate figaro:install
-
Open the generated config file (located at
config/application.yml
) and add your API key:# config/application.yml STEAM_WEB_API_KEY: '234F789DC78E6A88B987AD87F00F'
-
If you’re planning on uploading your repo (i.e. to GitHub), you should also hide your application’s secret token. To do so, move the secret token in
config/initializers/secret_token.rb
to theconfig/application.yml
file.# config/application.yml STEAM_WEB_API_KEY: '234F789DC78E6A88B987AD87F00F' SECRET_TOKEN: '904150a195ef13012705d0c15751b333b2b79cb1678ffe4191d29635d0c57175ea7354b8f4c3290b1085363b7eb546b7d49ca7e40bebee3dced5dc9524f4cbe7'
# config/initializers/secret_token.rb Dota2rails::Application.config.secret_key_base = ENV['SECRET_TOKEN']
-
-
Now we have the OmniAuth middleware, which handles the OpenID login process when we hit the
/auth/:provider
and/auth/:provider/callback
paths for our application (in this case:provider
issteam
). Let’s add a controller and views so we can show a login button to the user and handle the OpenID callback. Run the following command to generate a welcome controller with an index action and the associated view:$ rails generate controller welcome index
-
Edit
app/views/welcome/index.html.erb
to include a login button that links to theauth/steam
path:<h1>Welcome#index</h1> <%= link_to image_tag("http://cdn.steamcommunity.com/public/images/signinthroughsteam/sits_large_noborder.png"), '/auth/steam' %>
-
Mark the index action of the welcome controller as the root route of the application so that visiting
localhost:3000
will show the template we just edited (instead of having to visitlocalhost:3000/welcome/index
). Make this edit inconfig/routes.rb
:# config/routes.rb Dota2rails::Application.routes.draw do get "welcome/index" root 'welcome#index' … end
-
If you run
rails server
now and navigate tolocalhost:3000
, you should see the login button. Clicking it takes you tohttps://steamcommunity.com/openid/login?...
where you can login with your Steam account. Doing so will then redirect you back to your application (tolocalhost:3000/auth/steam/callback?...
), where you should see an error stating that “No route matches [POST] …”. We need to add a controller action and a route to handle the callback. I manually added a callback action to the welcome controller inapp/controllers/welcome_controller.rb
so that it looks as follows:# app/controllers/welcome_controller.rb class WelcomeController < ApplicationController # auth callback POST comes from Steam so we can't attach CSRF token skip_before_filter :verify_authenticity_token, :only => :auth_callback def index end def auth_callback auth = request.env['omniauth.auth'] session[:current_user] = { :nickname => auth.info['nickname'], :image => auth.info['image'], :uid => auth.uid } redirect_to root_url end end
Some extra explanation is in order. The WelcomeController class inherits from ApplicationController, which you can find in
app/controllers/application_controller.rb
. The ApplicationController includes a lineprotect_from_forgery with: :exception
, which enables CSRF protection by inserting a CSRF token in a hidden field in forms (which must match the CSRF token in the head section of the page in order for the submitted form to be accepted). Unfortunately, OmniAuth OpenID seems to be hard-coded to request a POST callback instead of a GET callback so, when Steam POSTs the user’s login info back to our application’s auth_callback action, Rails will expect a valid CSRF token. We have no control over what gets POSTed, so we add theskip_before_filter...
line to turn off CSRF protection for the auth_callback action. If you have a better solution, please let me know!Looking at the
auth_callback
action now, Steam will send the logged-in user’s info to/auth/steam/callback
; the omniauth middleware intercepts this callback and setsrequest.env['omniauth.auth']
as shown here to a hash (here’s an example of the hash returned for my account). I only care about a few items (username, profile image, and steam_id), so I put those items in the current user’s session (a CookieStore by default) and then redirect to the front page.Now, we just need to add the route to
config/routes.rb
for the callback so Rails knows which controller and action should receive the callback:# config/routes.rb Dota2rails::Application.routes.draw do get "welcome/index" root 'welcome#index' post 'auth/steam/callback' => 'welcome#auth_callback' … end
-
Now you should be able to experience the entire authentication flow! When you visit the main page of your website, you can click on the login button which redirects you to Steam, where you can login and then be redirected back to your main page. This is kind of boring though, because it doesn’t show that you are logged in or that it knows who you are. Let’s add that to the view in
app/views/welcome/index.html.erb
:<h1>Welcome#index</h1> <%= link_to image_tag("http://cdn.steamcommunity.com/public/images/signinthroughsteam/sits_large_noborder.png"), '/auth/steam' %> <% if session.key? :current_user %> <h3>Current user:</h3> <%= image_tag session[:current_user][:image] %> <p><%= session[:current_user][:nickname] %></p> <p><%= session[:current_user][:uid] %></p> <% end %>
-
Great! Now if you are logged in, it will display your Steam username, profile image, and SteamID. That’s the groundwork for using Steam OpenID, but I’m going to continue a bit further and show fetching and displaying a list of the logged-in user’s recent Dota 2 matches from the Steam WebAPI. The data provided by the Steam WebAPI for Dota 2 match history looks like this bit of json. To fetch match history, we’ll edit the welcome#index action to call the Steam WebAPI:
class WelcomeController < ApplicationController # auth callback POST comes from Steam so we can't attach CSRF token skip_before_filter :verify_authenticity_token, :only => :auth_callback def index @matchlist = [] if session.key? :current_user url = URI.parse("https://api.steampowered.com/IDOTA2Match_570/GetMatchHistory/v001/?key=#{ENV['STEAM_WEB_API_KEY']}&account_id=#{session[:current_user][:uid]}") res = Net::HTTP::get(url) @matchlist = JSON.load(res)['result']['matches'] || [] end end def auth_callback auth = request.env['omniauth.auth'] session[:current_user] = { :nickname => auth.info['nickname'], :image => auth.info['image'], :uid => auth.uid } redirect_to root_url end end
Edit: If you get a “connection closed by remote host” error when visiting the index page (because of the Net::HTTP::get call), try changing the url from https to http to see if it then works. (However, you probably won’t want to leave it as http, otherwise people that sniff your traffic can see your Steam API Key.) If you do get this error, let me know if you know what’s causing it! I haven’t been able to reproduce it in my environment (Ubuntu 12.04, Ruby 2.0.0, Rails 4.0.0).
-
The
@matchlist
instance variable from welcome#index is available in the corresponding view, where we need to list the matches:# app/views/welcome/index.html.erb <h1>Welcome#index</h1> <%= link_to image_tag("http://cdn.steamcommunity.com/public/images/signinthroughsteam/sits_large_noborder.png"), '/auth/steam' %> <% if session.key? :current_user %> <h3>Current user:</h3> <%= image_tag session[:current_user][:image] %> <p><%= session[:current_user][:nickname] %></p> <p><%= session[:current_user][:uid] %></p> <% end %> <ul> <% @matchlist.each do |match| %> <li><%= Date.strptime(match['start_time'].to_s, '%s').to_formatted_s(:rfc822) %> - <%= match['match_id'] %></li> <% end %> </ul>
Pardon the mess for converting from UNIX timestamp to date. That’s the basics of getting data from the Steam WebAPI, though, and there are many directions this example could be taken from here (match detail pages, stats across multiple matches, and many of the other features provided by existing stats websites such as dotabuff). But now, on to deployment!
Deploying to Heroku
These instructions mostly follow Heroku’s Getting Started with Rails4 article, but are more abbreviated to hopefully get up and running even faster!
-
Signup for a Heroku account if you don’t have one
-
Download the Heroku toolbelt for your platform
-
Log in using the heroku command from the toolbelt you just installed:
$ heroku login Enter your Heroku credentials. Email: your_email@example.com Password: Could not find an existing public key. Would you like to generate one? [Yn] Generating new SSH public key. Uploading ssh public key /home/your_username/.ssh/id_rsa.pub
-
Add the Heroku gem and specify your Ruby version in your Gemfile. Also, comment out the sqlite3 gem, which Heroku doesn’t support and will complain about if it’s enabled:
# Gemfile … # Use sqlite3 as the database for Active Record #gem 'sqlite3' … ruby "2.0.0" # Heroku integration gem gem 'rails_12factor', group: :production
-
Heroku expects deployed apps to use a database, which requires a bunch of extra configuration, so let’s just disable all of that by removing ActiveRecord from our app. Go to
config/application.rb
and change the code fromrequire 'rails/all'
to the following:# config/application.rb … #require 'rails/all' require "action_controller/railtie" require "rails/test_unit/railtie" require "sprockets/railtie" …
-
Update your dependencies by running:
$ bundle install
-
Oh no! If you try running it locally now, you probably will get an error about an undefined method for action_mailer. ActionMailer is a part of the
rails/all
that we just removed (because we don’t need it), but it turns out there are still configuration settings that reference action_mailer and active_record. Go toconfig/environments/development.rb
and comment out the offending lines:# config/environments/development.rb … # Don't care if the mailer can't send. #config.action_mailer.raise_delivery_errors = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations #config.active_record.migration_error = :page_load …
-
Make a git repo at the root of your Rails site (for me it’s at
/home/greg/repos/dota2rails
) if you haven’t already and commit all of your changes:$ git init # if you haven't created a repo already $ git add . $ git commit -m "steam openid and webapi"
-
From the same directory (the root of your Rails site), register your repo with Heroku by running the following:
$ heroku create Creating salty-ravine-7463... done, stack is cedar http://salty-ravine-7463.herokuapp.com/ | git@heroku.com:salty-ravine-7463.git Git remote heroku added
-
Now we need to set Heroku environment variables that we set locally with figaro. Figaro is supposed to be able to do this by running
rake figaro:heroku
, but I encountered an error about active_record being missing so I ended up using the following approach instead:$ heroku config:add STEAM_WEB_API_KEY='234F789DC78E6A88B987AD87F00F' Setting config vars and restarting salty-ravine-7463... done, v3 STEAM_WEB_API_KEY: 234F789DC78E6A88B987AD87F00F $ heroku config:add SECRET_TOKEN='904150a195ef13012705d0c15751b333b2b79cb1678ffe4191d29635d0c57175ea7354b8f4c3290b1085363b7eb546b7d49ca7e40bebee3dced5dc9524f4cbe7' Setting config vars and restarting salty-ravine-7463... done, v4 SECRET_TOKEN: 904150a195ef13012705d0c15751b333b2b79cb1678ffe4191d29635d0c57175ea7354b8f4c3290b1085363b7eb546b7d49ca7e40bebee3dced5dc9524f4cbe7
-
Deploy your app to Heroku!
$ git push heroku master … -----> Ruby/Rails app detected -----> Using Ruby version: ruby-2.0.0 -----> Installing dependencies using Bundler version 1.3.2 Running: bundle install --without development:test --path vendor/bundle --binstubs vendor/bundle/bin --deployment Fetching gem metadata from https://rubygems.org/......... Fetching gem metadata from https://rubygems.org/.. Installing rake (10.1.0) …
-
Visit your new website:
$ heroku open
If you’ve gotten this far successfully, congratulations! You’ve made a simple Rails app with authentication via Steam OpenID and data from the Steam WebAPI that is deployed on Heroku!
If you encountered issues following this guide or have other thoughts/suggestions/corrections, please let me know on twitter, email, or github (links at left)!