socialcast / devise_oauth2_providable

Rails3 engine integrating OAuth2 authentication with Devise
MIT License
219 stars 102 forks source link

Help: Wanting to do login post-password-reset #48

Open adamburmister opened 11 years ago

adamburmister commented 11 years ago

Hi, sorry to file this under issues, but I thought this might be a common enough question.

I'm attempting to do an auto-login after password-reset within devise's passwords controller. I can't figure out how to do this, and thought perhaps you could suggest an approach.

The snippet looks a lot like the standard passwords controller, but I'm returning a JSON only response.

  # PUT /reset
  def update
    self.resource = resource_class.reset_password_by_token(resource_params)

    if resource.errors.empty?
      resource.unlock_access! if unlockable?(resource)
      message = resource.active_for_authentication? ? :updated : :updated_not_active
      sign_in(resource_name, resource)

      # ***TODO: Do an OAuth2 login here***

      render :json => { success: true, message: message, auth: @access_tokens }
    else
      render :json => { success: false, errors: resource.errors }, :status => :bad_request
    end
  end

Any help would be much appreciated

goofrider commented 11 years ago

Hmmm, not that I use this module, but I know a tiny bit about Devise and Oauth in general. Your scenario seems odd though:

If you're happy with your current implementation and just want to create the OAuth2 token for the user, you can try to find the code you need in here (I picked out a few relevant lines).

https://github.com/socialcast/devise_oauth2_providable/blob/master/app/controllers/devise/oauth2_providable/authorizations_controller.rb

@client = Devise::Oauth2Providable::Client.find_by_identifier(req.client_id) || req.bad_request!

when :code
            authorization_code = current_user.authorization_codes.create!(:client => @client)
            res.code = authorization_code.token
when :token
            access_token = current_user.access_tokens.create!(:client => @client).token
            bearer_token = Rack::OAuth2::AccessToken::Bearer.new(:access_token => access_token)
adamburmister commented 11 years ago

Thanks for the reply.

No, my scenario is pretty much the standard flow for Devise password reset.

From a UI the user uses a forgot form to post a request for a password reset link The UI posts to the API (without auth), which sends the link - BUT instead of rendering an HTML view it renders a JSON response The user clicks the reset link in their email which goes to the UI The UI posts to the API the reset token and new password (without auth)

At this point I want to differ from the normal flow. The password is now reset, and I want to render a JSON response (no cookies) which contains tokens for a new session so the user can use them straight away, rather than having to go through the UI's login step.

This sounds reasonable and as-secure to me. I'd be interested to hear if you disagree.

I think with your code snippet below though I should be able to achieve what I'm after.

Many thanks Adam

On 16 Feb 2013, at 10:27, goofrider notifications@github.com wrote:

Hmmm, not that I use this module, but I know a tiny bit about Devise and Oauth in general. Your scenario seems odd though:

Why would you want to let Oauth2 clients (which are supposed to be untrusted) reset user passwords by API calls?

Why would you want to make PasswordController into an API? It's pretty specific to this multiple-step web->email->web password reset flow. How is it gonna get the one-time token embedded in the password reset link?

Wouldn't you still want to send them the password reset link via email for security reason? So why not just let them use the normal web flow?

Users can still login to the OAuth2 client app if they forgot the password as long as the OAuth2 access token hasn't expired. You can just allow them to change the password directly if they can login to the mobile app. It's actually more secure than submit the password reset via an open API call not protected by Oauth2

Can't you leverage the built-in auto-login in PasswordController and just follow the normal OAuth flow after that? Let your OAuth client open a web browser popup for the password reset (using the normal web form), once its completed it should save a session cookie. The session cookie should keep the user logged in so that they don't have to log in again during OAuth authorize/token request phase.

If you're happy with your current implementation and just want to create the OAuth2 token for the user, you can try to find the code you need in here (I picked out a few relevant lines).

https://github.com/socialcast/devise_oauth2_providable/blob/master/app/controllers/devise/oauth2_providable/authorizations_controller.rb

