token_auth 0.3.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +111 -0
  4. data/Rakefile +52 -0
  5. data/app/controllers/token_auth/api/authentication_tokens_controller.rb +53 -0
  6. data/app/controllers/token_auth/api/base_controller.rb +7 -0
  7. data/app/controllers/token_auth/api/payloads_controller.rb +108 -0
  8. data/app/controllers/token_auth/authentication_tokens_controller.rb +45 -0
  9. data/app/controllers/token_auth/base_controller.rb +5 -0
  10. data/app/controllers/token_auth/concerns/api_resources.rb +51 -0
  11. data/app/controllers/token_auth/concerns/cors_settings.rb +16 -0
  12. data/app/controllers/token_auth/configuration_tokens_controller.rb +49 -0
  13. data/app/controllers/token_auth/tokens_controller.rb +13 -0
  14. data/app/models/token_auth/authentication_token.rb +33 -0
  15. data/app/models/token_auth/configuration_token.rb +65 -0
  16. data/app/models/token_auth/payload.rb +77 -0
  17. data/app/models/token_auth/synchronizable_resource.rb +34 -0
  18. data/app/models/token_auth/uuid_enabled.rb +15 -0
  19. data/app/serializers/token_auth/synchronizable_resource_serializer.rb +8 -0
  20. data/app/views/token_auth/tokens/index.html.erb +55 -0
  21. data/config/locales/en.yml +31 -0
  22. data/config/locales/es-PE.yml +31 -0
  23. data/config/locales/pt-BR.yml +31 -0
  24. data/config/routes.rb +18 -0
  25. data/db/migrate/20150428210721_create_configuration_tokens.rb +21 -0
  26. data/db/migrate/20150428211137_create_authentication_tokens.rb +25 -0
  27. data/db/migrate/20151229184253_create_token_auth_synchronizable_resources.rb +25 -0
  28. data/lib/tasks/token_auth_server_rails_tasks.rake +5 -0
  29. data/lib/token_auth/engine.rb +7 -0
  30. data/lib/token_auth/version.rb +5 -0
  31. data/lib/token_auth.rb +9 -0
  32. metadata +229 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 064111700731121265d4aada907cee407a956a06
