Add WIP login via Steam + omniauth

This commit is contained in:
Ari Timonen 2020-04-10 18:32:18 +03:00
parent 4583136768
commit f29eb22aa9
15 changed files with 239 additions and 54 deletions

14
Gemfile
View file

@ -13,16 +13,18 @@ gem 'dotenv-rails'
# DB
gem 'mysql2'
gem 'dalli'
gem 'connection_pool' # Needed for MT
# Web server
gem 'faraday'
gem 'puma'
gem 'unicorn'
# gem 'unicorn'
# Model plugins
gem 'unread'
gem 'scrypt'
# gem 'impressionist'
gem 'active_flag'
# gem 'impressionist
# gem 'ratyrate'
# gem "acts_as_rateable", :git => "git://github.com/anton-zaytsev/acts_as_rateable.git"
@ -30,6 +32,13 @@ gem 'scrypt'
gem 'google-api-client', '~> 0.10.3'
gem 'steam-condenser', github: 'koraktor/steam-condenser-ruby'
# Auth
gem 'omniauth'
gem 'omniauth-steam'
gem 'omniauth-rails_csrf_protection'
# FIXME
# gem 'rails_csrf_protection'
# View and model helper gems
gem 'time_difference'
gem 'public_suffix'
@ -133,6 +142,7 @@ group :development, :test do
gem 'pry-byebug'
gem 'spring'
gem "rails_best_practices"
gem 'awesome_print'
# For n+1 uqeries
# gem 'bullet'
end

View file

@ -87,6 +87,8 @@ GEM
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_flag (1.5.0)
activerecord (>= 5)
active_link_to (1.0.5)
actionpack
addressable
@ -119,6 +121,7 @@ GEM
archive-zip (0.12.0)
io-like (~> 0.3.0)
ast (2.4.0)
awesome_print (1.8.0)
bbcoder (1.1.1)
better_errors (2.6.0)
coderay (>= 1.0.0)
@ -163,6 +166,7 @@ GEM
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.1.6)
connection_pool (2.2.2)
countries (3.0.1)
i18n_data (~> 0.10.0)
sixarm_ruby_unaccent (~> 1.1)
@ -228,6 +232,7 @@ GEM
haml (5.1.2)
temple (>= 0.8.0)
tilt
hashie (4.1.0)
httpclient (2.8.3)
hurley (0.2)
i18n (0.9.5)
@ -253,7 +258,6 @@ GEM
thor (>= 0.14, < 2.0)
json (2.3.0)
jwt (2.2.1)
kgio (2.11.3)
loofah (2.4.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
@ -284,6 +288,18 @@ GEM
mini_portile2 (~> 2.4.0)
nokogumbo (2.0.2)
nokogiri (~> 1.8, >= 1.8.4)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
omniauth-openid (1.0.1)
omniauth (~> 1.0)
rack-openid (~> 1.3.1)
omniauth-rails_csrf_protection (0.1.2)
actionpack (>= 4.2)
omniauth (>= 1.3.1)
omniauth-steam (1.0.6)
multi_json
omniauth-openid
os (1.0.1)
parallel (1.19.1)
parser (2.7.0.5)
@ -306,6 +322,9 @@ GEM
puma (4.3.3)
nio4r (~> 2.0)
rack (2.2.2)
rack-openid (1.3.1)
rack (>= 1.1.0)
ruby-openid (>= 2.1.8)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.0.2.2)
@ -349,7 +368,6 @@ GEM
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
rainbow (3.0.0)
raindrops (0.19.1)
rake (13.0.1)
rb-fsevent (0.10.3)
rb-inotify (0.10.1)
@ -377,6 +395,7 @@ GEM
rexml
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
ruby-openid (2.9.2)
ruby-progressbar (1.10.1)
ruby-vips (2.0.17)
ffi (~> 1.9)
@ -440,9 +459,6 @@ GEM
execjs (>= 0.3.0, < 3)
unicode-display_width (1.6.1)
unicode_utils (1.4.0)
unicorn (5.5.4)
kgio (~> 2.6)
raindrops (~> 0.7)
unread (0.11.0)
activerecord (>= 3)
web-console (4.0.1)
@ -462,9 +478,11 @@ PLATFORMS
ruby
DEPENDENCIES
active_flag
active_link_to
active_record_union
annotate
awesome_print
bbcoder
better_errors
binding_of_caller
@ -474,6 +492,7 @@ DEPENDENCIES
carrierwave
codeclimate-test-reporter
coffee-rails
connection_pool
country_select
dalli
database_cleaner-active_record
@ -494,6 +513,9 @@ DEPENDENCIES
neat (~> 1.6.0)
newrelic_rpm
nokogiri
omniauth
omniauth-rails_csrf_protection
omniauth-steam
phantomjs
poltergeist
pry-byebug
@ -527,7 +549,6 @@ DEPENDENCIES
timecop
tinymce-rails
uglifier
unicorn
unread
web-console
will_paginate

