GithubHelp home page GithubHelp logo

jdickey / crypt_ident Goto Github PK

View Code? Open in Web Editor NEW
5.0 5.0 3.0 490 KB

Yet another fairly basic authentication Gem. (Authorisation, and batteries, sold separately.)

License: MIT License

Ruby 99.46% Shell 0.54%

crypt_ident's Introduction

Hi there πŸ‘‹

crypt_ident's People

Contributors

jdickey avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

crypt_ident's Issues

Implement 'sign up' functionality and needed precursor(s)

Implement the CryptIdent module #sign_up (Registration) method. This will need some means of accessing documented configuration details such as the Repository to use and so on. Implementing a single major API method per feature branch should give us significantly quicker turnaround than waiting to merge back into develop until everything is done. We've been there, and we'd really rather not go back, thank you very much.

Whither Hanami 1.2 support?

Given that the app this is being developed to support has successfully migrated from Hanami 1.2.0 to Hanami 1.3.0, and given that the Hanami team themselves are encouraging people to migrate existing apps and to start new apps on 1.3.0, is there any benefit served by supporting Hanami 1.2.0? This was prompted by newly bundling (see Commit cc505f7) and running tests, which spit out a deprecation warning:

### The warning below actually was presented on a single line;
### wrapped here and Gem backtrace elided for presentation as issue.
Hanami::Utils::Class.load_from_pattern! is deprecated, please use \
    Hanami::Utils::Class.load! instead - called from: \
    .../gems/hanami-controller-1.2.0/lib/hanami/controller/configuration.rb:100:in `for'.

# Running tests with run options --seed 19164:

# ... etc ...

Fix uses of Repository #find_by_name and #find_by_token methods

Integration tests β€” where we use live Hanami::Model classes for UserRepository, etc β€” have been proving embarrassingly useful for pointing out places where the mock-everything-outside unit tests have left open questions. Sometimes-careless code hasn't helped.

The latest example: how we access the UserRepository. Here's a comparison of two code excerpts: one from test/support/unit_test_model_and_repo_classes.rb which uses the "faked" Repository, and one from test/support/model_and_repo_classes.rb, which uses a Hanami::Repository subclass.

# test/support/unit_test_model_and_repo_classes.rb

class UserRepository < Repository
  # ...

  def find_by_name(name)
    select(:name, name)
  end

  def find_by_token(token)
    select(:token, token)
  end

  # ...
end

where #select was defined in the FakeRepository class as

  def select(key, value)
    @records.values.select { |other| other.to_h[key] == value }
  end

The details of @records.values aren't relevant except to note that it's an Array of Entities, which respond to #to_h to allow Hash-like selection of values, which is what's going on here. But, because it's calling Array#select, what's returned is an Array that, for the key fields used, is either empty or has a single entry. In some ORMs (e.g., ActiveRecord), this is expected behaviour; the thinking apparently goes along the line of "if we always return an Array, then the client app's logic can be more consistent". A perfectly reasonable design decision; does it map to Hanami::Model?

The test/support/model_and_repo_classes.rb, which uses a real Hanami Repository, is significantly different:

# `test/support/model_and_repo_classes.rb`

class UserRepository < Hanami::Repository
  def find_by_name(name)
    users.where(name: name).map_to(User).one
  end

  def find_by_token(token)
    users.where(token: token).map_to(User).one
  end

  # ...
end

Note that we're explicitly limiting the result to a single value, which will either be an Entity or nil.

Right; so which is correct? Well, let's take a look at the source for Hanami::Repository::Commands#find, which reads:

    # Find by primary key
    #
    # @return [Hanami::Entity,NilClass] the entity, if found
    #
    # @raise [Hanami::Model::MissingPrimaryKeyError] if the table doesn't
    #   define a primary key
    #
    # @since 0.7.0
    #
    # @example
    #   repository = UserRepository.new
    #   user       = repository.create(name: 'Luca')
    #
    #   user       = repository.find(user.id)
    def find(id)
      root.by_pk(id).as(:entity).one
    rescue => e
      raise Hanami::Model::Error.for(e)
    end

This would seem to argue for not using an Array. We'll retrofit our existing code and tests to match.

update_session_expiry breaks with insufficiently Hash-like session_data

In live code, when CryptIdent.update_session_expiry is called passing in a session object (e.g., an instance of Rack::Session::Abstract::SessionHash, then the current UpdateSessionExpiry#call method breaks because Abstract::SessionHash implements many of the methods of the standard Hash class, but #merge is not among them.

We appear to have two ways we could address this:

  1. Convert the incoming session_data parameter to an actual Hash instance and merge into that before returning it. Doing so would be consistent with our present documentation which states that the method returns a Hash; or
  2. Use the Rack::Session::Abstract::SessionHash#each instance method to loop through the data to be merged and insert each item into the session_data.

We prefer Option 1; it's simpler, easier for the reader to follow, and apparently less likely to immediately blow up in our faces if we neglect something.

The pyrotechnics in update_session_expiry appear to be the only place we exercise an unsupported method on session data, after a brief inspection of the existing code.

Address notes

There are several XXX: WTAF?!? and FIXME: notes in the code and tests. Those should be examined more closely and the notes removed, or at least reworded.

Remaining known issues with Guard

Our Guardfile and Guard usage are still far from ideal.

Known problems include, but are not necessarily limited to:

"How can people use Guard", you might reasonably ask, "if maintenance for so many of its plugins died 3-4 years ago?" The answers seems to be that (a) many non-Ruby-development Guard plugins are actively maintained, and (b) Ruby dev usage seems to now largely be limited to the minitest and rake Guard plugins, which are reasonably well-maintained as of late 2018.

Rack session access does not transparently recreate object instances.

We've just re-discovered that Rack (the server protocol underlying all reasonably-modern Ruby Web frameworks) doesn't deal nicely with objects stored in session data (which is, by default, persisted in a cookie). If you assign, say, a Hanami::Entity-subclass instance to session[:current_user] (where session is Hanami's access to Rack::Session), when you later read from session[:current_user], you'll be handed back a Hash of the Entity's attributes

Entity semantics specify that any two instances of the same Entity class with the sasme attribute values refer to the same value of the Entity, not merely equal values, so converting back to an Entity is harmless. You just need to remember to do it and, as of 0.2.0, we didn't. That must be done throughout the published API.

Identify where to initialise module instance variable @crypt_ident

This causes tests to pass that shouldn't. Specifically, the support classes include code like that of CryptIdent::UpdateSessionExpiry#initialize:

class UpdateSessionExpiry
    def initialize
      @config = CryptIdent.cryptid_config
    end
    # ...
end

Somehow this Works Just Fineβ„’ in the test code but fails horribly outside the test environment, since UpdateSessionExpiry does not include the CryptIdent module (obviously) and #cryptid_config is an instance method, not a class method. πŸ™ˆ

The module which calls the support-class code has a #cryptid_config getter. That should be passed into the classes that need it.

Should a Guest User's session NEVER expire or ALWAYS be expired?

There is a conflict in the current documentation/spec for the #restart_session_counter and #session_expired? methods.

#restart_session_counter sets the :expires_at? value in its returned Hash to the Unix epoch (00:00:00 GMT on 1 January 1970, adjusted to local time) if the passed-in session_data[:current_user] is the Guest User; it declares that any Guest User session is always expired.

#session_expired? returns false if the passed-in session_data[:current_user] is the Guest User; it declares that any Guest User session never expires.

This matters for consistency, obviously; it also matters because how one implements session validation. If the Guest User's session _never_expires, then the first line of your session-validation method would probably look like

    return restart_session_counter(session) unless session_expired?(session)

Any further code in that method would safely assume that the Current User is a Registered User who had previously Authenticated and whose session had Expired; the usual practice is to redirect to the Landing Page with a Flash message indicating that the session has Expired.

On the other hand, to make that guard clause work when the Guest User's session is always Expired would have to check the Current User explicitly:

    return restart_session_counter(session) unless guest_user?(session) || session_expired?(session)

    # ... later on down in the module...

    private

    def guest_user?(session_data)
        return true if session_data[:current_user]&.guest_user?
        session_data[:current_user].nil?
    end

The answer to the question posed by this issue should likely hinge on whether the second approach (checking for the Guest User explicitly) adds any value over the first (merely comparing timestamps).

Consider using 'monotime' Gem as basis of time measurement

Sometime in The Glorious Futureβ„’, Freaky/monotime looks like it could be quite useful, not to mention interesting. In particular, its Instant class lets you establish a point in time and then take successive readings of the time difference between "now" and when that Instant was instantiated. Before we understood the difference between the Time class as defined in Ruby core and the Time class as defined in the Standard Library, we were considering using Instant for the password_reset_sent_atattribute required byCryptIdent. That would make checking for expiration even more trivially easy than straight Time` arithmetic.