4
+ data.tar.gz: d3b86f096bd944e54e154c9da276f304617946e1
5
+ SHA512:
6
+ metadata.gz: 5bb1c3bf891008100dc89ca0e9996c04b4fe0e1e301a7d9ecc7915e6e73675794ba553d5accaf1750441308caa3116d61a6457eb9001ceb58c82a627f0df364b
7
+ data.tar.gz: 5a2d00439e671fa715bb52de55a89f5f8146592d922155a81350224af4b52095e434900103e3accd44333637dabd60ca0c43215f072be3412717428069f088df
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 Eric Carty-Fickes
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # Token Authentication
2
+
3
+ This Rails Engine implements a simple configuration handshake with clients. It
4
+ also provides Controller Concerns for authenticating clients via an API.
5
+
6
+ ## Overview
7
+
8
+ The basic workflow is as follows:
9
+
10
+ 1. A user generates a Configuration Token within your host application tied to
11
+ an "entity" within the application. This entity will likely represent a human
12
+ user.
13
+ 1. The Configuration Token is shared with the client you wish to authorize.
14
+ 1. If the Configuration Token is transmitted along with a unique client
15
+ identifier while the Token is still valid, the Configuration Token will be
16
+ destroyed and an Authentication Token will be created and returned to the
17
+ client.
18
+ 1. The Authentication Token can be used to authenticate subsequent requests, as
19
+ long as it is accompanied by the unique client identifier.
20
+ 1. The Authentication Token can be disabled temporarily or permanently, and it
21
+ can be destroyed. If it is destroyed, this process must be repeated.
22
+
23
+ ## Installation
24
+
25
+ Add the gem to your Gemfile:
26
+
27
+ ```ruby
28
+ # Gemfile
29
+
30
+ gem 'token_auth',
31
+ git: 'https://github.com/NU-CBITS/token_auth_server_rails',
32
+ tag: '0.1.1'
33
+ ```
34
+
35
+ Install it:
36
+
37
+ ```
38
+ bundle
39
+ ```
40
+
41
+ Install and run the migrations:
42
+
43
+ ```
44
+ rake token_auth:install:migrations; rake db:migrate
45
+ ```
46
+
47
+ Mount the Engine and make its routes available to the host application:
48
+
49
+ ```ruby
50
+ # config/routes.rb
51
+
52
+ mount TokenAuth::Engine => '/token_auth'
53
+ ```
54
+
55
+ ## Configuration
56
+
57
+ To customize the behavior of the token management functions, you can override
58
+ the `BaseController` class:
59
+
60
+ ```ruby
61
+ # app/controllers/token_auth/base_controller.rb
62
+
63
+ module TokenAuth
64
+ class BaseController < ApplicationController
65
+ before_action :authenticate_user!
66
+ end
67
+ end
68
+ ```
69
+
70
+ To authenticate an API controller:
71
+
72
+ ```ruby
73
+ # app/controllers/api/my_controller.rb
74
+
75
+ module Api
76
+ class MyController < ActionController::Base
77
+ include TokenAuth::Concerns::ApiResources
78
+
79
+ after_action do |controller|
80
+ controller.cors_set_access_control_headers(allow_methods: "GET, OPTIONS")
81
+ end
82
+
83
+ def show
84
+ end
85
+ end
86
+ end
87
+ ```
88
+
89
+ ## Testing
90
+
91
+ After generating a configuration token, test authentication token generation:
92
+
93
+ ```
94
+ curl --data "data[clientUuid]=asdf&configurationToken=4C3LM9" http://localhost:3000/token_auth/api/authentication_tokens
95
+ ```
96
+
97
+ ## Development
98
+
99
+ Clone the repository and install dependencies:
100
+
101
+ ```
102
+ git clone git@github.com:NU-CBITS/token_auth_server_rails.git
103
+ bundle
104
+ ```
105
+
106
+ Run the unit tests and linters for this Engine:
107
+
108
+ ```
109
+ RAILS_ENV=test bin/rake db:create db:migrate
110
+ bin/rake
111
+ ```
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ begin
3
+ require "bundler/setup"
4
+ rescue LoadError
5
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
6
+ end
7
+
8
+ require "rdoc/task"
9
+
10
+ RDoc::Task.new(:rdoc) do |rdoc|
11
+ rdoc.rdoc_dir = "rdoc"
12
+ rdoc.title = "TokenAuth"
13
+ rdoc.options << "--line-numbers"
14
+ rdoc.rdoc_files.include("lib/**/*.rb")
15
+ end
16
+
17
+ require "rspec/core/rake_task"
18
+
19
+ desc "Run all specs in spec directory (excluding plugin specs)"
20
+ RSpec::Core::RakeTask.new(:spec)
21
+
22
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
23
+ load "rails/tasks/engine.rake"
24
+
25
+ Bundler::GemHelper.install_tasks
26
+
27
+ require "rubocop/rake_task"
28
+
29
+ RuboCop::RakeTask.new
30
+
31
+ dir = File.dirname(__FILE__)
32
+
33
+ desc "Run Brakeman"
34
+ task :brakeman do
35
+ puts `#{ File.join(dir, "bin", "brakeman") } #{ File.join(dir, ".") }`
36
+ end
37
+
38
+ desc "Run MarkDown lint"
39
+ task :mdl do
40
+ results = `#{ File.join(dir, "bin", "mdl") } #{ File.join(dir, "README.md") }`
41
+ unless results.strip.empty?
42
+ puts results
43
+ exit 1
44
+ end
45
+ end
46
+
47
+ task :default do
48
+ Rake::Task["spec"].invoke
49
+ Rake::Task["rubocop"].invoke
50
+ Rake::Task["brakeman"].invoke
51
+ Rake::Task["mdl"].invoke
52
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuth
4
+ module Api
5
+ # API to manage Authentication Tokens.
6
+ class AuthenticationTokensController < ::TokenAuth::Api::BaseController
7
+ RESOURCE_TYPE = "authenticationTokens"
8
+
9
+ include Concerns::CorsSettings
10
+
11
+ after_action do |controller|
12
+ controller.cors_set_access_control_headers(
13
+ allow_methods: "POST, OPTIONS"
14
+ )
15
+ end
16
+
17
+ def options
18
+ render json: {}, status: 200
19
+ end
20
+
21
+ def create
22
+ if configuration_token && authentication_token
23
+ render(json: {
24
+ data: {
25
+ type: RESOURCE_TYPE,
26
+ id: authentication_token.uuid,
27
+ value: authentication_token.value
28
+ }
29
+ },
30
+ status: 201)
31
+ else
32
+ render json: {}, status: 400
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def authentication_token
39
+ @authentication_token ||=
40
+ configuration_token.make_authentication_token(token_params)
41
+ end
42
+
43
+ def token_params
44
+ { client_uuid: (params[:data] || {})[:clientUuid] }
45
+ end
46
+
47
+ def configuration_token
48
+ @configuration_token ||=
49
+ ConfigurationToken.find_match(params[:configurationToken])
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ module TokenAuth
3
+ module Api
4
+ class BaseController < ActionController::Base
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+ require "openssl"
3
+
4
+ module TokenAuth
5
+ module Api
6
+ # Processes inbound and outbound resources.
7
+ class PayloadsController < ActionController::Base
8
+ include TokenAuth::Concerns::CorsSettings
9
+
10
+ attr_reader :authentication_token
11
+
12
+ rescue_from TokenAuth::Concerns::ApiResources::Api::Unauthorized,
13
+ with: :unauthorized
14
+
15
+ before_action do |controller|
16
+ controller.cors_set_access_control_headers(
17
+ allow_methods: "GET, POST, OPTIONS",
18
+ allow_headers: "Authorization"
19
+ )
20
+ end
21
+
22
+ def index
23
+ authenticate!
24
+ render json: pullable_records,
25
+ meta: { timestamp: Time.zone.now.iso8601 }
26
+ end
27
+
28
+ def options
29
+ render json: {}, status: 200
30
+ end
31
+
32
+ def create
33
+ authenticate!
34
+ payload = Payload.new(entity_id: authentication_token.entity_id)
35
+ payload.save request_data
36
+
37
+ headers["Errors"] = payload.errors.join(", ")
38
+ render json: payload.valid_resources, status: 201
39
+ rescue TokenAuth::Payload::MalformedPayloadError
40
+ render json: {}, status: 400
41
+ end
42
+
43
+ private
44
+
45
+ def find_token!
46
+ @authentication_token = TokenAuth::AuthenticationToken.find_by(
47
+ client_uuid: @metadata[:key],
48
+ is_enabled: true
49
+ )
50
+
51
+ return if @authentication_token
52
+
53
+ raise TokenAuth::Concerns::ApiResources::Api::Unauthorized
54
+ end
55
+
56
+ def request_data
57
+ return nil unless request.post?
58
+
59
+ params[:data] || []
60
+ end
61
+
62
+ def calculate_signature
63
+ OpenSSL::Digest::MD5.hexdigest([
64
+ request_data ? request_data.to_json : nil,
65
+ @metadata[:key],
66
+ @metadata[:nonce],
67
+ @metadata[:timestamp],
68
+ @metadata[:url],
69
+ @metadata[:method],
70
+ authentication_token.value
71
+ ].compact.join)
72
+ end
73
+
74
+ def split_header
75
+ if request.headers[:Authorization].nil?
76
+ raise TokenAuth::Concerns::ApiResources::Api::Unauthorized
77
+ end
78
+
79
+ auth_headers = request.headers[:Authorization].split(",")
80
+ @metadata = auth_headers.each_with_object({}) do |h, metadata|
81
+ p = h.split("=")
82
+ metadata[p[0].to_sym] = p[1].gsub(/(^")|("$)/m, "")
83
+ end
84
+ end
85
+
86
+ def authenticate!
87
+ split_header
88
+ find_token!
89
+
90
+ return if @metadata[:signature] == calculate_signature
91
+
92
+ raise TokenAuth::Concerns::ApiResources::Api::Unauthorized
93
+ end
94
+
95
+ def pullable_records
96
+ entity_id = authentication_token.entity_id
97
+ SynchronizableResource.pullable_records_for(
98
+ entity_id: entity_id,
99
+ filter: params[:filter]
100
+ )
101
+ end
102
+
103
+ def unauthorized
104
+ render json: {}, status: 401
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ module TokenAuth
3
+ # Manages Authentication Tokens.
4
+ class AuthenticationTokensController < ::TokenAuth::BaseController
5
+ def update
6
+ token = AuthenticationToken.find_by_entity_id(params[:entity_id])
7
+
8
+ if token.update(token_params)
9
+ redirect_to tokens_url(token.entity_id),
10
+ notice: t("activerecord.success_saving",
11
+ name: token.class.model_name.human)
12
+ else
13
+ redirect_to tokens_url(token.entity_id),
14
+ alert: t("activerecord.failure_saving",
15
+ name: token.class.model_name.human,
16
+ errors: errors_on(token))
17
+ end
18
+ end
19
+
20
+ def destroy
21
+ token = AuthenticationToken.find_by_entity_id(params[:entity_id])
22
+
23
+ if token.destroy
24
+ redirect_to tokens_url(token.entity_id),
25
+ notice: t("activerecord.success_destroying",
26
+ name: token.class.model_name.human)
27
+ else
28
+ redirect_to tokens_url(token.entity_id),
29
+ alert: t("activerecord.failure_destroying",
30
+ name: token.class.model_name.human,
31
+ errors: errors_on(token))
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def errors_on(model)
38
+ model.errors.full_messages.join ", "
39
+ end
40
+
41
+ def token_params
42
+ params.permit(:is_enabled)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ module TokenAuth
3
+ class BaseController < ApplicationController
4
+ end
5
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ module TokenAuth
3
+ module Concerns
4
+ # Behavior related to authentication and CORS.
5
+ module ApiResources
6
+ module Api
7
+ class Unauthorized < StandardError
8
+ end
9
+ end
10
+
11
+ extend ActiveSupport::Concern
12
+
13
+ include ::TokenAuth::Concerns::CorsSettings
14
+
15
+ included do
16
+ before_action :authenticate_token!
17
+
18
+ rescue_from Api::Unauthorized, with: :unauthorized
19
+ end
20
+
21
+ AUTH_HEADER = "X-AUTH-TOKEN"
22
+
23
+ def options
24
+ render nothing: true
25
+ end
26
+
27
+ private
28
+
29
+ def authenticate_token!
30
+ @authentication_token = client_uuid && auth_header &&
31
+ ::TokenAuth::AuthenticationToken
32
+ .find_enabled(client_uuid: client_uuid,
33
+ value: auth_header)
34
+
35
+ raise Api::Unauthorized unless @authentication_token
36
+ end
37
+
38
+ def client_uuid
39
+ params[:clientUuid]
40
+ end
41
+
42
+ def auth_header
43
+ request.headers[AUTH_HEADER]
44
+ end
45
+
46
+ def unauthorized
47
+ render json: {}, status: 401
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module TokenAuth
3
+ module Concerns
4
+ # Allow cross-domain requests.
5
+ module CorsSettings
6
+ def cors_set_access_control_headers(allow_methods:, allow_headers: "")
7
+ headers["Access-Control-Allow-Origin"] = "*"
8
+ headers["Access-Control-Allow-Methods"] = allow_methods
9
+ headers["Access-Control-Allow-Headers"] = [
10
+ "Content-Type",
11
+ allow_headers
12
+ ].join(", ")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ module TokenAuth
3
+ # Manages Configuration Tokens.
4
+ class ConfigurationTokensController < ApplicationController
5
+ def create
6
+ token = ConfigurationToken.new(entity_id: params[:entity_id])
7
+
8
+ if token.save
9
+ redirect_to tokens_index,
10
+ notice: t("activerecord.success_saving",
11
+ name: token.class.model_name.human)
12
+ else
13
+ redirect_to tokens_index,
14
+ alert: t("activerecord.failure_saving",
15
+ name: token.class.model_name.human,
16
+ errors: errors_on(token))
17
+ end
18
+ end
19
+
20
+ def destroy
21
+ token = ConfigurationToken.find_by_entity_id(params[:entity_id])
22
+
23
+ if token.nil?
24
+ redirect_to tokens_index,
25
+ alert: t("activerecord.cannot_find",
26
+ name: ConfigurationToken.model_name.human)
27
+ elsif token.destroy
28
+ redirect_to tokens_index,
29
+ notice: t("activerecord.success_destroying",
30
+ name: ConfigurationToken.model_name.human)
31
+ else
32
+ redirect_to tokens_index,
33
+ alert: t("activerecord.failure_destroying",
34
+ name: ConfigurationToken.model_name.human,
35
+ errors: errors_on(token))
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def tokens_index
42
+ tokens_url params[:entity_id]
43
+ end
44
+
45
+ def errors_on(model)
46
+ model.errors.full_messages.join ", "
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ module TokenAuth
3
+ # Manage configuration and authentication tokens.
4
+ class TokensController < ::TokenAuth::BaseController
5
+ def index
6
+ @entity_id = params[:entity_id]
7
+ @authentication_token = AuthenticationToken
8
+ .find_by_entity_id(@entity_id)
9
+ @configuration_token = ConfigurationToken
10
+ .find_by_entity_id(@entity_id)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ module TokenAuth
3
+ # A key shared with a client for authenticating requests.
4
+ class AuthenticationToken < ActiveRecord::Base
5
+ TOKEN_LENGTH = 32
6
+ UUID_LENGTH = 36
7
+
8
+ validates :entity_id, :value, :uuid, :client_uuid, presence: true
9
+ validates :value, :entity_id, :client_uuid, uniqueness: true
10
+ validates :value, length: { is: TOKEN_LENGTH }
11
+ validates :uuid, length: { is: UUID_LENGTH }
12
+ validates :is_enabled, inclusion: { in: [true, false] }
13
+
14
+ before_validation :set_value, :set_uuid, on: :create
15
+
16
+ def self.find_enabled(client_uuid:, value:)
17
+ find_by(client_uuid: client_uuid, value: value, is_enabled: true)
18
+ end
19
+
20
+ private
21
+
22
+ def set_value
23
+ loop do
24
+ self.value = SecureRandom.hex(TOKEN_LENGTH / 2)
25
+ break unless self.class.exists?(value: value)
26
+ end
27
+ end
28
+
29
+ def set_uuid
30
+ self.uuid = SecureRandom.uuid
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuth
4
+ # A single use human readable token for use with client configuration.
5
+ class ConfigurationToken < ActiveRecord::Base
6
+ mattr_accessor :valid_period
7
+ self.valid_period = 4.hours
8
+ SAMPLE_SET = %w( A B C D E F H J K L M N P Q R S T U V W X Y Z
9
+ 2 3 4 5 7 8 9
10
+ # $ ).freeze
11
+ TOKEN_LENGTH = 6
12
+
13
+ validates :expires_at, :entity_id, :value, presence: true
14
+ validates :value, :entity_id, uniqueness: true
15
+
16
+ before_validation :set_value, :set_expires_at, on: :create
17
+
18
+ # Returns case insensitive match.
19
+ # rubocop:disable Rails/FindBy
20
+ def self.find_match(value)
21
+ return nil unless value.is_a?(String)
22
+
23
+ query = value.gsub(/[\s]+/, "")
24
+
25
+ return nil unless query.length == TOKEN_LENGTH
26
+
27
+ where(arel_table[:expires_at].gt(Time.zone.now))
28
+ .where(arel_table[:value].matches(query))
29
+ .first
30
+ end
31
+ # rubocop:enable Rails/FindBy
32
+
33
+ def make_authentication_token(client_uuid:)
34
+ return nil if expired?
35
+
36
+ authentication_token = AuthenticationToken
37
+ .new(entity_id: entity_id,
38
+ client_uuid: client_uuid)
39
+ transaction do
40
+ authentication_token.save! && destroy!
41
+ end
42
+
43
+ authentication_token
44
+ rescue
45
+ nil
46
+ end
47
+
48
+ def expired?
49
+ Time.zone.now > expires_at
50
+ end
51
+
52
+ private
53
+
54
+ def set_value
55
+ loop do
56
+ self.value = (0...TOKEN_LENGTH).map { SAMPLE_SET.sample }.join
57
+ break unless self.class.exists?(value: value)
58
+ end
59
+ end
60
+
61
+ def set_expires_at
62
+ self.expires_at = Time.zone.now + self.class.valid_period
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+ module TokenAuth
3
+ # An inbound resource. Inbound resource requests are validated and an
4
+ # attempt is made to upsert them.
5
+ class Payload
6
+ attr_reader :valid_resources, :errors
7
+
8
+ class MalformedPayloadError < StandardError
9
+ end
10
+
11
+ def self.resource_type(params)
12
+ params.extract!(:type)[:type]
13
+
14
+ rescue NoMethodError
15
+ nil
16
+ end
17
+
18
+ def initialize(entity_id:)
19
+ @entity_id = entity_id
20
+ @valid_resources = []
21
+ @errors = []
22
+ end
23
+
24
+ def save(entities_params)
25
+ raise MalformedPayloadError unless entities_params.respond_to?(:each)
26
+
27
+ entities_params.each do |entity_params|
28
+ type = self.class.resource_type(entity_params)
29
+ resource = find_pushable_resource(type)
30
+
31
+ if resource
32
+ upsert_resource(
33
+ klass: resource.klass,
34
+ params: entity_params.merge(
35
+ "#{ resource.entity_id_attribute_name }": @entity_id
36
+ )
37
+ )
38
+ else
39
+ @errors << "invalid resource '#{ type }'"
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def find_pushable_resource(type)
47
+ SynchronizableResource.find_by(
48
+ name: type,
49
+ is_pushable: true,
50
+ entity_id: @entity_id
51
+ )
52
+ end
53
+
54
+ def upsert_resource(klass:, params:)
55
+ attributes = deserialize(params)
56
+ resource = klass.find_or_initialize_by(uuid: attributes.delete("uuid"))
57
+
58
+ if resource.update(attributes)
59
+ @valid_resources << resource
60
+ else
61
+ @errors << resource.errors.full_messages.join(", ")
62
+ end
63
+
64
+ rescue ActiveRecord::UnknownAttributeError => error
65
+ @errors << error.message
66
+ end
67
+
68
+ def deserialize(params)
69
+ params.each_with_object({}) do |pair, attrs|
70
+ name, value = pair
71
+ name = name.to_s.underscore
72
+ name = "uuid" if name == "id"
73
+ attrs[name] = value
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ module TokenAuth
3
+ # A resource that may be pushed and/or pulled by an entity.
4
+ class SynchronizableResource < ActiveRecord::Base
5
+ include UuidEnabled
6
+
7
+ validates :uuid, :entity_id, :entity_id_attribute_name, :name, :class_name,
8
+ presence: true
9
+ validates :is_pullable, :is_pushable, inclusion: { in: [true, false] }
10
+
11
+ def self.pullable_records_for(entity_id:, filter:)
12
+ where(is_pullable: true, entity_id: entity_id)
13
+ .map { |r| apply_filter(r.records, filter) }
14
+ .flatten
15
+ end
16
+
17
+ def self.apply_filter(relation, filter)
18
+ return relation unless filter && filter[:updated_at] &&
19
+ filter[:updated_at][:gt]
20
+
21
+ timestamp = filter[:updated_at][:gt]
22
+
23
+ relation.where(relation.arel_table[:updated_at].gt(timestamp))
24
+ end
25
+
26
+ def records
27
+ klass.where(entity_id_attribute_name => entity_id)
28
+ end
29
+
30
+ def klass
31
+ class_name.constantize
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ require "active_support/concern"
3
+
4
+ module TokenAuth
5
+ # Create a uuid, and add validation for format and presence of uuid.
6
+ module UuidEnabled
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ before_validation { self.uuid ||= SecureRandom.uuid }
11
+
12
+ validates :uuid, length: { is: 36 }, uniqueness: true, presence: true
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ module TokenAuth
3
+ # Serializer for SynchronizableResource.
4
+ class SynchronizableResourceSerializer < ActiveModel::Serializer
5
+ attributes :name
6
+ attribute :uuid, key: :id
7
+ end
8
+ end
@@ -0,0 +1,55 @@
1
+ <h2><%= t(".manage_tokens_for", default: "Manage tokens for") %> <%= @entity || @entity_id %></h2>
2
+
3
+ <table class="table">
4
+ <% unless @authentication_token %>
5
+ <tr id="config-token">
6
+ <td class="lead">
7
+ <%= TokenAuth::ConfigurationToken.model_name.human %>
8
+ </td>
9
+ <td>
10
+ <% if @configuration_token %>
11
+ <% if @configuration_token.expired? %>
12
+ <span class="label label-warning"><%= t(".expired", default: "Expired") %></span>
13
+ <% else %>
14
+ <%= t(".expires_in", default: "Expires in") %> <%= time_ago_in_words @configuration_token.expires_at %>
15
+ <% end %>
16
+ <% else %>
17
+ <%= button_to t(".create", default: "Create"), configuration_token_path(@entity_id), class: "btn btn-default" %>
18
+ <% end %>
19
+ </td>
20
+ <% if @configuration_token %>
21
+ <td>
22
+ <% unless @configuration_token.expired? %>
23
+ <%= t(".value", default: "Value") %> <mark class="lead"><%= @configuration_token.value %></mark>
24
+ <p><small><%= t(".case_insensitive", default: "case insensitive") %></small></p>
25
+ <% end %>
26
+ </td>
27
+ <td>
28
+ <%= button_to t(".destroy", default: "Destroy"), token_auth.configuration_token_path(@entity_id), method: :delete, class: "btn btn-warning" %>
29
+ </td>
30
+ <% end %>
31
+ </tr>
32
+ <% else %>
33
+ <tr id="auth-token">
34
+ <td class="lead">
35
+ <%= @authentication_token.model_name.human %>
36
+ <% unless @authentication_token.is_enabled? %>
37
+ <span class="label label-danger"><%= t(".disabled", default: "Disabled") %></span>
38
+ <% end %>
39
+ </td>
40
+ <td>
41
+ <%= t("activerecord.attributes.token_auth/authentication_token.client_uuid", default: "Client uuid") %> <%= @authentication_token.client_uuid %>
42
+ </td>
43
+ <td>
44
+ <% if @authentication_token.is_enabled? %>
45
+ <%= button_to t(".disable", default: "Disable"), authentication_token_path(@entity_id), method: :patch, class: "btn btn-warning", params: { is_enabled: false } %>
46
+ <% else %>
47
+ <%= button_to t(".enable", default: "Enable"), authentication_token_path(@entity_id), method: :patch, class: "btn btn-info", params: { is_enabled: true } %>
48
+ <% end %>
49
+ </td>
50
+ <td>
51
+ <%= button_to t(".destroy", default: "Destroy"), authentication_token_path(@entity_id), method: :delete, class: "btn btn-danger", data: { confirm: t(".are_you_sure", default: "Are you sure?") } %>
52
+ </td>
53
+ </tr>
54
+ <% end %>
55
+ </table>
@@ -0,0 +1,31 @@
1
+ en:
2
+ activerecord:
3
+ attributes:
4
+ token_auth/authentication_token:
5
+ client_uuid: Client uuid
6
+ cannot_find: Unable to find %{name}
7
+ success_saving: Successfully saved %{name}
8
+ failure_saving: "Unable to save %{name}: %{errors}"
9
+ success_destroying: Successfully destroyed %{name}
10
+ failure_destroying: "Unable to destroy %{name}: %{errors}"
11
+ models:
12
+ token_auth/authentication_token:
13
+ one: Authentication token
14
+ other: Authentication tokens
15
+ token_auth/configuration_token:
16
+ one: Configuration token
17
+ other: Configuration tokens
18
+ token_auth:
19
+ tokens:
20
+ index:
21
+ are_you_sure: Are you sure?
22
+ case_insensitive: Case insensitive
23
+ create: Create
24
+ disable: Disable
25
+ disabled: Disabled
26
+ destroy: Destroy
27
+ enable: Enable
28
+ expired: Expired
29
+ expires_in: Expires in
30
+ manage_tokens_for: Manage tokens for
31
+ value: Value
@@ -0,0 +1,31 @@
1
+ es-PE:
2
+ activerecord:
3
+ attributes:
4
+ token_auth/authentication_token:
5
+ client_uuid: Identificador único universal (uuid) del cliente
6
+ cannot_find: No se pudo encontrar %{name}
7
+ success_saving: Guardado exitosamente %{name}
8
+ failure_saving: "No se pudo guardar %{name}: %{errors}"
9
+ success_destroying: Destruido exitosamente %{name}
10
+ failure_destroying: "No se pudo destruir %{name}: %{errors}"
11
+ models:
12
+ token_auth/authentication_token:
13
+ one: Autorizar identificador
14
+ other: Autorizar identificador
15
+ token_auth/configuration_token:
16
+ one: Identificador
17
+ other: Identificador
18
+ token_auth:
19
+ tokens:
20
+ index:
21
+ are_you_sure: ¿Estás seguro?
22
+ case_insensitive: No distingue mayúsculas y minúsculas
23
+ create: Crear identificador
24
+ disable: Desactivar
25
+ disabled: Inhabilitado
26
+ destroy: Destruir
27
+ enable: Activar
28
+ expired: Vencido
29
+ expires_in: Vence en
30
+ manage_tokens_for: Administrar identificador para
31
+ value: Valor
@@ -0,0 +1,31 @@
1
+ pt-BR:
2
+ activerecord:
3
+ attributes:
4
+ token_auth/authentication_token:
5
+ client_uuid: Identificador Único Universal (uuid) do cliente
6
+ cannot_find: Não foi possível encontrar %{name}
7
+ success_saving: Salvo com sucesso %{name}
8
+ failure_saving: "Não foi possível salvar %{name}: %{errors}"
9
+ success_destroying: Destruído com sucesso %{name}
10
+ failure_destroying: "Não foi possível destruir %{name}: %{errors}"
11
+ models:
12
+ token_auth/authentication_token:
13
+ one: Autorizar Token
14
+ other: Autorizar Token
15
+ token_auth/configuration_token:
16
+ one: Configurar token
17
+ other: Configurar token
18
+ token_auth:
19
+ tokens:
20
+ index:
21
+ are_you_sure: Está certo disto?
22
+ case_insensitive: Não distingue maiúsculas e minúsculas
23
+ create: Criar token
24
+ disable: Desativar
25
+ disabled: Desabilitado
26
+ destroy: Destruir
27
+ enable: Ativar
28
+ expired: Expirado
29
+ expires_in: Expira em
30
+ manage_tokens_for: Gerenciar Token para
31
+ value: Valor
data/config/routes.rb ADDED
@@ -0,0 +1,18 @@
1
+ TokenAuth::Engine.routes.draw do
2
+ scope '/entities/:entity_id' do
3
+ resource :authentication_token, only: [:update, :destroy]
4
+ resource :configuration_token, only: [:create, :destroy]
5
+ resources :tokens, only: :index
6
+ end
7
+
8
+ namespace :api do
9
+ match 'authentication_tokens',
10
+ to: 'authentication_tokens#options',
11
+ via: :options
12
+ resources :authentication_tokens, only: :create
13
+ match 'payloads',
14
+ to: 'payloads#options',
15
+ via: :options
16
+ resources :payloads, only: [:index, :create]
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ module CreateConfigurationTokensMigration
2
+ def change
3
+ create_table :token_auth_configuration_tokens do |t|
4
+ t.datetime :expires_at, null: false
5
+ t.string :value, null: false
6
+ t.integer :entity_id, null: false
7
+ end
8
+
9
+ add_index :token_auth_configuration_tokens, :entity_id, unique: true
10
+ end
11
+ end
12
+
13
+ if ActiveRecord::Migration.respond_to? :[]
14
+ class CreateConfigurationTokens < ActiveRecord::Migration[4.2]
15
+ include CreateConfigurationTokensMigration
16
+ end
17
+ else
18
+ class CreateConfigurationTokens < ActiveRecord::Migration
19
+ include CreateConfigurationTokensMigration
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ module CreateAuthenticationTokensMigration
2
+ def change
3
+ create_table :token_auth_authentication_tokens do |t|
4
+ t.integer :entity_id, null: false
5
+ t.string :value, null: false, limit: ::TokenAuth::AuthenticationToken::TOKEN_LENGTH
6
+ t.boolean :is_enabled, null: false, default: true
7
+ t.string :uuid, null: false, limit: ::TokenAuth::AuthenticationToken::UUID_LENGTH
8
+ t.string :client_uuid, null: false
9
+ end
10
+
11
+ add_index :token_auth_authentication_tokens, :entity_id, unique: true
12
+ add_index :token_auth_authentication_tokens, :value, unique: true
13
+ add_index :token_auth_authentication_tokens, :client_uuid, unique: true
14
+ end
15
+ end
16
+
17
+ if ActiveRecord::Migration.respond_to? :[]
18
+ class CreateAuthenticationTokens < ActiveRecord::Migration[4.2]
19
+ include CreateAuthenticationTokensMigration
20
+ end
21
+ else
22
+ class CreateAuthenticationTokens < ActiveRecord::Migration
23
+ include CreateAuthenticationTokensMigration
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module CreateTokenAuthSynchronizableResourcesMigration
2
+ def change
3
+ create_table :token_auth_synchronizable_resources do |t|
4
+ t.string :uuid, null: false
5
+ t.integer :entity_id, null: false
6
+ t.string :entity_id_attribute_name, null: false
7
+ t.string :name, null: false
8
+ t.string :class_name, null: false
9
+ t.boolean :is_pullable, default: false, null: false
10
+ t.boolean :is_pushable, default: false, null: false
11
+
12
+ t.timestamps null: false
13
+ end
14
+ end
15
+ end
16
+
17
+ if ActiveRecord::Migration.respond_to? :[]
18
+ class CreateTokenAuthSynchronizableResources < ActiveRecord::Migration[4.2]
19
+ include CreateTokenAuthSynchronizableResourcesMigration
20
+ end
21
+ else
22
+ class CreateTokenAuthSynchronizableResources < ActiveRecord::Migration
23
+ include CreateTokenAuthSynchronizableResourcesMigration
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ # desc "Explaining what the task does"
3
+ # task :token_auth do
4
+ # # Task goes here
5
+ # end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ module TokenAuth
3
+ # Engine application superclass.
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace TokenAuth
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ # Engine version.
3
+ module TokenAuth
4
+ VERSION = "0.3.0.beta1"
5
+ end
data/lib/token_auth.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ require "token_auth/engine"
3
+
4
+ require "active_model_serializers"
5
+ ActiveModel::Serializer.config.adapter = :json_api
6
+
7
+ # nodoc
8
+ module TokenAuth
9
+ end
metadata ADDED
@@ -0,0 +1,229 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: token_auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0.beta1
5
+ platform: ruby
6
+ authors:
7
+ - Eric Carty-Fickes
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: active_model_serializers
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.10.0.rc3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.10.0.rc3
27
+ - !ruby/object:Gem::Dependency
28
+ name: actionpack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 5.0.0.rc1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 5.0.0.rc1
41
+ - !ruby/object:Gem::Dependency
42
+ name: activerecord
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 5.0.0.rc1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 5.0.0.rc1
55
+ - !ruby/object:Gem::Dependency
56
+ name: railties
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 5.0.0.rc1
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 5.0.0.rc1
69
+ - !ruby/object:Gem::Dependency
70
+ name: pg
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 3.5.0.beta3
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 3.5.0.beta3
97
+ - !ruby/object:Gem::Dependency
98
+ name: capybara
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.5'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.5'
111
+ - !ruby/object:Gem::Dependency
112
+ name: brakeman
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: mdl
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: simplecov
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '0.11'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '0.11'
167
+ description: A Rails engine that allows for configuring secure communication between
168
+ client and server via a shared human-readable token.
169
+ email:
170
+ - ericcf@northwestern.edu
171
+ executables: []
172
+ extensions: []
173
+ extra_rdoc_files: []
174
+ files:
175
+ - MIT-LICENSE
176
+ - README.md
177
+ - Rakefile
178
+ - app/controllers/token_auth/api/authentication_tokens_controller.rb
179
+ - app/controllers/token_auth/api/base_controller.rb
180
+ - app/controllers/token_auth/api/payloads_controller.rb
181
+ - app/controllers/token_auth/authentication_tokens_controller.rb
182
+ - app/controllers/token_auth/base_controller.rb
183
+ - app/controllers/token_auth/concerns/api_resources.rb
184
+ - app/controllers/token_auth/concerns/cors_settings.rb
185
+ - app/controllers/token_auth/configuration_tokens_controller.rb
186
+ - app/controllers/token_auth/tokens_controller.rb
187
+ - app/models/token_auth/authentication_token.rb
188
+ - app/models/token_auth/configuration_token.rb
189
+ - app/models/token_auth/payload.rb
190
+ - app/models/token_auth/synchronizable_resource.rb
191
+ - app/models/token_auth/uuid_enabled.rb
192
+ - app/serializers/token_auth/synchronizable_resource_serializer.rb
193
+ - app/views/token_auth/tokens/index.html.erb
194
+ - config/locales/en.yml
195
+ - config/locales/es-PE.yml
196
+ - config/locales/pt-BR.yml
197
+ - config/routes.rb
198
+ - db/migrate/20150428210721_create_configuration_tokens.rb
199
+ - db/migrate/20150428211137_create_authentication_tokens.rb
200
+ - db/migrate/20151229184253_create_token_auth_synchronizable_resources.rb
201
+ - lib/tasks/token_auth_server_rails_tasks.rake
202
+ - lib/token_auth.rb
203
+ - lib/token_auth/engine.rb
204
+ - lib/token_auth/version.rb
205
+ homepage: https://github.com/NU-CBITS/token_auth
206
+ licenses:
207
+ - MIT
208
+ metadata: {}
209
+ post_install_message:
210
+ rdoc_options: []
211
+ require_paths:
212
+ - lib
213
+ required_ruby_version: !ruby/object:Gem::Requirement
214
+ requirements:
215
+ - - ">="
216
+ - !ruby/object:Gem::Version
217
+ version: '0'
218
+ required_rubygems_version: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">"
221
+ - !ruby/object:Gem::Version
222
+ version: 1.3.1
223
+ requirements: []
224
+ rubyforge_project:
225
+ rubygems_version: 2.6.4
226
+ signing_key:
227
+ specification_version: 4
228
+ summary: Rails engine for authenticating clients anonymously.
229
+ test_files: []