Forgot password flow
I'm trying to set up the "forgot password flow". In my Rails 5 backend I'm using devise_token_auth (0.1.40).
I set the frontend up as in the documentation, meaning when an user requests a new password, I call:
this._tokenService.resetPassword({
email: this.resetPasswordForm.value.email,
});
This works as intended, and the backend sends an e-mail to the user with a token.
However, when the user clicks the link, and the password reset form is I call this:
this._tokenService.updatePassword({
password: this.resetPasswordForm.value.password,
passwordConfirmation: this.resetPasswordForm.value.password_confirmation,
passwordCurrent: null,
resetPasswordToken: this.resetPasswordForm.value.password_token
});
I now see a call happening to /auth with the provided data which fails with error "User not found". In my Rails log, I see this:
| Started PUT "//auth" for ::1 at 2017-04-02 14:23:03 +0200 | Processing by DeviseTokenAuth::RegistrationsController#update as JSON | Parameters: {"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "reset_password_token"=>"[FILTERED]", "registration"=>{"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "reset_password_token"=>"[FILTERED]"}} | Unpermitted parameters: reset_password_token, registration
It seems that the password, password_confirmation and reset_password_token are being sent, and somewhere being re-routed/copied into registration: { password, password_confirmation, reset_password_token}.
I've looked into the source code of devise_token_auth, and noticed that the flow of reset password might be different than you expect. It seems you need an extra call, to allow a user-object to change the password once without a "current password". The 'def edit' in the PasswordController of devise_token_auth states:
this is where users arrive after visiting the password reset confirmation link
So, it seems like when you open the form, you have to make a call to tell the backend an user has visited the form with the reset-token. https://github.com/lynndylanhurley/devise_token_auth/blob/master/app/controllers/devise_token_auth/passwords_controller.rb#L94 There it seems like the user-permissions is set to change the password once, basically setting up the User model to expect an incoming password change.
Any ideas? If you require additional information, I'd be happy to help.
Thanks for the explanation @cybey, were you ever able to get this working?
I'm trying to generate a custom link to allow users to 'set' their passwords initially after our admins create them from the backend. I've got angular2-token set up to call the backend using:
this._tokenService.updatePassword({
password: this.resetPasswordForm.value.password,
passwordConfirmation: this.resetPasswordForm.value.password_confirmation,
passwordCurrent: null,
resetPasswordToken: this.resetPasswordForm.value.password_token
});
Then I get the same output on the server that you're getting:
Started PUT "/auth" for ::1 at 2017-04-30 22:46:24 -0400
Processing by DeviseTokenAuth::RegistrationsController#update as JSON
Parameters: {"current_password"=>"[FILTERED]", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "reset_password_token"=>"[FILTERED]", "registration"=>{"current_password"=>"[FILTERED]", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "reset_password_token"=>"[FILTERED]"}}
Unpermitted parameters: reset_password_token, registration
Completed 404 Not Found in 1ms (Views: 0.3ms | ActiveRecord: 0.0ms)
What was the configuration change you made to devise_token_auth to get this working?
Thanks!
I'm also using the correct passwordCurrent value in the above example, although ideally I wouldn't need one at all
I got it working through some hacks.
In devise_token_auth I made an override for PasswordsController:
module Overrides
class PasswordsController < DeviseTokenAuth::PasswordsController
def edit
@resource = resource_class.reset_password_by_token({
reset_password_token: resource_params[:reset_password_token]
})
if @resource && @resource.id
client_id = SecureRandom.urlsafe_base64(nil, false)
token = SecureRandom.urlsafe_base64(nil, false)
token_hash = BCrypt::Password.create(token)
expiry = (Time.now + DeviseTokenAuth.token_lifespan).to_i
@resource.tokens[client_id] = {
token: token_hash,
expiry: expiry
}
# ensure that user is confirmed
@resource.skip_confirmation! if @resource.devise_modules.include?(:confirmable) && [email protected]_at
# allow user to change password once without current_password
@resource.allow_password_change = true;
@resource.save!
yield @resource if block_given?
render json: {
token: token,
client_id: client_id,
expiry: expiry,
uid: @resource.uid,
}
else
render_edit_error
end
end
end
end
In my frontend, I have a component where users can reset their password and added this in the ngOnInit:
ngOnInit() {
this._route.queryParams.subscribe((params: Params) => {
let token = params['reset_password_token'];
let options = { search: { reset_password_token: token, redirect_url: 'auth/password' } };
this._tokenService.get('auth/password/edit', options)
.subscribe(
res => this.headerInfo = res.json(),
err => {
this._alertService.error('Password could not be reset with this token');
this._router.navigate(['/users/signin']);
}
);
});
}
Then I use the "headerInfo" to submit the new password-reset when submitting the form:
public resetPassword = (headerInfo: any, params: any): Observable<Response> => {
let headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Accept', 'application/json;q=0.9,*/*;q=0.8'); // Fix for Firefox
headers.append('access-token', headerInfo['token']);
headers.append('client', headerInfo['client_id']);
headers.append('expiry', headerInfo['expiry']);
headers.append('uid', headerInfo['uid']);
headers.append('token-type', 'Bearer');
return this._http.put(this._baseUrl + '/auth/password', params, { headers: headers }).map(res => res.json());
}
public submitForm() {
let params = {
password: this.resetPasswordForm.value.password,
password_confirmation: this.resetPasswordForm.value.password_confirmation
};
this.resetPassword(this.headerInfo, params);
}
So what's happening is that before the user can enter anything, I do a call to the backend to log the user in based on the reset token. Then I store that information and use that header-info to submit the real password change.
(these are really rough copy/pastes, so please excuse any errors)
Thanks @cybey, I ended up using another somewhat hacky approach and just sidestepped devise altogether. Basically, I generate a link with a password reset token and just search my users by that token when the user comes back, update the user with the password/password_confirmation provided by the client, return a 200 then login from the client using the newly set password and _tokenService.signIn(). Not the best but it seems to be the simplest for my purposes.
Thanks for the help!
Ah, no i figured this out just now. Here's the deal. Check out the line of code here: https://github.com/neroniaky/angular2-token/blob/master/src/angular2-token.service.ts#L270
So it's actually checking for null. Here's the problem with the reset password flow (something I may submit an issue for): the interface for updating a password has current password as a required attribute.
If you create a hidden attribute of "passwordCurrent" and set it to empty in your form:
<input ngModel type="hidden" name="passwordCurrent" value="" />
And then in your service reset passwordCurrent to null:
updatePassword(resetData: ResetForm) {
resetData.passwordCurrent = null;
return this.tokenService.updatePassword(resetData);
}
Then in the payload that gets sent, the current password is not set/sent with the new password and the token. Success!
Hi,
Sorry to drag up an old thread, but I am having difficulty getting the forgot / reset password flow working. In the example code above, I see this line :
resetPasswordToken: this.resetPasswordForm.value.password_token
Where is the password form getting the token from? I understand it is in the email URL as a query param, but how are you guys getting it from that? I know there are issues on here talking about the difficulty of getting it from the URL as the query is before the hash so its not available to the usual Angular tools like $location. It is also doesn't appear in UI Router routes, which is the router I am using.
Am I generally on the right track in thinking that the reset password form component has the responsibility of trying to extract the reset token from the URL?
Thanks!
I have a reset component that does something like this:
this.activatedRoute.queryParams.subscribe((params: Params) => {
this.token = params['token'];
}, () => {
this.notify.error('Error with token.');
});
Thanks for that Ben. The problem is I'm using UI Rotuer 2 and it doesn't easily have a mechanism for accessing a query before the hash. I ended up using url-parser (https://github.com/unshiftio/url-parse) like this:
import { Component, OnInit } from '@angular/core';
import {Angular2TokenService} from "angular2-token";
import {MatSnackBar} from "@angular/material";
import {StateService, Transition} from '@uirouter/core';
import * as urlParse from 'url-parse';
@Component({
selector: 'app-reset-password',
templateUrl: './reset-password.component.html',
styleUrls: ['./reset-password.component.css']})
export class ResetPasswordComponent implements OnInit {
credentials = {
password: '',
passwordConfirmation: '',
resetPasswordToken: ''
};
constructor(
private tokenService: Angular2TokenService,
private snackBar: MatSnackBar,
private stateService: StateService,
) {
console.log("In Reset Password constructor")
}
ngOnInit() {
}
resetPassword() {
const url = new urlParse(window.location.href, true);
console.log("In Reset Password resetPassword, url etc", url.query, url.query.token);
if(url.query && url.query.reset_password && url.query.token){
this.credentials.resetPasswordToken = url.query.token
}
console.log("requestPasswordReset, Credentials : ", this.credentials);
this.tokenService.updatePassword(this.credentials).subscribe(
res => {
console.log(res);
let snackBarRef = this.snackBar.open(
'Your password has been reset. Please login with your new password.',
'OK')
snackBarRef.onAction().subscribe(() => {
this.stateService.go('signIn');
return;
});
},
error => {
console.log("In Request Password, error: ", error);
let snackBarRef = this.snackBar.open(
'Error : ' + error.data.errors.full_messages.join(". "),
'OK')
snackBarRef.onAction().subscribe(() => {
this.snackBar.dismiss();
return;
});
}
);
}
}
Note that the true in new urlParse(window.location.href, true) is vital as it says to parse the query string into an object, so you can do url.query.token. Without that flag it just has a big query string with all query elements together.
OK, turns out that isn't enough. as cbey discovered earlier in the thread, update password needs the auth headers in the request, which is why they are included in the redirected url from devise token auth. I am using my own fork of this lib that uses HTTPClient and interceptors, so I'll probably just modify the code there to extract the headers from the query strings and send the password update with the new headers.
As it stands, this library has no mechanism for dealing with a reset password where the user isn't currently logged in - aka forgot password flow. Including the reset token as a property in the update password call is pointless as that call needs the auth headers if it is to work and currently the only time it has the auth headers is when the user is already logged and it has them from the normal back and forth. What is interesting is that in the only working scenario, user logged in, the reset password token isn't actually used at all, you can delete it from the url and the update still works, as it is using the actual auth header token from the live user session. It isn't totally clear to me that the reset token property would ever be actually used as the real way to do a forgot password reset to use the reset token to set the token header property, not to include it in the payload.
I checked this with a working project that uses ng token auth (aka the client lib for AngularJS) and it doesn't include the reset token in the payload, instead it copies the token into the usual token header property and that approach works for a forgot password flow. Note also that ng token auth lib figures all this out itself - it inspects the url, extracts the data from the queries and sets up the header for the update password request. This is the approach this lib should take. As it stands, the only way to get this working within the confines of the lib itself is to use the .request() method to set up a manually created call to update password with manually created headers that you extract yourself from the queries in the redirected url. This is similar to what cbey did although he went the whole hog and just did a direct http api call.
I'll post a doco PR to at least try to clarify some of this in the readme.
I've made the changes in my fork and the forgot password flow is now working. Briefly, I changed getAuthDataFromParams in tryLoadAuthData to use url-parse instead of relying on activatedRoute to parse the url (import * as urlParse from 'url-parse';). I then called tryLoadAuthData inside the update password call.
Based on that, it looks like just calling tryLoadAuthData from within update password might work for the master branch.
For the record, my getAuthDataFromParams now looks like this:
// Try to get auth data from url parameters.
private getAuthDataFromParams(): void {
const url = new urlParse(window.location.href, true);
if(url && url.query) {
let authData: AuthData = {
accessToken: url.query['token'] || url.query['auth_token'],
client: url.query['client_id'],
expiry: url.query['expiry'],
tokenType: 'Bearer',
uid: url.query['uid']
};
if (this.checkAuthData(authData)){
this.atCurrentAuthData = authData;
}
}
}
I also obviously call this directly, I am not checking for activatedRoute :
// Try to load auth data
private tryLoadAuthData(): void {
let userType = this.getUserTypeByName(localStorage.getItem('userType'));
if (userType)
this.atCurrentUserType = userType;
this.getAuthDataFromStorage();
this.getAuthDataFromParams();
if (this.atCurrentAuthData)
this.validateToken();
}
Finally, my update password :
// Update password request
updatePassword(updatePasswordData: UpdatePasswordData): Observable<any> {
if (updatePasswordData.userType != null)
this.atCurrentUserType = this.getUserTypeByName(updatePasswordData.userType);
let args: any;
if (updatePasswordData.passwordCurrent == null) {
args = {
password: updatePasswordData.password,
password_confirmation: updatePasswordData.passwordConfirmation
}
} else {
args = {
current_password: updatePasswordData.passwordCurrent,
password: updatePasswordData.password,
password_confirmation: updatePasswordData.passwordConfirmation
};
}
// Redo the header load in case this is a forgot password scenario and we need to get the headers from the
// redirected URL
this.tryLoadAuthData();
let body = JSON.stringify(args);
return this.request('PUT', this.getUserPath() + this.atOptions.updatePasswordPath, body);
}
will try your approach ! https://github.com/neroniaky/angular-token/issues/469
@colmben Can you please explain me how do you "intercept" on the frontend the client_id, expiry, token and uid??
This is the big and only mystery for me because I can only intercept the "token param" that is sent from there, nothing else
Did you make any change on the devise-token output? I find it very confusing, I confess I don't fully understand all that messy devise / devise auth shared controllers, views etc , and like for example where some variables like @token come from
I think I found a reasonably elegant solution to work with rails, working on some of the code in this thread.Effectively, this works around the issue that you cannot use the 'update password' method in angular-token library since there is no token set when you are not logged in and a token is required by devise_token_auth to access the reset password link . What I have done is effectively created a 'magic link' to auto sign in the user upon opening the email redirect url by overriding the passwords controller edit function. This fetches the user based on the url, , generates new tokens and returns the tokens generated to the front end so that it can be used in the ui:
Update the passwords_controller in rails:
class OverridePasswordsController < DeviseTokenAuth::PasswordsController
def edit
@resource = resource_class.reset_password_by_token({
reset_password_token: resource_params[:reset_password_token]
})
if @resource && @resource.id
client_id = SecureRandom.urlsafe_base64(nil, false)
token = SecureRandom.urlsafe_base64(nil, false)
token_hash = BCrypt::Password.create(token)
expiry = (Time.now + DeviseTokenAuth.token_lifespan).to_i
@resource.tokens[client_id] = {
token: token_hash,
expiry: expiry
}
# ensure that user is confirmed
@resource.skip_confirmation! if @resource.devise_modules.include?(:confirmable) && [email protected]_at
@resource.save!
puts @resource.tokens[client_id]
yield @resource if block_given?
render json: {
token: token,
client_id: client_id,
expiry: expiry,
uid: @resource.uid,
}
else
render_edit_error
end
end
end
Now, once the tokens are in the front end, you can now set them to the localstorage in your browser, which effectively logs the user in and then, use the updatePassword method within the angular token library to update the password. Ta -daaa.
Update the component where I am setting the password (post reset password link)
ngOnInit() {
this.activatedRoute.queryParams.map((params: Params) => {
this.reset_password_token = params['reset_password_token'];
return this.reset_password_token
).switchMap(token => {
let httpHeaders = new HttpHeaders({
'Content-Type' : 'application/json'
});
return this.http.get(this._tokenService.tokenOptions.apiBase + '/auth/password/edit', {headers:httpHeaders, params: {reset_password_token: this.reset_password_token, redirect_url: '/auth/password'}})
}).subscribe(
res => {
console.log(res);
localStorage.setItem('accessToken', res['token']);
localStorage.setItem('client', res['client_id']);
localStorage.setItem('expiry', res['expiry']);
localStorage.setItem('uid', res['uid']);
localStorage.setItem('tokenType', 'Bearer');
}, error => {
console.log(error);
}
)
}
public submitSetPasswordForm($ev, value: any) {
this._tokenService.updatePassword({
'password': value.passwordGroup.password,
'passwordConfirmation': value.passwordGroup.confirmPassword
}).subscribe(
res => {
console.log(res);
this.router.navigate(['/home']);
},
err=> console.log(err));
}
I like this. Maybe these snippets should be added to the wiki
I'm in the process of looking at this very issue. I don't think it needs that magic on the server side. The current process effectively logs in the user when they click on the email link (you can test this by pasting in a protected URL at that point). The password reset page response from the server after clicking the email link includes a new valid access code. The problem is that the actual call to edit the password doesn't have this code in it (instead it repeats the reset token, which will actually work if you set your "burst" time setting on the server to something long enough to allow you to get the new password in before it expires). So it's a one line fix I think. I'm currently fighting with the structure of this lib trying to set up a test of that one line, but I think I have figured that out, so should have a pr by tomorrow (big sports game in Dublin today! )
Sooo, I have an answer, at least for the standard Angular default way of doing local client routing. First off - the library as it stands does actually work for Forgot Password flow! There are a couple of things to consider:
Angular can do local URL routing 2 ways - either the old way it did by putting a hash symbol in the URL that indicated anything after that was for Angular or the new (now default) way of using some modern browser magic to prevent local routes getting sent out to the server. These are known as HashLocationStrategy and PathLocationStrategy respectively. PathLocationStrategy is the default for new Angular apps. It is great not to have the strange hashes in the URL any more but it has one rather significant side effect - on a refresh or a URL paste the full URL will pass through the server. Therefore you need to set up the server to automatically pass any unknown server routes straight through to index.html, which is the start of the Angular app (which then sends into Angular router etc.). For most web server this can be done using a very low level setting to catch what would normally be a 404 response (via .htaccess for example on an Apache web server). For a rails server, the situation can be a little trickier depending on your set up. My initial way of doing was to just put a catch all route that rendered to public/index.html (which is where my Heroku hosted app builds the angular index.html, yours might be in dist/index.html etc.). I did this by putting a match everything in routes.rb:
match "*path", to: 'boot#angular' , via: :all
and setting that action to render the file :
class BootController < ApplicationController
def angular
puts "in Angular render action"
render file: 'public/index.html', :layout => false
end
end
and that actually solves all the refresh issues with a PathLocationStrategy Angular app. But it introduced a subtle devise auth token problem. The match all -> index.html response was getting a token added to it! This didn't matter for refreshes (for reasons I haven't delved into) but it is fatal for the password redirect from the forgotten password email link. That redirect is issued by the server, and it then then flows through the server into the match all and on to index.html to get to the resetPassword Angular controller, which sets up the new password screen. But now the server has moved on one token and because the redirect request wasn't issued by Angular, it can't get at the response token (or at least I couldn't figure out how it can access it). After trying a number of different ways around this, I realised that all we have to do is stop the server putting a token on that redirect (and indeed on any non-matched route). My problem was that the controller I was using to do the render to index.html was inheriting from ApplicationController, which has the devise token auth Concerns line in it, which the bit that does all the token adding. So all I had to do was step back a level and instead inherit from ActionController.
class BootController < ActionController
def angular
puts "in Angular render action"
render file: 'public/index.html', :layout => false
end
end
And that worked! No token added to the redirect, so the resetPassword updatePassword call gets given the reset token as the access token by the lib and the server recognises that as the last valid token for the user and the password change works.
So actually no need for a lib change if you are using PathLocationStrategy.
For HashLocationStrategy, I figured it would actually work straight off as we don't have the issue of the redirect going through the server, but for some reason the redirect is eating the query string and so your resetPassword controller can't get at the resetToken to manually change the access_token. I didn't look to hard at this process as I want to use PathLocationStrategy.
I'll submit a PR tomorrow to take out the old doco saying none of this works (which I actually wrote, but for the old pre-interceptor version) and add in some notes about using PathLocationStrategy.
Can we get the 0.2.0 label taken off this issue btw? Confusing when you are searching.
Edit - btw, the server side lib devise token auth has accepted a patch to allow just the reset token, as part of the credentials, to authenticate during the password update process. See the entry for require_client_password_reset_token on https://github.com/lynndylanhurley/devise_token_auth/blob/master/docs/config/initialization.md. This is currently unreleased (in terms of not being in the current gem), but presumably it is on the way. Looking at the code in this lib, it looks like it is anticipating this change as it is trying to attach the reset token to the credentials in the case of a forgot password update password flow. In my testing, this wasn't actually working for HashLocationStrategy, possibly due to how I set up the reset callback url. But it seems to offer an easier way to do the password update from this lib's point of view.
Just a quick update, I figured out why HashLocationStrategy apps wouldn't work, the Devise Token Auth lib made a change a few years ago to put the query params before the hash fragment instead of afterwards. This causes current version of Angular to eat those query params so they don't show in the final redirected url nor are they catchable via activatedRoute.queryParams. I raised a PR on that lib to just dupe the query params when we have a fragment so they are sent both before and after the fragment. This fixes the password reset flow for HashLocationStrategy apps and leaves PathLocationStrategy apps working ok.