Implement CryptIdent module as boundary layer for session data wrt "worker" classes

Session data (by default) makes use of cookies via Rack::Session::Cookie, which requires that its secret values be String instances (or objects which can be implicitly converted to String). That's called serialisation, and it's often (but not necessarily) done using JSON.

What we wind up with when we store, say, a User Entity instance in session data and then later retrieve that (indirectly) from where it's been stored in a cookie or similar string-only data store, is a Hash of that Entity's attribute values, from which it's trivial (and cheap) to construct a new Entity instance representing the same value as the original.

What our support classes expect presently (as of Commit dcd8df5) for, say, session[:current_user] is an Entity instance. What they're getting is a Hash. The logical place to make that conversion is at the main CryptIdent module level, handing explicit instance values to the worker classes rather than expecting them to be able to figure out session data on their own. Because, after all, if some user of this library uses a different store than cookies for session data, we trust the Rack::Session API without having to care about how it's serialised.

Rework README before 1.0

The link from the README to the API doc doesn't work. That obviously needs fixing.

Also, the Use Case Workflows descriptive section is really too long for a README; a short summary of the API might be better. The existing workflow documentation should live on as a separate document.

Timestamp for 'password reset sent at' or for 'password reset expires at'?

Presently, we require the User Entity and the schema underlying the User Repository to have an attribute or field, respectively, named password_reset_sent_at. This was copied from the API for another authentication Gem that makes more explicit assumptions about how it's being used, e.g., in sending email messages, than our library does. Given how we're now defining #generate_reset_token as being that specific and not doing anything else, this name no longer conveys accurate information, since the code that adds that timestamp to a User has no direct role in sending an email or any other form of user communication.