@client = Devise::Oauth2Providable::Client.find_by_identifier(req.client_id) || req.bad_request!

when :code authorization_code = current_user.authorization_codes.create!(:client => @client) res.code = authorization_code.token when :token access_token = current_user.access_tokens.create!(:client => @client).token bearer_token = Rack::OAuth2::AccessToken::Bearer.new(:access_token => access_token) — Reply to this email directly or view it on GitHub.

goofrider commented 11 years ago

Hi Adam,

Thanks for the clarification. I thought about your scenarios and also looked into the varies modules' source code looking for some ideas.

From the sound of it, you're asking for the user's username and password from your UI and then login by calling the /sign_in route using JSON API, aren't you? I'm pretty sure you're not supposed to ask for the user password directly in an Oauth2 client. From the client, you're supposed to redirect the user to the OAuth authorization endpoint (/oauth2/authoize) on the Oauth provider's website, only then the user enter the username/password directly to the provider's login dialog, afterwards the provider redirect to the callback URL on the client side with the access token, the token it returns encapsulate the uid/auth combo. The username and password is never revealed to the Oauth client. That's how OAuth supposed to work.

Are you using an Oauth client gem/library on the client app? If you use a pre-baked Oauth library, this should all be very obvious to you, as those libraries never ask for uid/pwd. All the redirections is handled for you. We need to get you back on the standard Oauth flow first, because all the underlying layers depend on it.

Here's the normal Oauth flow:

There's also a lot more to this though when we add password reset to the equation. I was trying to look into the codebase and figure out how to get a completely in-app password reset flow, and found a few places in the code where they might make that kind of flow impossible. But that reply is really long and need some major editing

I really don't have all the answers. Maybe someone more familiar with Devise, Oauth2Proviable and the underlying layers can explain better than I do. But I'll post that part of the reply later after some cleanup, hopefully it'll at least point you in the right direction.

goofrider commented 11 years ago

OK, in order to satisfy my curiousity, I rolled a test Oauth2 client/server setup with devise_oauth2_providable using this test suite (with caveat):

https://github.com/ZenCocoon/testoauth2provider https://github.com/ZenCocoon/testoauth2client

Here's how the normal Oauth login flow is (without password reset):

[client/server] indicates what's directly facing the user at that stage

  1. [client] user click login button in your app (mobile app, webapp, doesn't matter)
  2. [client] Your app calls /oauth2/authorize in the background and redirect you to the server's website
  3. [server] server presents web-based login dialog, user enter uid/pwd
  4. [server] server presents web-based oauth authorize dialog (with that "authorize button). user clicks it
  5. [client] server rediects back to client's callback URL

As long as you follow the normal Oauth flow, the password reset process will just become a longer version of step 3, and the user should be redirected back to your app, theoretically. I went ahead and did some real testing and see what really happens during this password reset process:

A. Start the OAuth flow from the client, it redirects me to the server, I click on password reset. Then I paste the password reset link in the same window the client initiated, change the password, hit "authorize", and got redirected back to the app nicely.

B. Do the same thing to initiate OAuth from the client side and click password reset, but this time, I open a new browser tab to paste in the password reset link. After the password reset I was logged in at the server but didn't get redirect to the client app. Furthermore, when I go back to the client app and try to sign-in via OAuth, I still have to enter username and password, even though the server thinks I'm still logged in in the other browser tab.

So there you have it. If you get your client app's oauth process back to spec, and somehow keep the password reset in the client-initiated window, the user will get redirect back to the client app. Hope that's enough for you to start with. Looks to me there's some pretty good browser session isolation implemented here, I must say.

EDIT: I should note that testoauth2client didn't work with testoauth2provider out of the box, turns out testoauth2client's Oauth initiation has a params[:state] by default and check if the server response matches (it's for CRSF protection). testoauth2provider didn't have that enabled, and raised a non-descriptive CallbackError on the client side that took me forever to track down. In the test above I just set :provider_ignores_state to TRUE in testoauth2client. Not sure if the whole password reset process will break the Oauth params[:state] checking. You should try it out yourself to be sure.