Skip to content

Conversation

@ruslankhaertdinov
Copy link

@ruslankhaertdinov
Copy link
Author

@timurvafin @MaximLarionov take a look please)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retrieve providers list from OmniAuth.strategies

@ruslankhaertdinov ruslankhaertdinov force-pushed the authentication-via-social-networks branch 2 times, most recently from 2837724 to 01814ea Compare February 1, 2016 19:41
@maxinspace
Copy link

👌 good work on this

class UsersController < ApplicationController
before_action :authenticate_user!, only: :home

expose(:user, attributes: :user_params)
Copy link
Author

@ruslankhaertdinov ruslankhaertdinov Apr 29, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MaximLarionov Main issue that we pass direct id into url. In this case user can manually change id param and update foreign users email and password attributes.
What do you think, maybe we should pass some token (e.g. already generated confirmation_token or something else) instead of id?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ruslankhaertdinov right, I guess we can use devise generated tokens for this

@ruslankhaertdinov
Copy link
Author

ruslankhaertdinov commented May 1, 2016

@MaximLarionov And another one question: When Oauth confirmed, when user not found by UID or email, we don't create an identity, could you please clarify why (don't create)?

@ruslankhaertdinov ruslankhaertdinov changed the title Authentication via social networks [WIP] Authentication via social networks May 3, 2016
@maxinspace
Copy link

@ruslankhaertdinov we actually should create both User and identity, if Oauth is confirmed, and user is not found. Moreover, User account should be already confirmed after creation in this case.

@ruslankhaertdinov
Copy link
Author

Ok, thanks. I'll update PR accordingly.

@ruslankhaertdinov
Copy link
Author

@MaximLarionov This is updated part of Schema. Could you please confirm that it is correct?

omniauth schema png 2016-05-26 22-58-28

@maxinspace
Copy link

@ruslankhaertdinov yup, it's correct.
Since OAuth is confirmed, we can treat this email as also confirmed.
And sine User haven't been found, we should create new User for this email.
Although, before sign in, User should enter his new password at finish signup page.

Everything else seems good.

p.s. - wdyt, maybe we shouldn't allow unconfirmed OAuth to be used anywhere?
It's a really massive piece of code, and probability of it's actual usage is quite low.

@ruslankhaertdinov
Copy link
Author

@MaximLarionov Agreed. Handling unconfirmed OAuth is very complex and hard to understand. Lets try to simplify this case and only show alert message for the user (with unconfirmed oauth).
Another one question: why user with confirmed oauth (that is not in the database yet) should enter his password via finish signup page? May be we should skip this step?

@maxinspace
Copy link

maxinspace commented May 27, 2016

@ruslankhaertdinov In case of creating "totally new" user from confirmed OAuth, his password at the moment of creating is just a random set of symbols, because Devise won't allow us to create user with empty password. And user doesn't know this password.

I see 2 ways of working with this case:

  1. After signing in, redirect user to page, where he can change his password.
  2. Send an email to user, with link to "change his forgotten password"

First is more user-friendly, second is much more simple.

@ruslankhaertdinov
Copy link
Author

ruslankhaertdinov commented May 27, 2016

@MaximLarionov In case when Devise configured to prompt current password on password changing, I think sending "change forgotten password" email is more universal approach. What do you think?

@maxinspace
Copy link

@ruslankhaertdinov Yup, this seems to be more universal, guess that will work 👍

@ruslankhaertdinov
Copy link
Author

Thank you!))

@ruslankhaertdinov
Copy link
Author

omniauth schema simplified

@ruslankhaertdinov
Copy link
Author

@MaximLarionov Может "Change password email" вообще не нужно отправлять? Ведь пользователь всегда может нажать ссылку "Forgot password" и получить инструкцию на почту?

@maxinspace
Copy link

@ruslankhaertdinov по сути, можно и так сделать.
Просто, как юзер, я не особо люблю делать лишние движения, чтобы поставить пароль себе.
Вопрос User Experience по сути, можно change password email вообще не отправлять. Наверное, для auth_examples хватить и просто ссылки Forgot password.

p.s. Диаграмма норм 👍

@ruslankhaertdinov ruslankhaertdinov force-pushed the authentication-via-social-networks branch from a57c899 to 8367272 Compare June 1, 2016 15:23
@ruslankhaertdinov
Copy link
Author

@MaximLarionov переделал под новую схему, посмотри как будет время, пожалуйста ;)

@ruslankhaertdinov ruslankhaertdinov force-pushed the authentication-via-social-networks branch from 8367272 to 78f3ef7 Compare June 1, 2016 16:34
@maxinspace
Copy link

maxinspace commented Jun 2, 2016

@ruslankhaertdinov еще пара мест и и все классно)

include OmniauthHelper

Identity::PROVIDERS.each do |provider|
define_method(provider.to_s) do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String-cast is unnessesary

define_method provider do

@ruslankhaertdinov
Copy link
Author

@MaximLarionov исправил)

@maxinspace
Copy link

@ruslankhaertdinov теперь все классно)

@maxinspace
Copy link

@ruslankhaertdinov let's fix conflicts, I guess it's already time to merge it

@ruslankhaertdinov
Copy link
Author

@MaximLarionov sure, I'll do it today

class ConnectIdentity
attr_reader :user, :auth
private :user, :auth

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems we should include Interactor here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

end

def update_identity
identity.update_attribute(:user, user)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you use #update_attribute here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I should bethink whole logic here)


Identity::PROVIDERS.each do |provider|
define_method(provider) do
show_verification_notice and return unless auth_verified?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason to write all this logic in one line? It's so difficult to understand (at least for me) what is going on here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this one is not simple. Let me to refactor it.