Hence, renaming the field as password_reset_expires_at makes more sense. It also makes it much simpler to determine whether a Password Reset request has actually Expired, since no processing beyond comparing the current time to the timestamp need be done; the policy is applied on the front rather than back end of that flow.

This would continue to make use of the configuration-specified :reset_expiry value in computing that value.

Finally, that should probably be a GMT/UTC timestamp rather than a server-local-time timestamp (as Ruby's time.now returns by default).

Rework methods so that success/failure handling is more consistent and meaningful

This was triggered by reading this blog post and seeing the example snippet that demonstrates how to use Dry::Monads::Result and Dry::Matcher to do just this. A single block that, given a result object, lets you fluently handle success and failure results.

Brilliant, and worth exploring. Or is there anyone who thinks that the mismatch between our#sign_up method and everything else doesn't suck with a particularly hard self-inflicted vacuum?

Utility to rewrite .gemspec for dependency versions

There's a note in the crypt_ident.gemspec file reminding us that

NOTE: During development, we're not specifying Gem versions, because we're bundling locally using bin/setup and scripts/build-gem-list.rb. Before merging back to master, at latest, this must be updated to show minimum Gem versions; e.g., '>= 3.1.12' for bcrypt. It Would Be Very Nice if we had a script to automatically rewrite the Gemspec for us each way. PRs welcome.

So, basically, this utility, let's call it gemspec_versions, would take one of two command-line parameters:

  • --lock-down would rewrite each add_runtime_dependency and add_development_dependency specification in the .gemspec to specify a version greater than or equal to the version in the present Gemfile.lock. For example (as of mid-December, 2018), it would rewrite the line
  spec.add_development_dependency "minitest"

to

  spec.add_development_dependency "minitest", '5.11.3'
  • --unlock would rewrite each dependency specification which includes a version specification to omit the version specifier, exactly reversing the --lock option.

Given a locked-down .gemspec and a Gemfile.lock file, running the commands gemspec_versions --unlock && gemspec_versions --lock must produce a .gemspec that matches the original.

Marking this as TGF, though having it for the next update cycle would be a Very Good Thing.

Signing up should always generate a default random password.

You could argue either way: is this a combination of two bugs, or does fixing the bug in the title allow us to also make an enhancement? Regardless, we're about to describe at least one bug.

Currently (as of Commit 8db7906 on develop), we call SecureRandom.urlsafe_base64 to generate a password if none was specified. This raises two points:

  1. #sign_up isn't supposed to accept a password in the passed-in attribs. We're supposed to generate a new user with the specified name, email address, and profile text (if any) and a random password, and then send the new user an email with a link that will let her/him change their password so they can sign in. This is to verify that users both intend to sign up and supply a correct email address. Also, nobody uses good passwords in a new-user form.
  2. Since SecureRandom.urlsafe_base64 generates a random sequence of bytes, its output isn't acceptable to Base64.strict_encode (and encoding it with Base64.encode, which is essentially part of what BCrypt::Password.create does internally, produces a "Base64" string that can't be decoded by Base64.strict_encode. Why do we care about that, you ask? While TDDing CryptIdent#generate_reset_token, we looked at decoding the generated token (which is generated in the same way as the sign-up password). Nope; not valid Base64, so tests verifying that it is will fail.

To close this issue, the following must happen:

In CryptIdent::SignUp

  1. CryptIdent::SignUp#hashed_password must not accept a password parameter and must always generate a random Clear-Text Password;
  2. That generated random Clear-Text Password should use SecureRandom.alphanumeric or equivalent, which generates ASCII-normal characters acceptable as input to Base64.strict_encode;
  3. That generated random password also must use the config-default token_bytes to specify the length of the Clear-Text Password to generate.

In CryptIdent::GenerateResetToken

  1. CryptIdent::GenerateResetToken (forthcoming) must use the same mechanism as in item 2 above (SecureRandom.alphanumeric or equivalent) and also must use the config-default token_bytes to specify the length of the token string being generated before being encoded.

Being in a hurry always slows you down, eventually.

Fix docs

This has two aspects to it:

  • Fixing the YARD-related tooling so that the generated docs (from the source files; the README is parsed differently, apparently) include all necessary GFM features including back-ticks for marking monospace text, GFM table notation, and so on.
  • Reworking the README to include a comprehensive set of simplified use-case explanations that do not simply repeat the API documentation, and

The problem may have come about because our initial Gem setup/YARD plugin setup was cargo-culted from another project somewhere that is now out-of-date due to GitHub infrastructure changes. Project Paper Cuts is awesome, but it clearly isn't the sum total of moving parts in the GitHub documentation infrastructure.

Support PREA and Token in separate table from Users

To avoid cluttering up the users table with fields that are only used in specific use cases, app developers may well prefer to create separate tables to support specific use cases, such as resetting/changing passwords. Configuration should be extended to add a password_reset_table attribute. Defaulting to null, it must be set to a table name if that table should be used to access the password_reset_expires_at and token attributes.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.