I'm trying to implement a 3 legged authentication in my app to the Google API, to be able to access registered users' Google Calendars.
In the quickstart Ruby guide, this command comes up that as far as I understand should point to the user's tokens:
token_store = Google::Auth::Stores::FileTokenStore.new(file: CREDENTIALS_PATH)
It expects the tokens stored in a file (or Redis), but (of course) I store each user's tokens in my database (Postgres).
Have I understand the purpose of the command wrongly or otherwise - how do I use it with a database store?
Official documentation
I implemented it myself based on #Rafe's answer. Just wanted to share in case someone wants to copy the ActiveRecord / Database store implementation:
module Google
module Auth
module Stores
class DatabaseTokenStore < Google::Auth::TokenStore
def load(id)
user = User.find(id)
{
"client_id": ENV['google_client_id'],
"access_token": user.token,
"refresh_token": user.refresh_token,
"scope": ENV['google_scopes'],
"expiration_time_millis": user.token_expires_at
}.to_json
end
def store(id, token)
user = User.find(id)
hsh = JSON.parse(token)
user.update(
token: hsh["access_token"],
token_expires_at: hsh["expiration_time_millis"] / 1000
)
end
end
end
end
end
Implement it yourself, according to the the readme:
Custom storage implementations can also be used. See token_store.rb for additional details.
It shouldn't be too hard to implement the load(id), store(id, token) and delete(id) with ActiveRecord (or another ORM) by the looks of the mentioned files.
Accepted answer above it's good and I recommend it https://stackoverflow.com/a/48267763/473040, but I find it useful to store everything for future debugging. Also simplicity is beautiful :)
Add json column to Postgres DB table (or serialise text field in other db)
class AddGooglePhotosTokensToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :google_photo_tokens, :json
end
end
class:
class GoogleAuthDbStore < Google::Auth::TokenStore
def load(id)
user = User.find(id)
user.google_photo_tokens
end
def store(id, tokens)
user = User.find(id)
user.google_photo_tokens = tokens
user.save!
end
def delete id
user = User.find(id)
user.google_photo_tokens = nil
user.save!
end
end
use
def authorizer
return #authorizer if #authorizer
client = Google::Auth::ClientId.new(Rails.application.credentials.google.fetch(:client_id), Rails.application.credentials.google.fetch(:client_secret))
scope = ['https://www.googleapis.com/auth/photoslibrary.readonly']
token_store = Provider::GoogleAuthDbStore.new # <<<here
#authorizer = Google::Auth::WebUserAuthorizer.new(client, scope, token_store, '/auth/google/callback')
#authorizer
end
Related
So, I'm new on a Rails 4 project and I must create a new way to authenticate users by API Keys. I need to generate them and then it must be shown only once to the user, and when it is saved in the Database, must be an encrypted value for security purposes.
This is my current class:
class ApiKey < ActiveRecord::Base
# Hexdigest the token's HMAC digest
HMAC_SECRET_KEY = Figaro.env.secret_key_base!
# Validate the token's HMAC digest
attr_accessor :token
#Relationships
belongs_to :bearer, polymorphic: true
#Callbacks
before_create :remove_previous_api_key_and_return_warning, :create_digested_api_key,
private
def remove_existing_api_key
# we need to remove all the existing api keys for the user before we create a new one.
#user = Aim::User.find_by(id: bearer_id)
#user.api_keys.destroy_all
end
def api_key_warning
# this method must return a string containing a warning message that will be shown to the user right after creating the API key
puts 'Please, save your just created API Key, it will not be shown again.'
end
def create_original_api_key
# this method must return a string containing the original API key that will be used to authenticate the user
token = SecureRandom.hex(32)
token
end
def remove_previous_api_key_and_return_warning
remove_existing_api_key
api_key_warning
end
def create_digested_api_key
self.token = create_original_api_key
raise ActiveRecord::RecordInvalid.new(self.token), 'token is required' unless token.present?
self.token_digest = OpenSSL::HMAC.hexdigest('SHA256', HMAC_SECRET_KEY, token)
digest = OpenSSL::HMAC.hexdigest('SHA256', HMAC_SECRET_KEY, token)
self.token_digest = digest
end
end
The data saved will be the result of the create_digested_api_key method and that is not intended. I tried using callbacks to return the original key before saving and inserting the digested key after saving, but despite it seems to work in the console, the original value is the saved one.
This will not have a controller to make crud operations in, the created API keys here, will be used to authenticate directly with the controller that handles that matter directly, so I could not generate the key in a create method inside the controller and return it from there. Any thoughts?
I have create several applications that communicate with our central auth server via doorkeeper. I want to make some applications accessible/inaccessible for specific users.
Is there a way to restrict access to specific oauth_applications and return a 401?
I believe the easiest way to achieve this would be the following:
In your doorkeeper application, change the Users table to include a permissions relationship. Something like, User -> has many -> permissions
And those permissions could contain just the name of the application you want to give them access to, (Or the ID of the application, you choose)
Then, in your config/initializer/doorkeeper.rb - inside Doorkeeper::JWT.configure - you add which applications that particular user can access inside the token payload, something like:
token_payload do |opts|
...
token[:permissions] = user.permissions.pluck(:application_name)
end
If you are using Doorkeeper without JWT, you can still pass extra information to the token by prepending a custom response to the ResponseToken object like so:
Doorkeeper::OAuth::TokenResponse.send :prepend, CustomTokenResponse
and CustomTokenResponse just need to implement the methods body, like so:
module CustomTokenResponse
def body
additional_data = {
'username' => env[:clearance].current_user.username,
'userid' => #token.resource_owner_id # you have an access to the #token object
# any other data
}
# call original `#body` method and merge its result with the additional data hash
super.merge(additional_data)
end
end
extra information can be found in Doorkeepers' wiki: https://github.com/doorkeeper-gem/doorkeeper/wiki/Customizing-Token-Response
and in the Doorkeeper JWT gem: https://github.com/doorkeeper-gem/doorkeeper-jwt#usage
On 9 Feb 2020 a new configuration option was introduced in Doorkeeper to exactly do this.
Therefore, you can configure config/initializer/doorkeeper.rb:
authorize_resource_owner_for_client do |client, resource_owner|
resource_owner.admin? || client.owners_whitelist.include?(resource_owner)
end
I wanted the same behaviour. I use the resource_owner_authenticator block in config/initializer/doorkeeper.rb. When a user has one or more groups which are connected with an Oauth application it can continue.
rails g model UserGroup user:references group:references
rails g model GroupApplications group:references oauth_application:references
resource_owner_authenticator do
app = OauthApplication.find_by(uid: request.query_parameters['client_id'])
user_id = session["warden.user.user.key"][0][0] rescue nil
user = User.find_by_id(user_id)
if !app && user
user
elsif app && user
if !(user.groups & app.groups).empty?
user
else
redirect_to main_app.root_url, notice: "You are not authorized to access this application."
end
else
begin
session['user_return_to'] = request.url
redirect_to(new_user_session_url)
end
end
end
My app is using Rails 3.0.4 and Devise 1.1.7.
I'm looking for a way to prevent users from sharing accounts as the app is a subscription based service. I've been searching for over a week, and I still don't know how to implement a solution. I'm hoping someone has implemented a solution and can point me in the right direction.
Solution (Thank you everyone for your answers and insight!)
In application controller.rb
before_filter :check_concurrent_session
def check_concurrent_session
if is_already_logged_in?
sign_out_and_redirect(current_user)
end
end
def is_already_logged_in?
current_user && !(session[:token] == current_user.login_token)
end
In session_controller that overrides Devise Sessions controller:
skip_before_filter :check_concurrent_session
def create
super
set_login_token
end
private
def set_login_token
token = Devise.friendly_token
session[:token] = token
current_user.login_token = token
current_user.save
end
In migration AddLoginTokenToUsers
def self.up
change_table "users" do |t|
t.string "login_token"
end
end
def self.down
change_table "users" do |t|
t.remove "login_token"
end
end
This gem works well: https://github.com/devise-security/devise-security
Add to Gemfile
gem 'devise-security'
after bundle install
rails generate devise_security:install
Then run
rails g migration AddSessionLimitableToUsers unique_session_id
Edit the migration file
class AddSessionLimitableToUsers < ActiveRecord::Migration
def change
add_column :users, :unique_session_id, :string, limit: 20
end
end
Then run
rake db:migrate
Edit your app/models/user.rb file
class User < ActiveRecord::Base
devise :session_limitable # other devise options
... rest of file ...
end
Done. Now logging in from another browser will kill any previous sessions. The gem actual notifies the user that he is about to kill a current session before logging in.
You can't do it.
You can control IP addresses of user, so you can prevent presence of user from two IP at a time. ANd you can bind login and IP. You can try to check cities and other geolocation data through IP to block user.
You can set cookies to control something else.
But none of this will guarantee that only one user uses this login, and that those 105 IP from all over the world doesn't belong to only one unique user, which uses Proxy or whatever.
And the last: you never need this in the Internet.
UPD
However, what I'm asking is about limiting multiple users from using the same account simultaneously which I feel should be possible
So you can store some token, that will contain some encrypted data: IP + secret string + user agent + user browser version + user OS + any other personal info: encrypt(IP + "some secret string" + request.user_agent + ...). And then you can set a session or cookie with that token. And with each request you can fetch it: if user is the same? Is he using the same browser and the same browser version from the same OS etc.
Also you can use dynamic tokens: you change token each request, so only one user could use system per session, because each request token will be changed, another user will be logged out as far as his token will be expired.
This is how I solved the duplicate session problem.
routes.rb
devise_for :users, :controllers => { :sessions => "my_sessions" }
my_sessions controller
class MySessionsController < Devise::SessionsController
skip_before_filter :check_concurrent_session
def create
super
set_login_token
end
private
def set_login_token
token = Devise.friendly_token
session[:token] = token
current_user.login_token = token
current_user.save(validate: false)
end
end
application_controller
def check_concurrent_session
if duplicate_session?
sign_out_and_redirect(current_user)
flash[:notice] = "Duplicate Login Detected"
end
end
def duplicate_session?
user_signed_in? && (current_user.login_token != session[:token])
end
User model
Add a string field via a migration named login_token
This overrides the default Devise Session controller but inherits from it as well. On a new session a login session token is created and stored in login_token on the User model. In the application controller we call check_concurrent_session which signs out and redirects the current_user after calling the duplicate_session? function.
It's not the cleanest way to go about it, but it definitely works.
As far as actually implementing it in Devise, add this to your User.rb model.
Something like this will log them out automatically (untested).
def token_valid?
# Use fl00rs method of setting the token
session[:token] == cookies[:token]
end
## Monkey Patch Devise methods ##
def active_for_authentication?
super && token_valid?
end
def inactive_message
token_valid? ? super : "You are sharing your account."
end
I found that the solution in the original posting did not quite work for me. I wanted the first user to be logged out and a log-in page presented. Also, the sign_out_and_redirect(current_user) method does not seem to work the way I would expect. Using the SessionsController override in that solution I modified it to use websockets as follows:
def create
super
force_logout
end
private
def force_logout
logout_subscribe_address = "signout_subscribe_response_#{current_user[:id]}"
logout_subscribe_resp = {:message => "#{logout_subscribe_address }: #{current_user[:email]} signed out."}
WebsocketRails[:signout_subscribe].trigger(signout_subscribe_address, signout_subscribe_resp)
end
end
Make sure that all web pages subscribe to the signout channel and bind it to the same logout_subscribe_address action. In my application, each page also has a 'sign out' button, which signs out the client via the devise session Destroy action. When the websocket response is triggered in the web page, it simply clicks this button - the signout logic is invoked and the first user is presented with the sign in page.
This solution also does not require the skip_before_filter :check_concurrent_session and the model login_token since it triggers the forced logout without prejudice.
For the record, the devise_security_extension appears to provide the functionality to do this as well. It also puts up an appropriate alert warning the first user about what has happened (I haven't figured out how to do that yet).
Keep track of uniq IPs used per user. Now and then, run an analysis on those IPs - sharing would be obvious if a single account has simultaneous logins from different ISPs in different countries. Note that simply having a different IP is not sufficient grounds to consider it shared - some ISPs use round-robin proxies, so each hit would necessarily be a different IP.
While you can't reliably prevent users from sharing an account, what you can do (I think) is prevent more than one user being logged on at the same time to the same account. Not sure if this is sufficient for your business model, but it does get around a lot of the problems discussed in the other answers. I've implemented something that is currently in beta and seems to work reasonably well - there are some notes here
I am trying to convert my custom simple auth system in my rails app to use AuthLogic. I have managed to get everything working fairly easily, but now when I try to login it will not properly validate my credentials. The pertinent code is below:
# app/models/profile.rb
class Profile < ActiveRecord::Base
acts_as_authentic do |c|
c.transition_from_crypto_providers = Authlogic::CryptoProviders::Sha1,
c.crypto_provider = Authlogic::CryptoProviders::Sha512
end
end
I used to use this to hash my password on creation:
# app/models/profile.rb
def hash_password
self.salt = ActiveSupport::SecureRandom.base64(8)
self.hashed_password = Digest:SHA1.hexdigest(self.salt + #password)
end
I have already converted all the necessary table columns to be compatible with AuthLogic.
Is there a way to log what AuthLogic is hashing the password as? What else could be wrong?
I solved my problem, I had to write a custom crypto_provider, looked like this for anyone who is curious:
class MyCryptoProvider
# Turns your raw password into a Sha1 hash.
def self.encrypt(*tokens)
tokens = tokens.flatten
tokens = tokens.reverse
digest = Digest::SHA1.hexdigest([*tokens].join)
digest
end
# Does the crypted password match the tokens? Uses the same tokens that were used to encrypt.
def self.matches?(crypted, *tokens)
encrypt(*tokens) == crypted
end
end
I am in my first steps of implementing OpenID in my Rails app.
open_id_authentication appeared to be a fairly easy-to-use plugin, which is why I decided to use it.
Logging in with my Google account seems to work perfectly, however I do not get the sreg/AX fields that I require.
My code is currently as follows:
class SessionsController < ApplicationController
def new; end
def create
open_id_authentication
end
protected
def open_id_authentication
authenticate_with_open_id(params[:openid_identifier], :required => ["http://axschema.org/contact/email"]) do |result, identity_url, registration|
if result.successful?
p registration.data
#current_user = User.find_by_identity_url(identity_url)
if #current_user
successful_login
else
failed_login "Sorry, no user by that identity URL exists (#{identity_url})"
end
else
failed_login result.message
end
end
end
private
def successful_login
session[:user_id] = #current_user.id
redirect_to(root_url)
end
def failed_login(message)
flash[:error] = message
redirect_to(new_session_url)
end
end
I have already read various discussions about Google OpenID and all only say that you need to require the AX schema instead of the sreg field email, but even when I am doing so (as you can see in the code above), registration.data will remain empty ({}).
How do I effectively require the email from most OpenID providers with open_id_authentication?
The authenticate_with_open_id return the Sreg object, not the AX response. So you need instanciate this respone with Rack::OpenID::REPONSE like that :
ax_response = OpenID::AX::FetchResponse.from_success_response(request.env[Rack::OpenID::RESPONSE])
After you can fetch your data
ax_response['http://axschema.org/contact/email']
ax_response['http://axschema.org/namePerson/first']
ax_response['http://axschema.org/namePerson/last']
I've also stitched together a complete solution to Ruby on Rails 3, OpenID, and Google: http://blog.sethladd.com/2010/09/ruby-rails-openid-and-google.html
this post contains a good strategy to use AX for google and Sreg for others, to make this happen a little more seamlessly
http://www.franzens.org/2009/01/using-google-federated-login-in-your.html