def destroy
action = indentity.destroy ? :notice : :alert
flash[action] = t "flash.actions.destroy.#{action}", resource_name: Identity.model_name.human

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use flash responder here instead of manually set flash message?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll revise it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (this is my own reminder)

end

def create_identity
user.identities.create!(provider: auth.provider, uid: auth.uid)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should handle result of this operation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please tell me why should we handle the result?

end

def call
user = User.new(user_params)
Copy link

@KirillKayumov KirillKayumov Oct 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

def call
  User.create!(user_params)
end

def user_params
  {
    email: auth.info.email,
    full_name: auth.info.name,
    password: Devise.friendly_token.first(8),
    confirmed_at: Time.current
  }
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also auth hashes for each provider are different. I suggest to have parsers for each provider to properly get data from auth

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For given providers seems like it works well. May be add parser only when it necessary?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KirillKayumov I believe you've had an example of implemented and well-structured architecture, which provides parsers for corresponding providers.
Could you please share a link? :)

From what I remember, that solution was quite good and worthy to implement here

end

def user_found_by_email
FindUserByEmail.new(auth).call

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and in #new_user context of interactor is returned. I guess we should return a user.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are POROs which return user instance. I'll redo it when implement them as interactors.

def call
return unless user

create_identity
Copy link

@KirillKayumov KirillKayumov Oct 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interactor is called FindUserByEmail. But it does additional things, such as Identity creating and user confirming. Seems it should be converted to an organizer.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

```ruby
# app/models/identity.rb
class Identity < ActiveRecord::Base
PROVIDERS = OmniAuth.strategies.map { |s| s.to_s.demodulize.underscore }.drop(1)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should do such complex converting of strategies. Instead let's explicitly specify list of available providers.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how exactly it should be. Current case not so complex, I guess. @MaximLarionov what do you think?)

Copy link

@maxinspace maxinspace Oct 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guess that the best way would be, if oauth gem could fetch all conventional names of used strategies simple way.

If gem can't do that, guess we'll have to go with explicit way, especially if strategies naming doesn't follow some conventions(could possibly lead to name errors).


```ruby
# app/views/identities/_provider_link.html.slim
= link_to "#{provider_name(identity.provider)} (#{identity.uid.truncate(9)}). Unauthorize?",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to include uid in link text?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only for demo cases when we have multiple oauth accounts with 1 provider (e.g. Facebook)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering if it's a good idea - to attach multiple identities from single kind of provider, to one account?

It feels to be an excessive feature, actually :)

Also, bare text in views should be preferably replaced with I18n translations

```ruby
# config/initializers/devise.rb
...
config.omniauth :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_CLIENT_SECRET"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use #fetch to get env variables

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point

class CreateIdentities < ActiveRecord::Migration
def change
create_table :identities do |t|
t.belongs_to :user, index: true
Copy link

@KirillKayumov KirillKayumov Oct 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need a foreign key here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

def change
create_table :identities do |t|
t.belongs_to :user, index: true
t.string :provider, index: true, null: false, default: ""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Value of provider and uid columns should never be empty string. Let's get rid of default values for these columns.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right

```ruby
# spec/factories/users.rb
...
trait :from_auth_hashie do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hashie? 😃

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep 🚬

end

def click_connect_fb
login_as(user, scope: :user)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper does SO MUCH. Can we move login and visit to background?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

let(:user) { create(:user, email: auth_hashie.info.email, confirmed_at: nil) }

it "confirms user" do
expect(user.confirmed?).to be_falsey

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use expect {}.to change {} here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess so


it "raises Exception" do
expect { subject }
.to raise_error(AuthVerificationPolicy::OauthError, I18n.t("omniauth.verification.not_implemented", kind: provider))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too loong

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes... need to refactor it

Copy link
Author

@ruslankhaertdinov ruslankhaertdinov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for detailed review 👍


def destroy
action = indentity.destroy ? :notice : :alert
flash[action] = t "flash.actions.destroy.#{action}", resource_name: Identity.model_name.human
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll revise it.


Identity::PROVIDERS.each do |provider|
define_method(provider) do
show_verification_notice and return unless auth_verified?
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this one is not simple. Let me to refactor it.

class ConnectIdentity
attr_reader :user, :auth
private :user, :auth

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

end

def create_identity
user.identities.create!(provider: auth.provider, uid: auth.uid)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please tell me why should we handle the result?

end

def call
user = User.new(user_params)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For given providers seems like it works well. May be add parser only when it necessary?

def change
create_table :identities do |t|
t.belongs_to :user, index: true
t.string :provider, index: true, null: false, default: ""
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right

```ruby
# spec/factories/users.rb
...
trait :from_auth_hashie do
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep 🚬

end

def click_connect_fb
login_as(user, scope: :user)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

let(:user) { create(:user, email: auth_hashie.info.email, confirmed_at: nil) }

it "confirms user" do
expect(user.confirmed?).to be_falsey
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess so


it "raises Exception" do
expect { subject }
.to raise_error(AuthVerificationPolicy::OauthError, I18n.t("omniauth.verification.not_implemented", kind: provider))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes... need to refactor it


private

def show_verification_notice

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be named as show_verification_error, since you actually return an error notice

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@ruslankhaertdinov
Copy link
Author

@MaximLarionov @KirillKayumov there are no enough time to apply all comments rapidly, so I'll refactor app step by step, thanks advance for understanding.

@maxinspace
Copy link

@ruslankhaertdinov sure, that's not a rush, or something, take your time surely. Also, you might add me as a collaborator to your fork, so I can do some pull-requests straight to your fork with fixes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants