banner



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, and password_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 footsteps 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 or false 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 step passwords_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 the wtf? 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 called, 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? or failure?.
  • 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 a success terminus.
  • This is why - with proper input - your CreateAccount is successful.
  • If a step returns a nil or false value, the flow deviates to the "failure" track (red), leading to the failure 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 and passwords_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 with fail 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 the fail_fast finish, not going down the failure 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 both neglect 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 the workflow 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 is UNIQUE, 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() from trailblazer-macro.
  • This time, nosotros can set the error bulletin direct in the method.
  • Returning false ways any further footstepsouth 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 the CreateAccount 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 the secure_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 a RecordNotUnique 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 in verify_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 method split_token is implemented in TokenUtils. Both values are assigned to ctx variables.
  • Note that, if the splitting wasn't successful, key will be nothing. This will make the step return nil 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 our verify_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 of VerifyAccount 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 with user_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 the split 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 in reset_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 in find_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 naughtified.
  • 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 and ResetPassword) 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 nested CreateToken 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 by CreateToken.
          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 in update_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 constant ResetPasswordKey.
  • 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 to ProcessPasswords.
          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 in UpdatePassword.
          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 on fail_fast, which is rewired using fail_fast: true. TODO: explain better
  • Nosotros then fix a new state on user.
  • Nosotros update the user with a new countersign and state. 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 in Subprocess.
  • 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 apply ProcessPasswords.
          # 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 utilise CheckToken.
          # 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 its.
  • 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

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel