Created
August 3, 2017 07:55
-
-
Save InsilicoSoft/e408db6d8f5eab6ab22ad059b5c3fdcf to your computer and use it in GitHub Desktop.
Custom Jwt Token encoder/decoder that takes care of confirmations and resets too.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| require 'base64' | |
| ## | |
| # A class for encoding and decoding auth information in form of JWT token. Note that only sensitive auth information | |
| # such as user id and/or email should be encrypted in token, not any possible api payload | |
| # @example Encrypt user's id and email | |
| # JwtCrypto.encode({ user: { id: 1, email: '[email protected]' }}) # => returns JWT token | |
| # @example Decode payload from provided JWT token | |
| # JwtCrypto.decode('hhjkasdhkja.hdjsakdhak.hasjdkhasjkd') # => returns decoded payload or fails with DecodeError | |
| class JwtCrypto | |
| # This error should be raised when there are troubles with decoding the JWT token. The error should be handled | |
| # at ApplicationController or whichever root controller there is. | |
| class DecodeError < StandardError; end | |
| class << self | |
| # Encodes payload with provided api secret. Please specify the secret in +config/secrets.yml+ | |
| # | |
| # @param [Hash] payload | |
| # @param [DateTime] exp token expiry date | |
| # @return [String] the encoded payload | |
| def encode(payload, exp = nil) | |
| exp ||= 5.days.from_now | |
| JWT.encode(payload.merge(exp: exp.to_i), Rails.application.secrets.api_secret) | |
| end | |
| # Encodes +payload+, SHA256-encrypting the values in it or only the values provided by +keys+ | |
| # | |
| # @param [Hash] payload | |
| # @param [DateTime] exp token expiry date | |
| # @param [Array<String|Symbol>] keys which corresponding values to encrypt. Default - all values. | |
| # @return [String] encoded payload, @see ::encode | |
| def encode_encrypting_values(payload, exp = nil, keys = nil) | |
| keys ||= payload.keys | |
| hash_with_encrypted_values = payload.select { |k, _| keys.include?(k) }.map do |k, v| | |
| [k, Digest::SHA256.hexdigest(v)] | |
| end.to_h | |
| encode(hash_with_encrypted_values, exp) | |
| end | |
| # Decodes JWT token and returns the decoded data. We return data in form of hash with indifferent access to not | |
| # worry about providing symbol or string keys for it. | |
| # | |
| # @param [String] token - JWT token | |
| # @return [ActiveSupport::HashWithIndifferentAccess] decoded payload body | |
| # @raise [DecodeError] if any other error is encountered - such as absence of Rails application secret, wrong | |
| # or expired token, decoded data that doesn't have Hash format, etc. | |
| def decode(token) | |
| body = JWT.decode(token, Rails.application.secrets.api_secret)[0] | |
| ActiveSupport::HashWithIndifferentAccess.new body.except('exp') | |
| rescue StandardError => e | |
| Rails.logger.fatal "JwtCrypto error: #{e}" | |
| raise DecodeError | |
| end | |
| # Decodes JWT +token+ and compares SHA256 hashes of +compared_payload+'s values with retrieved values | |
| # (which are supposed to already be encrypted) | |
| # | |
| # @param [String] token - JWT token | |
| # @param [Hash] compared_payload - payload with unencrypted values that need to be compared. e.g. { key: '2345678' } | |
| # @param [Boolean] soft - if set to false, a +DecodeError+ will be raised. Otherwise, +nil+ is returned in event | |
| # of error - if token cannot be decoded or encrypted values do not match. | |
| # @return [Hash|NilClass] decoded payload or +nil+ if an error occurred. | |
| def compare_value_digests!(token, compared_payload, soft = true) | |
| actual_payload = decode(token) | |
| equal = compared_payload.all? { |k, v| compare_with_digest(actual_payload[k], v) } | |
| raise DecodeError unless equal | |
| actual_payload | |
| rescue StandardError => e | |
| Rails.logger.fatal "JwtCrypto error: #{e}" | |
| raise DecodeError unless soft | |
| nil | |
| end | |
| # Compares one value with digest of another | |
| # | |
| # @param [String] digest SHA256 digest to be compared | |
| # @param [String] undigested string, whose SHA256 digest will be compared | |
| # @return [Boolean] result of comparison | |
| def compare_with_digest(digest, undigested) | |
| digest == Digest::SHA256.hexdigest(undigested) | |
| end | |
| # Decodes JWT token without raising an error - instead, returns +nil+ if decoding is impossible @see +decode+ | |
| # @return [ActiveSupport::HashWithIndifferentAccess | NilClass] decoded payload body or +nil+ | |
| def soft_decode(token) | |
| decode(token) | |
| rescue DecodeError | |
| nil | |
| end | |
| # :method confirmation_token_for / reset_password_token_for | |
| # Generates confirmation / reset password token with encrypted user's id and email | |
| # | |
| # @param [User] user | |
| # @return [String] token | |
| def confirmation_token_for(user) | |
| encode(user: { id: user.id, unconfirmed_email: user.unconfirmed_email }, type: :confirmation) | |
| end | |
| def reset_password_token_for(user) | |
| encode(user: { id: user.id }, type: :reset_password) | |
| end | |
| # :method decoded_user_from_confirmation / decoded_user_from_reset_password | |
| # Decodes confirmation /reset password token and returns user id and email in hash | |
| # | |
| # @param [String] token - JWT token | |
| # @return [Hash] decoded user: | |
| # * :id [Integer] - user id | |
| # * :email [String] - user email | |
| # @raise [DecodeError] if there are errors in decoding process (@see +decode+) or decoded payload hash does not | |
| # contain type of *confirmation* or *reset_password* | |
| def decoded_user_from_confirmation(token) | |
| user_data = decode token | |
| user = user_data[:user] | |
| raise DecodeError unless user.present? && user.keys == %w(id unconfirmed_email) && | |
| user_data[:type]&.to_sym == :confirmation | |
| user | |
| end | |
| def decoded_user_from_reset_password(token) | |
| user_data = decode token | |
| user = user_data[:user] | |
| raise DecodeError unless user.present? && user.keys == %w(id) && user_data[:type]&.to_sym == :reset_password | |
| user | |
| end | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment