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

View file

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

View file

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

View file

@ -7,7 +7,9 @@ class ApplicationController < ActionController::Base
before_action :update_user before_action :update_user
before_action :set_controller_and_action_names before_action :set_controller_and_action_names
protect_from_forgery # Omniauth has its own CSRF
protect_from_forgery :except => [:callback]
respond_to :html, :js respond_to :html, :js
def cuser def cuser
@ -24,6 +26,18 @@ class ApplicationController < ActionController::Base
redirect_to addr redirect_to addr
end 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 def redirect_to_back
if request.env["HTTP_REFERER"] if request.env["HTTP_REFERER"]
redirect_to 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 raise AccessError unless @user.can_create? cuser
if @user.valid? and @user.save if @user.valid? and @user.save
@user.profile = Profile.new
@user.profile.user = @user
@user.profile.save!
redirect_to action: :show, id: @user.id redirect_to action: :show, id: @user.id
save_session @user save_session @user
else else
@ -88,28 +85,26 @@ class UsersController < ApplicationController
redirect_to users_url redirect_to users_url
end 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 # FIXME: maybe move to session controller
def login def login
if params[:login] if params[:login]
if (u = User.authenticate(params[:login])) if (u = User.authenticate(params[:login]))
if u.banned? Ban::TYPE_SITE login_user(u)
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
else else
flash[:error] = t(:login_unsuccessful) flash[:error] = t(:login_unsuccessful)
end end
end end
# FIXME: check return on rails 6 return_back
if session[:return_to]
return_to
else
redirect_to_back
end
end end
def logout def logout
@ -134,10 +129,25 @@ class UsersController < ApplicationController
@user = User.find(params[:id]) @user = User.find(params[:id])
end 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 def save_session user
session[:user] = user.id session[:user] = user.id
user.lastip = request.ip user.lastip = request.ip
user.lastvisit = Time.now.utc user.lastvisit = Time.now.utc
user.save user.save!
end
def auth_hash
request.env['omniauth.auth']
end end
end end

View file

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

View file

@ -28,7 +28,9 @@
# #
require 'digest/md5' require 'digest/md5'
require 'steamid'
require "scrypt" require "scrypt"
require 'securerandom'
class SteamIdValidator < ActiveModel::Validator class SteamIdValidator < ActiveModel::Validator
def validate(record) def validate(record)
@ -50,11 +52,10 @@ class User < ActiveRecord::Base
PASSWORD_MD5_SCRYPT = 2 PASSWORD_MD5_SCRYPT = 2
#attr_protected :id, :created_at, :updated_at, :lastvisit, :lastip, :password, :version #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 :lastvisit, :datetime, default: Time.now.utc
attribute :password_hash, :integer, default: PASSWORD_SCRYPT attribute :password_hash, :integer, default: PASSWORD_SCRYPT
attr_accessor :password_force
belongs_to :team, :optional => true belongs_to :team, :optional => true
has_one :profile, :dependent => :destroy 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] # validates_inclusion_of :password_hash, in: => [User::PASSWORD_SCRYPT, User::PASSWORD_MD5, User::PASSWORD_MD5_SCRYPT]
validate :validate_team validate :validate_team
before_validation :set_name
before_create :init_variables before_create :init_variables
after_create :create_profile
before_save :correct_steamid_universe before_save :correct_steamid_universe
accepts_nested_attributes_for :profile accepts_nested_attributes_for :profile
@ -168,6 +171,17 @@ class User < ActiveRecord::Base
username username
end 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 def password_hash_s
case self.password_hash case self.password_hash
when User::PASSWORD_MD5 when User::PASSWORD_MD5
@ -327,6 +341,14 @@ class User < ActiveRecord::Base
def init_variables def init_variables
self.public_email = false self.public_email = false
self.time_zone = "Amsterdam" 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 end
# NOTE: function does not call save # NOTE: function does not call save
@ -363,6 +385,18 @@ class User < ActiveRecord::Base
true true
end 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 def can_update? cuser
cuser and (self == cuser or cuser.admin?) cuser and (self == cuser or cuser.admin?)
end end
@ -410,8 +444,8 @@ class User < ActiveRecord::Base
return nil return nil
end end
def self.get id def self.get(id)
id ? find(id) : "" id ? User.find(id) : ""
end end
def self.historic steamid def self.historic steamid
@ -442,4 +476,26 @@ class User < ActiveRecord::Base
allowed << :username if cuser&.admin? || operation == 'create' allowed << :username if cuser&.admin? || operation == 'create'
params.require(:user).permit(*allowed) 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 end

View file

@ -1,31 +1,49 @@
<div id="registration"> <div id="registration">
<h1>Registration</h1> <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| %> <%= form_for @user, html: { class: "square" } do |f| %>
<%= render 'shared/errors', messages: @user.errors.full_messages %> <%= render 'shared/errors', messages: @user.errors.full_messages %>
<div class="fields"> <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"> <div class="horizontal text-field">
<%= f.label :username %> <%= f.label :username %>
<%= f.text_field :username %> <%= f.text_field :username %>
</div> </div>
<div class="horizontal text-field">
Pick unique nickname for yourself.
</div>
<div class="horizontal text-field"> <div class="horizontal text-field">
<%= f.label :raw_password, "Password" %> <%= f.label :raw_password, "Password" %>
<%= f.password_field :raw_password %> <%= f.password_field :raw_password %>
</div> </div>
<div class="horizontal text-field">
Please don't use same password as any important place.
</div>
<div class="horizontal text-field"> <div class="horizontal text-field">
<%= f.label :email %> <%= f.label :email %>
<%= f.text_field :email %> <%= f.text_field :email %>
</div> </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"> <div class="horizontal text-field">
<%= f.label :steamid %> <%= f.label :steamid %>
<%= f.text_field :steamid, placeholder: "0:1:23456789" %> <%= f.text_field :steamid, placeholder: "0:1:23456789" %>
</div> </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"> <div class="horizontal">
<%= f.label :birthdate %> <%= 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> </div>
<div class="controls submit-field"> <div class="controls submit-field">

View file

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

View file

@ -23,12 +23,29 @@ module Ensl
# Custom directories with classes and modules you want to be autoloadable. # Custom directories with classes and modules you want to be autoloadable.
config.autoload_paths += Dir["#{config.root}/app/services/**/", "#{config.root}/app/models/concerns/"] 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 # Load secrets from .env
ENV['APP_SECRET'] ||= (0...32).map { (65 + rand(26)).chr }.join ENV['APP_SECRET'] ||= (0...32).map { (65 + rand(26)).chr }.join
config.secret_token = ENV['APP_SECRET'] config.secret_token = ENV['APP_SECRET']
# Use cookies # Use a different cache store
config.session_store :cookie_store, key: '_ENSL_session_key', expire_after: 30.days.to_i 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. # 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. # 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 # Enable the asset pipeline
config.assets.enabled = true 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 # il8n fix
config.i18n.fallbacks = true config.i18n.fallbacks = true
config.i18n.enforce_available_locales = false 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 # Tiny mce
config.tinymce.install = :copy 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
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? if Rails.env.production?
%w(403 404 422 500).each do |code| %w(403 404 422 500).each do |code|
get code, to: "errors#show", code: code get code, to: "errors#show", code: code
@ -71,6 +71,7 @@ Ensl::Application.routes.draw do
post 'forgot' post 'forgot'
end end
end end
post 'auth/:provider/callback', to: 'users#callback'
resources :locks resources :locks
resources :contesters 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