未经授权的请求使用设计身份验证方法作为 API 来重置密码
Unauthorized request using devise authentication methods as API for resetting password
我正在开发一个分为两个应用程序的项目:
- 一个 rails JSON API 正在处理数据库并将数据呈现为 JSON
- 一个 "front-end" rails 应用程序,它在需要时向 API 发送请求并以一种很好的方式显示 json 数据。
API 的身份验证是基于使用 gem'simple_token_authentication'
的令牌,这意味着对于发送到 API 的大多数请求,您必须将用户令牌及其电子邮件发送到header 请求授权。
在我之前从事该项目的人还在 API 端安装了 Devise 身份验证系统,以允许在使用电子邮件和密码成功登录后直接从导航器访问 API 方法.
我刚开始在应该请求 API 的 "front-end app" 上编码,我遇到了问题,尤其是在身份验证系统方面。
由于 Devise 已经安装在 API 上,我认为让用户登录 front-end 应用程序是个好主意,然后该应用程序将请求 [=] 上存在的设计方法60=] 用于创建用户、授权、重置密码...
问题是 devise 的方法正在渲染 html 而不是 JSON 所以我实际上不得不覆盖大部分 devise 的控制器。让您快速了解它是如何工作的:
您在 front-end 应用程序上填写注册表单,然后将参数发送到 front-end 应用程序控制器,然后在 API 上请求设计的注册用户方法:
1) front-end 应用程序控制器:
def create
# Post on API to create USER
@response = HTTParty.post(ENV['API_ADDRESS']+'users',
:body => { :password => params[:user][:password],
:password_confirmation => params[:user][:password_confirmation],
:email => params[:user][:email]
}.to_json,
:headers => { 'Content-Type' => 'application/json' })
# si le User est bien crée je récupère son email et son token, je les store en session et je redirige vers Account#new
if user_id = @response["id"]
session[:user_email] = @response["email"]
session[:user_token] = @response["authentication_token"]
redirect_to new_account_path
else
puts @response
@errors = @response["errors"]
puts @errors
render :new
end
end
2) API 覆盖了设计控制器:
class RegistrationsController < Devise::RegistrationsController
def new
super
end
def create
@user = User.new(user_params)
if @user.save
render :json => @user
else
render_error
end
end
def update
super
end
private
def user_params
params.require(:registration).permit(:password, :email)
end
def render_error
render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
end
end
这工作正常。在这里,我将刚刚在 API 上创建的用户发回为 JSON,我将身份验证令牌和他的电子邮件存储在 session 哈希中。
我的问题出在 reset_password 方法上,我试图为它重用一些设计代码。
首先,我要求重置密码,这会为请求更改的用户生成一个重置密码令牌。这还会向用户生成一封电子邮件,其中包含指向特定用户的重置密码表单的 link(其中包含令牌)。这运作良好。我在电子邮件中收到 link,然后转到我的 front-end 应用程序上的 edit_password 表格:
更改密码
<form action="/users/password" method='post'>
<input name="authenticity_token" value="<%= form_authenticity_token %>" type="hidden">
<%= hidden_field_tag "[user][reset_password_token]", params[:reset_password_token] %>
<%=label_tag "Password" %>
<input type="text" name="[user][password">
<%=label_tag "Password Confirmation" %>
<input type="text" name="[user][password_confirmation]">
<input type="Submit" value="change my password">
</form>
提交表单后,它会通过我的 front-end 应用程序控制器:
def update_password
@response = HTTParty.patch(ENV['API_ADDRESS']+'users/password',
:body => {
:user => {
:password => params[:user][:password],
:password_confirmation => params[:user][:password_confirmation],
:reset_password_token => params[:user][:reset_password_token]
}
}.to_json,
:headers => { 'Content-Type' => 'application/json' })
end
然后调用我覆盖的Devise::PasswordController(更新方法):
# app/controllers/registrations_controller.rb
class PasswordsController < Devise::RegistrationsController
# POST /resource/password
def create
if resource_params[:email].blank?
render_error_empty_field and return
end
self.resource = resource_class.send_reset_password_instructions(resource_params)
yield resource if block_given?
if successfully_sent?(resource)
render_success
else
render_error
end
end
def update
self.resource = resource_class.reset_password_by_token(resource_params)
yield resource if block_given?
if resource.errors.empty?
resource.unlock_access! if unlockable?(resource)
render_success
else
render_error
end
end
private
# TODO change just one big method render_error with different cases
def render_success
render json: { success: "You will receive an email with instructions on how to reset your password in a few minutes." }
end
def render_error
render json: { error: "Ce compte n'existe pas." }
end
def render_error_empty_field
render json: { error: "Merci d'entrer un email" }
end
end
但是请求始终是未经授权的:
Started PATCH "/users/password" for ::1 at 2016-02-05 11:28:30 +0100
Processing by PasswordsController#update as HTML
Parameters: {"user"=>{"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "reset_password_token"=>"[FILTERED]"}, "password"=>{"user"=>{"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "reset_password_token"=>"[FILTERED]"}}}
Completed 401 Unauthorized in 1ms (ActiveRecord: 0.0ms)
我不明白为什么最后一个请求未经授权?
您的前任很可能只是为了方便而在 API 方面把事情搞得一团糟。
我们知道,为 API 使用 cookie 是一个 非常糟糕的主意,因为它为 CSRF/XSRF 攻击敞开了大门。
我们不能对 API 使用 Rails CSRF 保护,因为它只能保证请求来自我们自己的服务器。并且只能从您自己的服务器使用的 API 不是很有用。
Devise 默认使用基于 cookie 的身份验证策略,因为这适用于基于 Web 的应用程序,而 Devise 的全部目的是让基于 Web 的应用程序中的身份验证变得简单。
因此,您应该做的是从 API 应用程序中完全删除 Devise,或者将 Devise 转换为使用基于令牌的策略。您还应该考虑从 API app 中删除会话中间件。此外,Devise 控制器非常倾向于客户端交互,因此试图将它们打入 API 控制器将变得非常混乱。
在 API 中更新密码只是:
class API::V1::Users::PasswordsController
before_action :authenticate_user!
def create
@user = User.find(params[:user_id])
raise AccessDenied unless @user == current_user
@user.update(password: params[:password])
respond_with(@user)
end
end
这是一个非常简化的示例 - 但重点是,如果您从控制器中删除所有与表单/闪烁和重定向相关的垃圾,那么您真正要重新使用的东西并不多。
如果您的前端应用程序是 "classical" client/server Rails 应用程序,那么您可以使用常规的基于 cookie 的身份验证(设计)并让它与 API 应用程序。由于其无状态的性质,基于令牌的身份验证不适用于经典 client/server 应用程序。
如果前端应用是像 Angular 或 Ember.js 这样的 SPA,您可能需要考虑使用 Doorkeeper 来设置您自己的 OAuth 提供程序。
我正在开发一个分为两个应用程序的项目: - 一个 rails JSON API 正在处理数据库并将数据呈现为 JSON - 一个 "front-end" rails 应用程序,它在需要时向 API 发送请求并以一种很好的方式显示 json 数据。
API 的身份验证是基于使用 gem'simple_token_authentication'
的令牌,这意味着对于发送到 API 的大多数请求,您必须将用户令牌及其电子邮件发送到header 请求授权。
在我之前从事该项目的人还在 API 端安装了 Devise 身份验证系统,以允许在使用电子邮件和密码成功登录后直接从导航器访问 API 方法.
我刚开始在应该请求 API 的 "front-end app" 上编码,我遇到了问题,尤其是在身份验证系统方面。
由于 Devise 已经安装在 API 上,我认为让用户登录 front-end 应用程序是个好主意,然后该应用程序将请求 [=] 上存在的设计方法60=] 用于创建用户、授权、重置密码...
问题是 devise 的方法正在渲染 html 而不是 JSON 所以我实际上不得不覆盖大部分 devise 的控制器。让您快速了解它是如何工作的: 您在 front-end 应用程序上填写注册表单,然后将参数发送到 front-end 应用程序控制器,然后在 API 上请求设计的注册用户方法:
1) front-end 应用程序控制器:
def create
# Post on API to create USER
@response = HTTParty.post(ENV['API_ADDRESS']+'users',
:body => { :password => params[:user][:password],
:password_confirmation => params[:user][:password_confirmation],
:email => params[:user][:email]
}.to_json,
:headers => { 'Content-Type' => 'application/json' })
# si le User est bien crée je récupère son email et son token, je les store en session et je redirige vers Account#new
if user_id = @response["id"]
session[:user_email] = @response["email"]
session[:user_token] = @response["authentication_token"]
redirect_to new_account_path
else
puts @response
@errors = @response["errors"]
puts @errors
render :new
end
end
2) API 覆盖了设计控制器:
class RegistrationsController < Devise::RegistrationsController
def new
super
end
def create
@user = User.new(user_params)
if @user.save
render :json => @user
else
render_error
end
end
def update
super
end
private
def user_params
params.require(:registration).permit(:password, :email)
end
def render_error
render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
end
end
这工作正常。在这里,我将刚刚在 API 上创建的用户发回为 JSON,我将身份验证令牌和他的电子邮件存储在 session 哈希中。
我的问题出在 reset_password 方法上,我试图为它重用一些设计代码。 首先,我要求重置密码,这会为请求更改的用户生成一个重置密码令牌。这还会向用户生成一封电子邮件,其中包含指向特定用户的重置密码表单的 link(其中包含令牌)。这运作良好。我在电子邮件中收到 link,然后转到我的 front-end 应用程序上的 edit_password 表格:
更改密码
<form action="/users/password" method='post'>
<input name="authenticity_token" value="<%= form_authenticity_token %>" type="hidden">
<%= hidden_field_tag "[user][reset_password_token]", params[:reset_password_token] %>
<%=label_tag "Password" %>
<input type="text" name="[user][password">
<%=label_tag "Password Confirmation" %>
<input type="text" name="[user][password_confirmation]">
<input type="Submit" value="change my password">
</form>
提交表单后,它会通过我的 front-end 应用程序控制器:
def update_password
@response = HTTParty.patch(ENV['API_ADDRESS']+'users/password',
:body => {
:user => {
:password => params[:user][:password],
:password_confirmation => params[:user][:password_confirmation],
:reset_password_token => params[:user][:reset_password_token]
}
}.to_json,
:headers => { 'Content-Type' => 'application/json' })
end
然后调用我覆盖的Devise::PasswordController(更新方法):
# app/controllers/registrations_controller.rb
class PasswordsController < Devise::RegistrationsController
# POST /resource/password
def create
if resource_params[:email].blank?
render_error_empty_field and return
end
self.resource = resource_class.send_reset_password_instructions(resource_params)
yield resource if block_given?
if successfully_sent?(resource)
render_success
else
render_error
end
end
def update
self.resource = resource_class.reset_password_by_token(resource_params)
yield resource if block_given?
if resource.errors.empty?
resource.unlock_access! if unlockable?(resource)
render_success
else
render_error
end
end
private
# TODO change just one big method render_error with different cases
def render_success
render json: { success: "You will receive an email with instructions on how to reset your password in a few minutes." }
end
def render_error
render json: { error: "Ce compte n'existe pas." }
end
def render_error_empty_field
render json: { error: "Merci d'entrer un email" }
end
end
但是请求始终是未经授权的:
Started PATCH "/users/password" for ::1 at 2016-02-05 11:28:30 +0100
Processing by PasswordsController#update as HTML
Parameters: {"user"=>{"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "reset_password_token"=>"[FILTERED]"}, "password"=>{"user"=>{"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "reset_password_token"=>"[FILTERED]"}}}
Completed 401 Unauthorized in 1ms (ActiveRecord: 0.0ms)
我不明白为什么最后一个请求未经授权?
您的前任很可能只是为了方便而在 API 方面把事情搞得一团糟。
我们知道,为 API 使用 cookie 是一个 非常糟糕的主意,因为它为 CSRF/XSRF 攻击敞开了大门。
我们不能对 API 使用 Rails CSRF 保护,因为它只能保证请求来自我们自己的服务器。并且只能从您自己的服务器使用的 API 不是很有用。
Devise 默认使用基于 cookie 的身份验证策略,因为这适用于基于 Web 的应用程序,而 Devise 的全部目的是让基于 Web 的应用程序中的身份验证变得简单。
因此,您应该做的是从 API 应用程序中完全删除 Devise,或者将 Devise 转换为使用基于令牌的策略。您还应该考虑从 API app 中删除会话中间件。此外,Devise 控制器非常倾向于客户端交互,因此试图将它们打入 API 控制器将变得非常混乱。
在 API 中更新密码只是:
class API::V1::Users::PasswordsController
before_action :authenticate_user!
def create
@user = User.find(params[:user_id])
raise AccessDenied unless @user == current_user
@user.update(password: params[:password])
respond_with(@user)
end
end
这是一个非常简化的示例 - 但重点是,如果您从控制器中删除所有与表单/闪烁和重定向相关的垃圾,那么您真正要重新使用的东西并不多。
如果您的前端应用程序是 "classical" client/server Rails 应用程序,那么您可以使用常规的基于 cookie 的身份验证(设计)并让它与 API 应用程序。由于其无状态的性质,基于令牌的身份验证不适用于经典 client/server 应用程序。
如果前端应用是像 Angular 或 Ember.js 这样的 SPA,您可能需要考虑使用 Doorkeeper 来设置您自己的 OAuth 提供程序。