How To Change Steering Rack On 2005 Trailblazer
Edifice your own authentication framework with Trailblazer
- If yous've never written a library, it'southward commonly best to start in a real app and and so excerpt the logic.
- We volition prototype our authentication library in a Rails app and then motility it to a split precious stone.
- This jewel is then used in our app and customized.
- I mostly don't explain why but how. The why I will elaborate in a divide volume.
- We don't use Reform or many macros in this first office.
- We don't hash out: mailers, AR (to exist covered in the book)
- Book ideas: stronger countersign, reset verify token, resend account verification token
Create Account
- Imagine a form with
e-mail
,countersign
, andpassword_confirm
fields. - Obviously, the e-mail has to exist valid and both passwords need to match.
- When submitted, a controller activeness will invoke an operation to validate the form data and create a new user in the database along with a hashed countersign.
# app/concepts/auth/operation/create_account.rb module Auth::Performance grade CreateAccount < Trailblazer::Operation stride :check_email step :passwords_identical? def check_email(ctx, email:, **) e-mail =~ /\A[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+\z/ # login_email_regexp, stolen from Rodauth. finish def passwords_identical?(ctx, password:, password_confirm:, **) password == password_confirm end end end
- Naming of Trailblazer assets such equally operations or contracts follows the convention
concept name::layer::verb
, where the concept name and verb is completely upward to you lot. - Deeper nesting is possible.
class CreateAccount < Trailblazer::Functioning pace :check_email step :passwords_identical?
- OP implements all business logic necessary to perform a function of your application.
- Not a god object, it's a delegator that encapsulates vs. controller activeness and model/SO
- An operation is structured into steps. Those are ordinarily smaller methods composing the logic but adhering to strict encapsulation.
- All
footstep
s are invoked in the lodge they are declared in the functioning body. - If one step fails, all post-obit steps are skipped. More on that later.
def check_email(ctx, email:, **) email =~ /\A[^,;@ \r\north]+@[^,@; \r\north]+\.[^,@; \r\n]+\z/ # login_email_regexp, stolen from Rodauth. end
- A
step
is generally implemented as an instance method in the operation body. Whatsoever callable objects can exist used as a step, though. - Its return value matters:
nil
orfalse
will signal it "failed". - It receives a
ctx
object that is passed from step to stride. - The keyword arguments can be used to admission specific variables from that
ctx
.
# test/concepts/auth/operation_test.rb it "accepts valid email and passwords" exercise result = Auth::Performance::CreateAccount.({}) # op-interface #=> ArgumentError: missing keyword: :email end
- TODO: test file/track setup?
- To run (or invoke) an operation you use its
#call
method and pass an options hash. - Simplified, this volition phone call all steps in their defined order and pass the options hash from step to stride.
- The options hash is from now on called
ctx
. - However, this breaks in the first footstep
:check_email
because we're expecting an:email
keyword argument.
def check_email(ctx, email:, **)
- The ctx contains all variables from that options hash we provided when invoking the performance in the test.
- This
email:
"keyword argument" is automatically extracted by Trailblazer from the ctx object. - Since we did not pass whatsoever
:email
central/value to the operation, this keyword is non available. So let's fix this.
# exam/concepts/auth/operation_test.rb it "accepts valid email and passwords" do result = Auth::Operation::CreateAccount.({e-mail: "yogi@trb.to"}) # here'south an email! #=> ArgumentError: missing keywords: :countersign, :password_confirm # app/concepts/auth/operation/create_account.rb:22:in `passwords_identical?' end
- Nosotros pass an
:electronic mail
variable. But this time, information technology breaks in the next steppasswords_identical?
. - How do we know? We tin can come across it in the stacktrace, but, hang tight, there's another beautiful way to debug in Trailblazer:
wtf?
.
- The
trailblazer-developer
gem provides thewtf?
method to run an performance and print the period.
# exam/concepts/auth/operation_test.rb information technology "accepts valid email and passwords" do event = Auth::Operation::CreateAccount.wtf?({electronic mail: "yogi@trb.to"}) cease
Running the operation using wtf?
prints the following on the terminal.
- When an exception is thrown, it displays the triggering step in light red.
- Tracing the flow is a bit more expensive, so you shouldn't use it in performance-critical product environments.
- It's a beautiful tool especially when you offset nesting operations or activities to more than complex compounds.
- Let'southward brand this test pass, finally.
Past passing all required signup variables we make the examination pass.
# exam/concepts/auth/operation_test.rb it "accepts valid email and passwords" exercise effect = Auth::Operation::CreateAccount.wtf?( { email: "yogi@trb.to", countersign: "1234", password_confirm: "1234", } ) assert result.success? finish
- When
call
ed, an operation returns a result object. -
wtf?
besides returns the consequence object, as it also ran the operation. - We can figure out if an operation was run successfully past asking the result for
success?
orfailure?
. - Passing all three required variables, the performance runs all steps.
- The trace shows that both steps were executed successfully.
- Permit's learn more about steps by breaking things, again.
# examination/concepts/auth/operation_test.rb it "fails on invalid input" do result = Auth::Operation::CreateAccount.wtf?( { email: "yogi@trb", # invalid email. password: "1234", password_confirm: "1234", } ) affirm result.failure? end
- When passing an invalid email,
check_email
fails considering the regular expression matching returns fake. - The
check_email
method thus returns false when the email is invalid, signalizing that this pace has "failed". - In the trace, the failing step is marked orange. Or is it chocolate-brown?
- If a footstep fails, all remaining steps are skipped and the performance effect is a
failure
. - Why is this?
- Using
step
volition place the step on the "success" track (green) which leads to asuccess
terminus. - This is why - with proper input - your
CreateAccount
is successful. - If a step returns a
nil
orfalse
value, the flow deviates to the "failure" track (red), leading to thefailure
terminus. - This is why the remaining steps on the success track are non executed, for example, when we provided an invalid email address.
- You can place steps on the failure track, too.
# app/concepts/auth/performance/create_account.rb module Auth::Operation class CreateAccount < Trailblazer::Operation step :check_email fail :email_invalid_msg # {neglect} places steps on the failure rail. step :passwords_identical? fail :passwords_invalid_msg # ... def email_invalid_msg(ctx, **) ctx[:error] = "Email invalid." end def passwords_invalid_msg(ctx, **) ctx[:error] = "Passwords do not match." finish end cease
- Nosotros add together two error handling steps to our performance:
email_invalid_msg
andpasswords_invalid_msg
. - Those are placed afterward their respective steps.
- Check out how you lot can also write to the
ctx
object. - Our next test case asserts that the fault messages are correct.
# test/concepts/auth/operation_test.rb it "returns error message for invalid email" do effect = Auth::Operation::CreateAccount.wtf?( { email: "yogi@trb", # invalid e-mail. password: "1234", password_confirm: "1234", } ) assert result.failure? assert_equal "Email invalid.", result[:error] #=> Expected: "Email invalid." # Actual: "Passwords do not match." stop
- Passing an incomplete e-mail to our operation, we'd expect the error message to complain virtually merely that.
- Notwithstanding, the error handler for the passwords must've been chosen.
- Let's check the trace.
- Both handlers have been called!
- The 2d overriding the first's error message.
- That'southward because merely like
stride
, steps placed withfail
volition be executed in their order, as shown below.
- Trailblazer'southward [Wiring API] allows literally any connections you want.
- For this specific case, nosotros make every
fail
stride connect directly to thefail_fast
finish, not going down thefailure
runway.
# app/concepts/auth/performance/create_account.rb module Auth::Operation class CreateAccount < Trailblazer::Operation stride :check_email fail :email_invalid_msg, fail_fast: true step :passwords_identical? fail :passwords_invalid_msg, fail_fast: true # ... end end
- Annotation the
fail_fast: true
for bothneglect
steps. - This results in a flow diagram as beneath.
- As noted, there are other ways to achieve this. You lot don't fifty-fifty have to have separate steps for error messages.
- Adding two tests to assert the email and countersign error messages.
# examination/concepts/auth/operation_test.rb it "validates email" do upshot = Auth::Performance::CreateAccount.wtf?( { e-mail: "yogi@trb", # invalid e-mail. countersign: "1234", password_confirm: "1234", } ) affirm result.failure? assert_equal "Electronic mail invalid.", result[:error] end
- Whatever is written to the
ctx
you tin read from the result object in tests, controllers, or other scripts. - The passwords test now also passes.
# test/concepts/auth/operation_test.rb it "validates passwords" practice result = Auth::Operation::CreateAccount.wtf?( { email: "yogi@trb.to", countersign: "12345678", password_confirm: "1234", } ) affirm issue.failure? assert_equal "Passwords practise not match.", upshot[:error] end
- When the input is correct, the next step is calculating a scrambled password hash.
- Since passwords should never be stored plain in the database, we utilise
bcrypt
to encrypt it.
# app/concepts/auth/performance/create_account.rb require "bcrypt" module Auth::Operation grade CreateAccount < Trailblazer::Operation step :check_email fail :email_invalid_msg, fail_fast: true footstep :passwords_identical? fail :passwords_invalid_msg, fail_fast: true step :password_hash # ... def password_hash(ctx, countersign:, password_hash_cost: BCrypt::Engine::MIN_COST, **) # stolen from Rodauth. ctx[:password_hash] = BCrypt::Countersign.create(password, price: password_hash_cost) end terminate stop
- Keyword arguments in steps tin can too be defaulted, the way we do it for
:password_hash_cost
. - Some of this code is stolen from Rodauth.
- We write the hashed string to
ctx[:password_hash]
. - Now, test.
# test/concepts/auth/operation_test.rb it "validates input, encrypts the password" do result = Auth::Functioning::CreateAccount.wtf?( { electronic mail: "yogi@trb.to", # invalid email. password: "1234", password_confirm: "1234", } ) affirm result.success? assert_equal "yogi@trb.to", event[:email] # {password_hash} is something similar "$2a$04$PgVsy.WbWmJ2tTT6pbDL..zSSQ6YQnlCTjsW8xczE5UeqeQw.EgAK" assert_equal sixty, consequence[:password_hash].size end
- After running the operation successfully, the
:password_hash
is at present filled with a random 60-grapheme token. This is the encrypted password.
- It'due south a good idea to set up a
country
field on the user, especially if we desire to utilise theworkflow
jewel later.
# app/concepts/auth/performance/create_account.rb require "bcrypt" module Auth::Operation class CreateAccount < Trailblazer::Operation step :check_email neglect :email_invalid_msg, fail_fast: true step :passwords_identical? fail :passwords_invalid_msg, fail_fast: truthful step :password_hash footstep :land # ... def state(ctx, **) ctx[:state] = "created, please verify account" end end finish
- Set a
:land
variable. - So far, this is non limited to a one-word token. Information technology'southward up to the states.
- We write this to the user in the next section.
- With an encrypted password in place, it is at present time to persist the user to the database.
- Later, we tin brand our library database-agnostic, for at present nosotros will but use the
User
model and ActiveRecord. - TODO: discuss db-cleaner and Gem/migration setup.
- To persist, we add some other step
#save_account
to our operation.
# app/concepts/auth/operation/create_account.rb require "bcrypt" module Auth::Operation class CreateAccount < Trailblazer::Functioning pace :check_email neglect :email_invalid_msg, fail_fast: truthful step :passwords_identical? fail :passwords_invalid_msg, fail_fast: truthful step :password_hash step :state step :save_account # ... def save_account(ctx, electronic mail:, password_hash:, land:, **) user = User.create(email: email, password: password_hash, state: land) ctx[:user] = user stop stop terminate
- This is zippo more than than using
User.create()
and passing the sane variables from our operation. - We write the created model to
ctx[:user]
. - We can update our happy-path test to check the returned
:user
model.
# test/concepts/auth/operation_test.rb it "validates input, encrypts the password, and saves user" do result = Auth::Operation::CreateAccount.wtf?( { electronic mail: "yogi@trb.to", password: "1234", password_confirm: "1234", } ) assert result.success? user = result[:user] assert user.persisted? assert_equal "yogi@trb.to", user.email assert_equal sixty, user.countersign.size assert_equal "created, please verify account", user.state end
- The persisted
User
model should take the provided e-mail and the hashed password.
- Since the
users.e-mail
column acts similar a user ID and isUNIQUE
, we must test what happens when two users with the same e-mail sign upward.
information technology "doesn't allow two users with same electronic mail" do options = { electronic mail: "yogi@trb.to", # invalid e-mail. password: "1234", password_confirm: "1234", } result = Auth::Operation::CreateAccount.wtf?(options) affirm result.success? result = Auth::Operation::CreateAccount.wtf?(options) # throws an exception! assert result.failure? end
- The
CreateAccount
operation is run twice with the same input. - Running this test case fires an exception for the second invocation.
ActiveRecord::RecordNotUnique: SQLite3::ConstraintException: UNIQUE constraint failed: users.electronic mail
- Obviously, the mistake comes from
save_account
. - We have to cover this case by catching the
RecordNotUnique
exception that ActiveRecord throws. - Instead of the exception, we but want to indicate a failure.
- In TRB, we refrain from exceptions wherever possible and rely on wiring activity outcomes accordingly.
- The
save_account
method needs to handle the potential exception, hither'southward a showtime version.
def save_account(ctx, electronic mail:, password_hash:, country:, **) begin user = User.create(electronic mail: electronic mail, countersign: password_hash, land: state) rescue ActiveRecord::RecordNotUnique ctx[:error] = "E-mail #{email} is already taken." return simulated end ctx[:user] = user end
- By using
begin/rescue
we take hold of ActiveRecord'southward uniqueness violation. - We tin can afterwards use the macro
Rescue()
fromtrailblazer-macro
. - This time, nosotros can set the error bulletin direct in the method.
- Returning
false
ways any furtherfootstep
south are not executed and instead the flow diverts to the failure track. - All tests pass, we can create users in the database with an encryted password and identified past their email accost.
- We also want the account to be confirmed by the email owner. To do so, we send a ostend link to the email address.
- When clicking this link in the e-mail, the user proofs that they signed upwardly using our library.
- The bodily confirmation is a split up operation
VerifyAccount
. - Nevertheless, we need to compute the verify or confirm token now.
- The confirm token is stored in a carve up table
account_verification_tokens
.
create_table "verify_account_tokens", strength: :pour practice |t| t.integer "user_id" t.text "token" t.datetime "created_at", precision: six, zero: false t.datetime "updated_at", precision: half-dozen, zero: false t.index ["token"], name: "index_verify_account_tokens_on_token", unique: true terminate
- To compute the
verify_account_token
, two more steps are added to theCreateAccount
functioning.
# app/concepts/auth/operation/create_account.rb require "bcrypt" module Auth::Operation class CreateAccount < Trailblazer::Operation step :check_email neglect :email_invalid_msg, fail_fast: true stride :passwords_identical? fail :passwords_invalid_msg, fail_fast: true step :password_hash pace :state pace :save_account step :generate_verify_account_key step :save_verify_account_key # ... def generate_verify_account_key(ctx, secure_random: SecureRandom, **) ctx[:verify_account_key] = secure_random.urlsafe_base64(32) end def save_verify_account_key(ctx, verify_account_key:, user:, **) begin VerifyAccountKey.create(user_id: user.id, fundamental: verify_account_key) rescue ActiveRecord::RecordNotUnique ctx[:error] = "Delight endeavour over again." return fake finish end end stop
- In
#generate_verify_account_token
we generate a random token to include in the confirmation email. - The token is generated using
SecureRandom
. Note that this class can be injected, nosotros merely default thesecure_random
keyword argument. - We need this for testing.
-
#save_verify_account_token
creates a new row in the respective table. Since the token must exist unique, we need to catch aRecordNotUnique
exception. - The exception is very unlikely to happen but withal needs to exist covered. If it happens,
CreateAccount
fails with an error message. - We have to test the happy-path and the uniqueness validation.
# test/concepts/auth/operation_test.rb information technology "validates input, encrypts the countersign, saves user, and creates a verify-business relationship token" exercise effect = Auth::Operation::CreateAccount.wtf?( { electronic mail: "yogi@trb.to", password: "1234", password_confirm: "1234", } ) assert result.success? user = result[:user] assert user.persisted? assert_equal "yogi@trb.to", user.electronic mail assert_equal 60, user.password.size assert_equal "created, delight verify account", user.state verify_account_token = VerifyAccountKey.where(user_id: user.id)[0] # key is something like "aJK1mzcc6adgGvcJq8rM_bkfHk9FTtjypD8x7RZOkDo" assert_equal 43, verify_account_token.central.size end
- In addition to the created
User
we now have a matching entry inverify_account_tokens
. - To exam the uniqueness for the verify token, nosotros need some other exam.
course NotRandom def self.urlsafe_base64(*) "this is non random" stop end it "fails when trying to insert the same {verify_account_token} twice" do options = { electronic mail: "fred@trb.to", countersign: "1234", password_confirm: "1234", secure_random: NotRandom # inject a test dependency. } result = Auth::Operation::CreateAccount.wtf?(options) affirm upshot.success? assert_equal "this is not random", result[:verify_account_key] result = Auth::Performance::CreateAccount.wtf?(options.merge(email: "celso@trb.to")) assert result.failure? # verify account token is non unique. assert_equal "Please effort again.", result[:fault] end
- By using
NotRandom
every bit our random string generator we can make sure nosotros get 2 identical tokens. - The second run fails equally we expect.
- The trace demonstrates this.
- Last affair to practise is ship a welcome email to the user with the verify account link in it.
# app/concepts/auth/operation/create_account.rb require "bcrypt" module Auth::Operation class CreateAccount < Trailblazer::Operation stride :check_email fail :email_invalid_msg, fail_fast: truthful stride :passwords_identical? fail :passwords_invalid_msg, fail_fast: truthful step :password_hash step :state step :save_account footstep :generate_verify_account_key step :save_verify_account_key step :send_verify_account_email # ... def send_verify_account_email(ctx, verify_account_key:, user:, **) token = "#{user.id}_#{verify_account_key}" # stolen from Rodauth. ctx[:verify_account_token] = token ctx[:email] = AuthMailer.with(email: user.email, verify_token: token).welcome_email.deliver_now stop end end
- The verify token is prefixed with the user ID to mitigate brute force attacks (stolen from Rodauth).
-
AuthMailer
sends the email. - Annotation how the operation computes the final token, not the mailer or controller.
- Be wary what you add to
ctx
in the op (e.m.verify_account_token
). This is public API y'all're creating.
# exam/concepts/auth/operation_test.rb it "validates input, encrypts the password, saves user, creates a verify-account token and send a welcome email" do effect = goose egg assert_emails i do result = Auth::Operation::CreateAccount.wtf?( { e-mail: "yogi@trb.to", password: "1234", password_confirm: "1234", } ) end assert result.success? user = result[:user] assert user.persisted? assert_equal "yogi@trb.to", user.email assert_equal 60, user.password.size assert_equal "created, delight verify account", user.land assert_match /#{user.id}_.+/, consequence[:verify_account_token] verify_account_key = VerifyAccountKey.where(user_id: user.id)[0] # key is something like "aJK1mzcc6adgGvcJq8rM_bkfHk9FTtjypD8x7RZOkDo" assert_equal 43, verify_account_key.key.size assert_match /\/auth\/verify_account\/#{user.id}_#{verify_account_key.central}/, effect[:email].body.to_s end
- We need to use
assert_emails
provided by Rails to catch the email. This is poor design in Rail. - The email nosotros tin can examination past accessing the rendered version from the consequence.
- Nosotros make sure it contains the verify account link with the token.
- Our commencement Tyrant operation is finished.
- AR and mailer Track coupling
- orchestration vs. god object
Verify Account
- When clicking the link in our welcome email, the user opens a URL like
http://example.com/auth/verify_account/158_NvMiR6UVglr4pXT_8dqIJB41c0o3lKul2RQc84Tn2kc
. - In the backend nosotros volition run
Auth::Operation::VerifyAccount
that splits the token into user ID and verification key, finds the respective user, compares the actual and the stored key, and marks the user business relationship every bit verified. - The
VerifyAccount
class looks massive, just information technology's a bunch of very elementary steps. - Talk over: should nosotros break down to code sample here?
# app/concepts/auth/operation/verify_account.rb module Auth::Performance course VerifyAccount < Trailblazer::Operation stride :extract_from_token footstep :find_verify_account_key step :find_user pace :compare_keys step :country # Talk over: motility exterior? footstep :save # Discuss: motion outside? step :expire_verify_account_key def extract_from_token(ctx, verify_account_token:, **) id, central = Auth::TokenUtils.split_token(verify_account_token) ctx[:id] = id ctx[:primal] = key # returns imitation if we don't take a central. cease def find_verify_account_key(ctx, id:, **) ctx[:verify_account_key] = VerifyAccountKey.where(user_id: id)[0] end def find_user(ctx, id:, **) ctx[:user] = User.find_by(id: id) end def compare_keys(ctx, verify_account_key:, fundamental:, **) Auth::TokenUtils.timing_safe_eql?(key, verify_account_key.key) # a hack-proof == comparison. end def land(ctx, user:, **) user.state = "ready to login" end def save(ctx, user:, **) user.save terminate def expire_verify_account_key(ctx, verify_account_key:, **) verify_account_key.delete terminate stop end
- We split up the token string into user id and verification cardinal in
extract_from_token
. The methodsplit_token
is implemented inTokenUtils
. Both values are assigned toctx
variables. - Note that, if the splitting wasn't successful,
key
will benothing
. This will make the step returnnil
and deviate to the failure rail. - In
find_verify_account_key
the token row is retrieved from the database by using the user id. Again, if there's no such row stored, this footstep will fail. -
find_user
finds the user. -
compare_keys
uses a hack-proof comparison to compare the provided key and the key stored in ourverify_account_keys
table. Encounter below. -
state
sets a new state on the user object. -
#salvage
persists the state. - Last,
#expire_verify_account_token
deletes the verification fundamental row from the database, it tin't be used anymore.
- For abyss, here's the
TokenUtils
class. - Both methods are stolen from Rodauth.
module Auth module TokenUtils # stolen from Rodauth module_function private def split_token(token) token.split("_", 2) end # https://codahale.com/a-lesson-in-timing-attacks/ individual def timing_safe_eql?(provided, actual) provided = provided.to_s Rack::Utils.secure_compare(provided.ljust(bodily.length), bodily) && provided.length == actual.length end end end
- To avoid timing attacks, nosotros use
Rack::Utils.secure_compare
. Run into https://codahale.com/a-lesson-in-timing-attacks/ to acquire what a timing attack is. - Don't worry, I had no idea myself that this exists.
- It'south a good idea to excerpt valid options for
CreateAccount
since nosotros volition use it a lot.
let(:valid_create_options) { { email: "yogi@trb.to", password: "1234", password_confirm: "1234", } }
- At present, permit's practice the happy-path kickoff.
# examination/concepts/auth/operation_test.rb it "allows finding an account from {verify_account_token}" practise issue = Exam::Auth::Operation::CreateAccount.wtf?(valid_create_options) affirm result.success? verify_account_token = result[:verify_account_token] # 158_NvMiR6UVglr4pXT_8dqIJB41c0o3lKul2RQc84Tn2kc upshot = Auth::Operation::VerifyAccount.wtf?(verify_account_token: verify_account_token) assert event.success? user = issue[:user] assert_equal "ready to login", user.state assert_equal "yogi@trb.to", user.email assert_nil VerifyAccountKey.where(user_id: user.id)[0] cease
- We use
CreateAccount
as a factory for a user business relationship waiting for confirmation. - Annotation how nosotros retrieve the token from the consequence object and laissez passer it directly into
VerifyAccount
. That's why we take a:verify_account_token
keyword at that place. - The country of the user object must be
"ready to login"
after a successful run ofVerifyAccount
with the correct input. - The row in
verify_account_keys
must be deleted by now as information technology expired due to being used.
- We must make sure that nosotros fail if an invalid user ID is in the token, due east.1000. for a user that doesn't be.
information technology "fails with invalid ID prefix" practice result = Auth::Operation::VerifyAccount.wtf?(verify_account_token: "0_safasdfafsaf") assert event.failure? finish
- In that location is no user with ID=
0
so the OP must neglect. - As visible in the trace, it fails in
find_verify_account_key
since we don't have a table row withuser_id=0
it "fails with invalid token" practice result = Examination::Auth::Operation::CreateAccount.wtf?(valid_create_options) assert issue.success? result = Auth::Operation::VerifyAccount.wtf?(verify_account_token: effect[:verify_account_token] + "rubbish") assert effect.failure? result = Auth::Operation::VerifyAccount.wtf?(verify_account_token: "") assert result.failure? end
- Nosotros create a valid business relationship to be verified.
- In the first exam, the prefixed ID is correct but the token is wrong.
- In the trace, you lot tin see it errors out in
compare_keys
. - The 2d exam checks cypher breaks for an empty token.
- It errors out in
extract_from_token
every bit thesplit
fails.
it "fails second fourth dimension" practice issue = Test::Auth::Operation::CreateAccount.wtf?(valid_create_options) affirm effect.success? result = Auth::Operation::VerifyAccount.wtf?(verify_account_token: upshot[:verify_account_token]) assert result.success? result = Auth::Operation::VerifyAccount.wtf?(verify_account_token: result[:verify_account_token]) assert issue.failure? cease
- When using the same token twice, it fails in
find_verify_account_key
as the corresponding row doesn't exist anymore. - We finished our second operation for
Tyrant
.
Reset Password
- When non logged in, y'all can click a link "Forgot password?" that brings y'all to a form.
- The course is an electronic mail field and a "Reset password" button.
- We will implement that in office II of the tutorial.
- When clicking "Reset password", our new
ResetPassword
operation is hitting.
module Auth::Operation class ResetPassword < Trailblazer::Operation step :find_user pass :reset_password step :state step :save_user step :generate_verify_account_token pace :save_verify_account_token step :send_reset_password_email def find_user(ctx, e-mail:, **) ctx[:user] = User.find_by(electronic mail: electronic mail) end def reset_password(ctx, user:, **) user.countersign = nil end def state(ctx, user:, **) user.country = "countersign reset, please change password" terminate def save_user(ctx, user:, **) user.save end # FIXME: copied from CreateAccount!!! def generate_verify_account_token(ctx, secure_random: SecureRandom, **) ctx[:reset_password_key] = secure_random.urlsafe_base64(32) end # FIXME: almost copied from CreateAccount!!! def save_verify_account_token(ctx, reset_password_key:, user:, **) brainstorm ResetPasswordKey.create(user_id: user.id, key: reset_password_key) # VerifyAccountKey => ResetPasswordKey rescue ActiveRecord::RecordNotUnique ctx[:mistake] = "Please try over again." return false end end def send_reset_password_email(ctx, reset_password_key:, user:, **) token = "#{user.id}_#{reset_password_key}" # stolen from Rodauth. ctx[:reset_password_token] = token ctx[:email] = AuthMailer.with(email: user.e-mail, reset_password_token: token).reset_password_email.deliver_now end finish finish
- We notice the respective user by their email or bail out in
find_user
. - The
password
field gets reset if user is plant inreset_password
. - Land is changed in
state
as the account status changes. - The following 2 steps are copied over from
CreateAccount
and slightly modified. This is not good and needs refactoring soon! They create the reset password token that we send in an email to the user account. - We send an email to the user with the token so they tin can open up the "Change countersign" form.
- Let's exam if an invalid email actually stops the OP.
it "fails with unknown email" do issue = Auth::Operation::ResetPassword.wtf?( { email: "i_do_not_exist@trb.to", } ) assert outcome.failure? terminate
- When passing an unknown e-mail to
ResetPassword
, the operation fails infind_user
. - Bank check the trace.
- let's test if a valid e-mail resets the password and sends an e-mail.
# test/concepts/auth/operation_test.rb it "resets countersign and sends a reset-password e-mail" do # exam setup aka "factories": issue = null assert_emails 2 do result = Test::Auth::Operation::CreateAccount.wtf?(valid_create_options) result = I::Auth::Functioning::VerifyAccount.wtf?(verify_account_token: result[:verify_account_token]) # the actual exam. result = Auth::Operation::ResetPassword.wtf?( { email: "yogi@trb.to", } ) end assert event.success? user = upshot[:user] assert_equal "yogi@trb.to", user.e-mail assert_nil user.password # password reset! assert_equal "password reset, please change countersign", user.state assert_match /#{user.id}_.+/, result[:reset_password_token] reset_password_token = ResetPasswordKey.where(user_id: user.id)[0] # token is something similar "aJK1mzcc6adgGvcJq8rM_bkfHk9FTtjypD8x7RZOkDo" assert_equal 43, reset_password_token.primal.size assert_match /\/auth\/reset_password\/#{user.id}_#{reset_password_token.central}/, consequence[:email].body.to_s end
- Note how we utilise two operations to setup test application state - that's the spirit of TRB!
- We test that the correct user'south password is
naught
ified. - Too, the
land
changes. - At that place must be a token in
reset_password_tokens
. - A right email containing the actual reset link must accept been sent.
- Notation that we all the same have
:verify_account_token
in the upshot considering we copied code.
- Since we reuse the logic to generate a token and save the token in two places (
CreateAccount
andResetPassword
) we have to extract this logic to a separate course. - It's easiest to move that to a separate operation.
- TRB makes it super simple to nest and etch more complex operations.
- We create
Auth::Action::CreateToken
which tin can exist "configured" through parameters.
module Auth module Activity class CreateKey < Trailblazer::Operation pace :generate_key footstep :save_key def generate_key(ctx, secure_random: SecureRandom, **) ctx[:key] = secure_random.urlsafe_base64(32) stop def save_key(ctx, cardinal:, user:, key_model_class:, **) brainstorm key_model_class.create(user_id: user.id, primal: primal) # key_model_class = VerifyAccountKey or ResetPasswordKey rescue ActiveRecord::RecordNotUnique ctx[:error] = "Please try over again." return imitation stop end cease # CreateKey end end
- Nosotros store the token in
:token
at present. - The ActiveRecord class needs to be given into that operation as
:token_model_class
. This is used in#save_token
. - To use this functioning in some other operation y'all use
Subprocess
.
module Auth::Performance form ResetPassword < Trailblazer::Operation pace :find_user pass :reset_password step :state step :save_user step Subprocess(Auth::Activity::CreateKey), input: ->(ctx, user:, **) { {key_model_class: ResetPasswordKey, user: user} }, output: {key: :reset_password_key} step :send_reset_password_email def find_user(ctx, e-mail:, **) ctx[:user] = User.find_by(email: email) terminate def reset_password(ctx, user:, **) user.password = zilch terminate def state(ctx, user:, **) user.state = "password reset, delight change countersign" end def save_user(ctx, user:, **) user.save cease def send_reset_password_email(ctx, reset_password_key:, user:, **) token = "#{user.id}_#{reset_password_key}" # stolen from Rodauth. ctx[:reset_password_token] = token ctx[:email] = AuthMailer.with(email: user.email, reset_password_token: token).reset_password_email.deliver_now end end end
- You tin employ
:input
to configure what goes into the nested operation. - Yous don't have to configure that, but we demand it.
- Nosotros use a proc that receives the current
ctx
and allows to return a hash with the input for the nestedCreateToken
operation. - Using
:output
, nosotros tin define what from the nested ctx nosotros want in the original ctx. - We tin also use a hash and define mappings, in our case "map
:token
to:reset_password_token
". - More than docs for variable mapping.
- Running our original tests all pass.
- Check how
wtf?
neatly shows yous the nesting. This is a major comeback to vanilla Ruddy.
- To complete the refactoring, we also need to update
CreateAccount
. - The 4
verify_account_token
-related steps get replaced byCreateToken
.
module Auth::Operation class CreateAccount < Trailblazer::Operation # ... footstep :save_account pace Subprocess(Auth::Activity::CreateKey), input: ->(ctx, user:, **) { {key_model_class: VerifyAccountKey, user: user} }, output: {token: :verify_account_token} # ... end finish
- All tests still pass when running the test suite.
Update Countersign
- When the user clicks the "Reset password" link in their email, they get sent to the "Modify password" form.
- This form can be opened multiple times (currently) and the link only expires once the password is gear up.
- We need two operations:
UpdatePassword::CheckToken
to find the user associated to the reset_password_token, and compare information technology in club to return the "Alter countersign" course. - and
UpdatePassword
to process the submitted course and eventually update the password. - Equally you already guessed, in
VerifyAccount
nosotros have iii steps with almost identical logic (#find_verify_account_key
,#find_user
,#compare_keys
). - Let'southward excerpt the "find user past token, and compare keys" logic.
The CheckToken
operation implements all common steps.
module Auth::Activeness # Splits token, finds user and key row by {:token}, and compares safely. class CheckToken < Trailblazer::Performance step :extract_from_token step :find_key step :find_user pace :compare_keys def extract_from_token(ctx, token:, **) id, fundamental = Auth::TokenUtils.split_token(token) ctx[:id] = id ctx[:input_key] = key # returns false if we don't have a cardinal. finish def find_key(ctx, id:, **) ctx[:cardinal] = key_model_class.where(user_id: id)[0] end def find_user(ctx, id:, **) # Hash out: might get moved outside. ctx[:user] = User.find_by(id: id) stop def compare_keys(ctx, input_key:, key:, **) Auth::TokenUtils.timing_safe_eql?(input_key, key.key) # a hack-proof == comparison. end private def key_model_class enhance "implement me" cease end # CheckToken end
- It splits the token (
1_xxx
) into user ID and key. - Finds both the corresponding user and the fundamental row.
- The corresponding db table for the central needs to be configured by overriding
#key_model_class
. This in an alternative approach to using an injection with:input
. - Compares the keys using the hack-proof method in
#compare_keys
. - All steps can fail and will omit the remaining steps.
- To use
CheckToken
to detect keys inupdate_password_keys
we need to subclass it.
module Auth::Operation class UpdatePassword < Trailblazer::Performance course CheckToken < Auth::Activity::CheckToken private def key_model_class ResetPasswordKey finish end # ... # ...
- We only have to override
key_model_class
and return the class constantResetPasswordKey
. - Both steps and methods are inherited from
Auth::Activity::CheckToken
. - We're set up to examination that logic. Recollect, we need this to decide whether or not an "update password" request is valid and whether nosotros should render the "Update password" course for the legitimated locked-out user.
# test/concepts/auth/operation_test.rb it "finds user by reset-countersign token and compares keys" do # test setup aka "factories", we don't have to use `wtf?` every time. upshot = K::Auth::Performance::CreateAccount.(valid_create_options) result = L::Auth::Operation::VerifyAccount.(token: effect[:verify_account_token]) result = 1000::Auth::Functioning::ResetPassword.(e-mail: "yogi@trb.to") token = issue[:reset_password_token] effect = Auth::Functioning::UpdatePassword::CheckToken.wtf?(token: token) affirm effect.success? original_key = effect[:key] # note how you can read variables written in CheckToken if you don't use {:output}. user = result[:user] assert user.persisted? assert_equal "yogi@trb.to", user.electronic mail assert_nil user.password # password reset! assert_equal "password reset, please change password", user.state # primal is withal in database: reset_password_key = ResetPasswordKey.where(user_id: user.id)[0] # key hasn't changed: assert_equal original_key, reset_password_key end
- We use our formerly written ops as factories.
- This test checks if the token check works, and asserts there's no application state change, yet.
- Talk over: refactor token-related tests?
- nosotros likewise need to test for an incorrect token.
information technology "fails with incorrect token" exercise issue = 1000::Auth::Operation::CreateAccount.(valid_create_options) outcome = L::Auth::Operation::VerifyAccount.(token: effect[:verify_account_token]) result = K::Auth::Operation::ResetPassword.(email: "yogi@trb.to") token = result[:reset_password_token] result = Auth::Operation::UpdatePassword::CheckToken.wtf?(token: token + "rubbish") assert result.failure? end
- By providing an invalid token, the
UpdatePassword::CheckToken
op fails.
- If the token is valid, the user can enter a new countersign forth with its confirmation string.
- We volition implement that grade in part Two of the book.
- When submitting, this hits
UpdatePassword
. - here, we need to check the token, again.
- we besides accept to check password identity and in case of success, hash the new password.
- This is logic from
CreateAccount
that we now extract toProcessPasswords
.
module Auth::Activity # Check if both {:password} and {:password_confirm} are identical. # And then, hash the password. class ProcessPasswords < Trailblazer::Functioning step :passwords_identical? neglect :passwords_invalid_msg, fail_fast: true footstep :password_hash def passwords_identical?(ctx, password:, password_confirm:, **) countersign == password_confirm stop def passwords_invalid_msg(ctx, **) ctx[:error] = "Passwords do not match." finish def password_hash(ctx, password:, password_hash_cost: BCrypt::Engine::MIN_COST, **) # stolen from Rodauth. ctx[:password_hash] = BCrypt::Countersign.create(password, cost: password_hash_cost) end end terminate
- Nosotros discussed this logic before.
- This fiddling op needs
:password
and:password_confirm
and provides the hashed countersign - if right and valid - in:password_hash
. - Discuss: show how injection still works.
- This
ProcessPasswords
can at present exist used inUpdatePassword
.
module Auth::Operation class UpdatePassword < Trailblazer::Operation # ... stride Subprocess(CheckToken) # provides {:user} step Subprocess(Auth::Activity::ProcessPasswords), # provides {:password_hash} fail_fast: true step :state step :update_user step :expire_reset_password_key def state(ctx, **) ctx[:land] = "ready to login" end def update_user(ctx, user:, password_hash:, state:, **) user.update_attributes( password: password_hash, state: land ) end def expire_reset_password_key(ctx, central:, **) key.delete cease terminate end
- The first stride runs
UpdatePassword::CheckToken
, which either fails or provides a:user
variable in the ctx. - This nested OP doesn't need any input/output configuration.
- The second stride runs
ProcessPasswords
, which fails in example of non matching passwords. If sucessful, it writes the:password_hash
to the ctx. - If
ProcessPasswords
fails, it terminates onfail_fast
, which is rewired usingfail_fast: true
. TODO: explain better - Nosotros then fix a new state on user.
- Nosotros update the user with a new
countersign
andstate
. Annotation how this footstep is coupled to ActiveRecord. - In the last stride, the
reset_password_keys
row is deleted from the database. The user can no longer update their password. - Hither is a successful run.
# test/concepts/auth/operation_test.rb it "finds user past reset_password_token and updates password" do result = 1000::Auth::Operation::CreateAccount.(valid_create_options) result = L::Auth::Operation::VerifyAccount.(token: result[:verify_account_token]) result = Yard::Auth::Functioning::ResetPassword.(email: "yogi@trb.to") token = result[:reset_password_token] result = Auth::Operation::UpdatePassword.wtf?(token: token, password: "12345678", password_confirm: "12345678") assert upshot.success? user = result[:user] assert user.persisted? assert_equal "yogi@trb.to", user.email assert_equal sixty, user.password.size assert_equal "ready to login", user.land # key is expired: assert_nil ResetPasswordKey.where(user_id: user.id)[0] end
- Now tell me you do not love this tracing!
- The updated user must take a countersign and a new country.
- The
reset_password_keys
row is deleted. - Hell aye!
- Let'south quickly test a failing issue.
it "fails with wrong password combo" practise result = K::Auth::Functioning::CreateAccount.(valid_create_options) result = L::Auth::Operation::VerifyAccount.(token: result[:verify_account_token]) result = K::Auth::Operation::ResetPassword.(email: "yogi@trb.to") token = result[:reset_password_token] upshot = Auth::Operation::UpdatePassword.wtf?( token: token, password: "12345678", password_confirm: "123" ) assert effect.failure? assert_equal "Passwords do not match.", event[:error] assert_nil result[:user].password end
- Every bit visible in the trace, it fails in
passwords_identical?
. - The inner OP halts on
fail_fast
. - The outer OP halts on
fail_fast
as it's wired automatically inSubprocess
. - We assert at that place were no side-effects, including no
countersign
assault user. - Wow, all OPs necessary are working.
Refactorings
- Nosotros nevertheless need to apply some refactorings to the older lawmaking.
-
CreateAccount
needs to applyProcessPasswords
.
# app/concepts/auth/functioning/create_account.rb module Auth::Functioning class CreateAccount < Trailblazer::Operation step :check_email neglect :email_invalid_msg, fail_fast: true step Subprocess(Auth::Activeness::ProcessPasswords) # provides {:password_hash} footstep :state step :save_account pace :generate_verify_account_token step :save_verify_account_token pace :send_verify_account_email # ... end end
- Instead of repeating logic, it now delegates to the countersign processing OP.
-
VerifyAccount
needs to utiliseCheckToken
.
# app/concepts/auth/operation/verify_account.rb module Auth::Functioning class VerifyAccount < Trailblazer::Operation class CheckToken < Auth::Activity::CheckToken private def key_model_class VerifyAccountKey finish end step Subprocess(CheckToken) pace :state # Hash out: move outside? step :salve # Talk over: move outside? step :expire_verify_account_key # ... def expire_verify_account_key(ctx, fundamental:, **) key.delete end cease end
- All token logic now comes from
CheckToken
. - This removes three steps.
- Everything only
#expire_verify_account_token
stays the aforementioned. Hither, nosotros at present use the more than generic:key
variable instead of:verify_account_key
. - This is personal taste and saves me an output mapping.
Login
- For completeness, we should as well accept a
Login
operation that checks the password. - Note that this performance is completely decoupled from HTTP, accessing and writing to cookies happens in the application. Come across part Two.
- A new functioning
Login
processes the data from a login form. - The login grade will transport
:email
and:password
.
module Auth::Functioning class Login < Trailblazer::Functioning step :find_user step :password_hash_match? def find_user(ctx, email:, **) # TODO: redundant with VerifyAccount#find_user. ctx[:user] = User.find_by(e-mail: e-mail) end def password_hash_match?(ctx, user:, password:, **) BCrypt::Password.new(user.password) == password # stolen from Rodauth. end # You lot could add more login logic hither, like logging log-in, logging failed attempt, and such. finish end
-
Login
tries to find the user by their email. - If found, information technology compares the provided password to the actual one.
- If they friction match,
Login
is successful. - DISCUSS: only allow verified accounts? We will comprehend that in part 2, III and IV. Excited? Me too!
- A two-in-ane test saves a lot of lawmaking and setup fourth dimension: we cover a successful login with correct electronic mail/password and the wrong password.
it "is successful with existing, active business relationship" practise outcome = K::Auth::Performance::CreateAccount.(valid_create_options) issue = Fifty::Auth::Operation::VerifyAccount.(token: result[:verify_account_token]) result = K::Auth::Operation::ResetPassword.(e-mail: "yogi@trb.to") token = result[:reset_password_token] issue = Auth::Operation::UpdatePassword.(token: token, countersign: "12345678", password_confirm: "12345678") consequence = Auth::Operation::Login.wtf?(email: "yogi@trb.to", countersign: "12345678") assert event.success? # fails with wrong password outcome = Auth::Functioning::Login.wtf?(email: "yogi@trb.to", password: "abcd") assert result.failure? end
- DISCUSS: we might change this to two
it
s. - Nosotros also need to cover the example when the user doesn't exist.
information technology "fails with unknown email" do result = Auth::Operation::Login.wtf?(email: "yogi@trb.to", countersign: "abcd") assert result.failure? cease
- This fails, which ways success!
- All operations are tested, we're ready to move to the frontend in part Ii.
discuss
inheritance => command flow not behavior ingoing approachable typing
Source: https://trailblazer.to/2.1/tutorials/book.html
Posted by: hardydocketook.blogspot.com
0 Response to "How To Change Steering Rack On 2005 Trailblazer"
Post a Comment