View file

@ -2,12 +2,13 @@
Banner
*/
header .banner {
height: 180px;
#authentication {
@include span-columns(5 of 12);
@include shift(7);
@include span-columns(6 of 12);
@include shift(6);
padding: 30px 0;
padding-top: 50px;
color: white;
@ -52,8 +53,14 @@ header .banner {
.fields {
@include span-columns(12);
img {
padding-top: 4px;
padding-right: 5px;
@include span-columns(2 of 12);
}
input {
@include span-columns(6);
@include span-columns(5 of 12);
margin-bottom: 10px;
}
}
@ -73,10 +80,6 @@ header .banner {
}
}
.password-reset {
float: right;
}
.links {
float: right;
position: relative;
@ -116,15 +119,23 @@ header .banner {
}
.buttons {
@include span-columns(12);
@include span-columns(12 of 12);
font-family: $header-font-family;
text-align: right;
text-transform: uppercase;
font-size: 12px;
.login,
.login {
@include span-columns(5 of 12);
@include shift(1.7);
}
.register {
@include span-columns(6);
@include span-columns(5 of 12);
.password-reset {
float: right;
}
}
.login input {

View file

@ -7,7 +7,9 @@ class ApplicationController < ActionController::Base
before_action :update_user
before_action :set_controller_and_action_names
protect_from_forgery
# Omniauth has its own CSRF
protect_from_forgery :except => [:callback]
respond_to :html, :js
def cuser
@ -24,6 +26,18 @@ class ApplicationController < ActionController::Base
redirect_to addr
end
def return_back
if session[:return_to]
return_to
elsif request.env["HTTP_REFERER"]
redirect_to request.env["HTTP_REFERER"]
else
redirect_to "/"
end
rescue
redirect_to "/"
end
def redirect_to_back
if request.env["HTTP_REFERER"]
redirect_to request.env["HTTP_REFERER"]

View file

@ -60,9 +60,6 @@ class UsersController < ApplicationController
raise AccessError unless @user.can_create? cuser
if @user.valid? and @user.save
@user.profile = Profile.new
@user.profile.user = @user
@user.profile.save!
redirect_to action: :show, id: @user.id
save_session @user
else
@ -88,28 +85,26 @@ class UsersController < ApplicationController
redirect_to users_url
end
def callback
@user = User.focfah(auth_hash, request.ip)
login_user(@user)
if @user.created_at > (Time.zone.now - 1.week.ago)
render :edit
else
return_back
end
end
# FIXME: maybe move to session controller
def login
if params[:login]
if (u = User.authenticate(params[:login]))
if u.banned? Ban::TYPE_SITE
flash[:notice] = t(:accounts_locked)
else
flash[:notice] = "%s (%s)" % [t(:login_successful), u.password_hash_s]
# FIXME: this doesn't work because model is saved before
flash[:notice] << " \n%s" % I18n.t(:password_md5_scrypt) if u.password_hash_changed?
save_session u
end
login_user(u)
else
flash[:error] = t(:login_unsuccessful)
end
end
# FIXME: check return on rails 6
if session[:return_to]
return_to
else
redirect_to_back
end
return_back
end
def logout
@ -134,10 +129,25 @@ class UsersController < ApplicationController
@user = User.find(params[:id])
end
def login_user(user)
if user.banned? Ban::TYPE_SITE
flash[:error] = t(:accounts_locked)
else
flash[:notice] = "%s (%s)" % [t(:login_successful), user.password_hash_s]
# FIXME: this doesn't work because model is saved before
flash[:notice] << " \n%s" % I18n.t(:password_md5_scrypt) if user.password_hash_changed?
save_session user
end
end
def save_session user
session[:user] = user.id
user.lastip = request.ip
user.lastvisit = Time.now.utc
user.save
user.save!
end
def auth_hash
request.env['omniauth.auth']
end
end

View file

@ -18,4 +18,9 @@ module UsersHelper
# link_to_remote text, options, html_options
end
def steamid_tool
df = DataFile.where("name LIKE '%SteamID Finder%'").first
df ? data_file_url(df) : "/"
end
end

View file

@ -28,7 +28,9 @@
#
require 'digest/md5'
require 'steamid'
require "scrypt"
require 'securerandom'
class SteamIdValidator < ActiveModel::Validator
def validate(record)
@ -50,11 +52,10 @@ class User < ActiveRecord::Base
PASSWORD_MD5_SCRYPT = 2
#attr_protected :id, :created_at, :updated_at, :lastvisit, :lastip, :password, :version
attr_accessor :raw_password, :password_updated
attr_accessor :raw_password, :password_updated, :password_force, :fullname
attribute :lastvisit, :datetime, default: Time.now.utc
attribute :password_hash, :integer, default: PASSWORD_SCRYPT
attr_accessor :password_force
belongs_to :team, :optional => true
has_one :profile, :dependent => :destroy
@ -142,7 +143,9 @@ class User < ActiveRecord::Base
# validates_inclusion_of :password_hash, in: => [User::PASSWORD_SCRYPT, User::PASSWORD_MD5, User::PASSWORD_MD5_SCRYPT]
validate :validate_team
before_validation :set_name
before_create :init_variables
after_create :create_profile
before_save :correct_steamid_universe
accepts_nested_attributes_for :profile
@ -168,6 +171,17 @@ class User < ActiveRecord::Base
username
end
def set_name
return unless fullname
if fullname.include?(" ")
# TODO: check this
self.firstname = fullname.match(/(?:^|(?:\.\s))(\w+)/)[1]
self.surname = fullname.match(/\s(\w+)$/)[1]
else
self.firstname = fullname
end
end
def password_hash_s
case self.password_hash
when User::PASSWORD_MD5
@ -327,6 +341,14 @@ class User < ActiveRecord::Base
def init_variables
self.public_email = false
self.time_zone = "Amsterdam"
self.raw_password = SecureRandom.base64(32) unless raw_password and new_record?
self.profile = profile.build unless profile&.present?
end
def create_profile
if profile
profile.save
end
end
# NOTE: function does not call save
@ -363,6 +385,18 @@ class User < ActiveRecord::Base
true
end
def fix_attributes
if errors[:username]
i = 2
loop do
new_username = "%s%d" % [username, i]
i+=1
break if User.find_by_username(new_username).count == 0 or i > 50
end
self.username = new_username
end
end
def can_update? cuser
cuser and (self == cuser or cuser.admin?)
end
@ -410,8 +444,8 @@ class User < ActiveRecord::Base
return nil
end
def self.get id
id ? find(id) : ""
def self.get(id)
id ? User.find(id) : ""
end
def self.historic steamid
@ -442,4 +476,26 @@ class User < ActiveRecord::Base
allowed << :username if cuser&.admin? || operation == 'create'
params.require(:user).permit(*allowed)
end
end
def self.focfah(auth_hash, lastip)
return nil unless auth_hash&.include?(:provider)
byebug
case auth_hash[:provider]
when 'steam'
return nil unless auth_hash&.include?(:uid)
steamid = SteamID::from_steamID64(auth_hash[:uid])
user = User.where("LOWER(steamid) = LOWER(?)", steamid).first
unless user
user = User.new(username: auth_hash[:info][:nickname], lastip: lastip, fullname: auth_hash[:info][:name], steamid: steamid)
user.fix_attributes
# TODO: user make valid by force
# user.profile.country
# get profile picture, :image
# This really shouldn't fail.
user.save!
end
return user
end
return nil
end
end

View file

@ -1,31 +1,49 @@
<div id="registration">
<h1>Registration</h1>
<%= link_to "You can use this tool to find your SteamID.", "/files/client/steamid_finder.exe" %>
<%= form_for @user, html: { class: "square" } do |f| %>
<%= render 'shared/errors', messages: @user.errors.full_messages %>
<div class="fields">
<div class="horizontal text-field">
<%= link_to "Create your account via Steam", "/auth/steam", method: :POST %>
</div>
<div class="horizontal text-field">
<%= f.label :username %>
<%= f.text_field :username %>
</div>
<div class="horizontal text-field">
Pick unique nickname for yourself.
</div>
<div class="horizontal text-field">
<%= f.label :raw_password, "Password" %>
<%= f.password_field :raw_password %>
</div>
<div class="horizontal text-field">
Please don't use same password as any important place.
</div>
<div class="horizontal text-field">
<%= f.label :email %>
<%= f.text_field :email %>
</div>
<div class="horizontal text-field">
The email is needed to reset password, verify identity and send account related emails. We don't send spam or sell your email. By default the email is private and only seen by admins.
</div>
<div class="horizontal text-field">
<%= f.label :steamid %>
<%= f.text_field :steamid, placeholder: "0:1:23456789" %>
</div>
<div class="horizontal text-field">
You can use <%= link_to "this tool", steamid_tool %> or
<%= link_to "this web page", 'https://steamidfinder.com/' %>
to find your SteamID. We need the steam id to identify unique players. If you use fake one, some things on website might be broken.
</div>
<div class="horizontal">
<%= f.label :birthdate %>
<%= date_select :user, :birthdate, order: [:year, :month, :day], start_year: 1950 %>
<%= date_select :user, :birthdate, order: [:year, :month, :day], start_year: 1950, include_blank: true, default: nil %>
</div>
<div class="horizontal text-field">
Only needed for fun stats (age etc.). You don't need to give valid one.
</div>
</div>
<div class="controls submit-field">

View file

@ -1,5 +1,8 @@
<%= form_tag({ controller: "users", action: "login" }, { class: 'dark' }) do %>
<div class="fields">
<%= link_to "/auth/steam", method: :POST do %>
<%= image_tag '/images/icons/steam_login.png' %>
<% end %>
<%= text_field "login", "username", placeholder: "Username" %>
<%= password_field "login", "password", placeholder: "Password" %>
</div>

View file

@ -23,12 +23,29 @@ module Ensl
# Custom directories with classes and modules you want to be autoloadable.
config.autoload_paths += Dir["#{config.root}/app/services/**/", "#{config.root}/app/models/concerns/"]
# Be sure to restart your server when you modify this file.
config.session_store :cookie_store, key: '_ensl_session'
# Load secrets from .env
ENV['APP_SECRET'] ||= (0...32).map { (65 + rand(26)).chr }.join
config.secret_token = ENV['APP_SECRET']
# Use cookies
config.session_store :cookie_store, key: '_ENSL_session_key', expire_after: 30.days.to_i
# Use a different cache store
config.cache_store = :dalli_store, 'memcached:11211'
# Use smtp-Server
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: 'smtp',
domain: ENV['MAIL_DOMAIN']
}
# Specifies the header that your server uses for sending files
# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
# Use a different logger for distributed setups
config.logger = Logger.new(Rails.root.join("log", Rails.env + ".log" ), 5 , 10 * 1024 * 1024)
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
@ -43,14 +60,21 @@ module Ensl
# Enable the asset pipeline
config.assets.enabled = true
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
# il8n fix
config.i18n.fallbacks = true
config.i18n.enforce_available_locales = false
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
# Tiny mce
config.tinymce.install = :copy
# Send deprecation notices to registered listeners
config.active_support.deprecation = :notify
# Enable threaded mode
# Almost nothing is thread-safe, do not
# config.threadsafe!
end
end

View file

@ -0,0 +1,3 @@
Rails.application.config.middleware.use OmniAuth::Builder do
provider :steam, ENV['STEAM_WEB_API_KEY']
end

View file

@ -1,3 +0,0 @@
# Be sure to restart your server when you modify this file.
Ensl::Application.config.session_store :cookie_store, key: '_ensl_session'

View file

@ -1,4 +1,4 @@
Ensl::Application.routes.draw do
Rails.application.routes.draw do
if Rails.env.production?
%w(403 404 422 500).each do |code|
get code, to: "errors#show", code: code
@ -71,6 +71,7 @@ Ensl::Application.routes.draw do
post 'forgot'
end
end
post 'auth/:provider/callback', to: 'users#callback'
resources :locks
resources :contesters

12
lib/steamid.rb Normal file
View file

@ -0,0 +1,12 @@
module SteamID
def self.to_steamID(steamid)
m = steamid.match(/^(STEAM_){0,1}([0-5]):([01]:\d+)$/)
return ((m[3].to_i * 2) + m[2].to_i) + 76561197960265728
end
def self.from_steamID64(sid)
y = sid.to_i - 76561197960265728
x = y % 2
return "0:%d:%d" % [x, (y - x).div(2)]
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB