ak4r 0.2.2

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 30f9acbcce2837f1b5a58f93267a3a1fc36d329136b90cfeb30601f9e2603b3e
4
+ data.tar.gz: eb9615af834214a2c7b091b536ba5e1f334ed13cbe782448d2e1eb100054f336
5
+ SHA512:
6
+ metadata.gz: f9345b35fd16afefe7063d8d6d7387d66fe7de9f0c441d940c9d402b1f32d148224db01560155d9156de3b6bf2ddc1248ec134896377d7b21f757eca016a72b2
7
+ data.tar.gz: 3e13a6875030f334bf4210847fec3739d50d8d6f2a14065a407b50fb1c19891948d71ae32e006091ba66ba5e9dc171dda54b76d4300e7b1f9acf5f0884aaaf83
data/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # AK4R = API Keys for Rails
2
+
3
+ AK4R is a Rack middleware which adds to Ruby on Rails the ability to protect APi calls whit an API key passed in the request headers.
4
+
5
+ The implementation is very similar to the description here: https://www.freecodecamp.org/news/best-practices-for-building-api-keys-97c26eabfea9/ ,
6
+ using some pieces of the rack-api-key gem.
7
+
8
+ API keys are stored in an Active Record model and validated at every request.
9
+
10
+ API keys are scoped, so you have the ability to fine tune permissions.
11
+
12
+ API keys can optionally expire.
13
+
14
+ ## Installation
15
+
16
+ Add this line to your Rails application's Gemfile:
17
+
18
+ gem 'ak4r'
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Finally you should generate the db migration:
25
+
26
+ ```ruby
27
+ rails generate ak4r_migration
28
+ rake db:migrate
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ Gem auto loads into the Rails application. The default is to protect all urls starting with "/api".
34
+ Since initially no key is present every request throws an exception:
35
+
36
+ ```ruby
37
+ Ak4r::ApiException
38
+ ```
39
+ you should rescue this in your application, e.g. you can add this line to `application_controller.rb` :
40
+
41
+ ```ruby
42
+ rescue_from Ak4r::ApiException, with: :handle_api_authorization
43
+ ```
44
+ ## How to generate API keys
45
+
46
+ There is a rake task for this:
47
+
48
+ ```ruby
49
+ rake ak4r:create["name","scope1;scope2"]
50
+ ```
51
+ Scopes are defined as [HTTP_VERB]:path, e.g. `GET:/api/books.json` .
52
+
53
+ This task outputs the key to put in X-API-KEY header. Please note that the key itself is not stored so you must immediatelly copy it in a secure place.
54
+
55
+ ## Configuration
56
+
57
+ You can customize its behaviour in your `config/application.rb` :
58
+
59
+ ```ruby
60
+ config.ak4r.[option] = '...'
61
+ ```
62
+
63
+ If options depend on your environment, you can define it in the according file: `config/environments/<env>.rb`
64
+
65
+ ### :salt
66
+ The salt used to generate keys.
67
+
68
+ ### :header_key
69
+ It's important to note that internally Rack actually mutates any given headers
70
+ and prefixes them with HTTP and subsequently underscores them. For example if an
71
+ API client passed "X-API-KEY" in the header, Rack would interpret that header
72
+ as "HTTP_X_API_KEY". "HTTP_X_API_KEY" is the default header. If you want to use
73
+ a different header you can specify it with this option.
74
+
75
+ ### :url_restriction
76
+ This is an option that can restrict the middleware to specific URLs.
77
+ This works well when you have a mixture of API endpoints that require
78
+ authentication and some that might not. Or a combination of API endpoints and
79
+ publicly facing webpages. Perhaps you've scoped all of your API endpoints to
80
+ "/api", and the rest of the URL mappings or routes are supposed to be wide open.
81
+
82
+ ### :url_exclusion
83
+ This is an option to allow specific URLs to bypass middleware authentication.
84
+ This works well when you require a single or few endpoints to not require
85
+ authentication. Perhaps you've scoped all of your API endpoints to "/api" but wish
86
+ to leave "/api/status" publicly facing.
87
+
88
+
89
+
90
+
91
+
data/lib/ak4r.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'ak4r/configuration'
2
+ require 'ak4r/railtie'
3
+
4
+ module Ak4r
5
+ def self.configure
6
+ yield config
7
+ end
8
+
9
+ def self.config
10
+ @config ||= Configuration.new
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ class Ak4r::ApiException < Exception
2
+ attr_accessor :code
3
+ attr_accessor :response
4
+
5
+ def initialize(code, response)
6
+ @code = code
7
+ @response = response
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module Ak4r
2
+ class ApiKey < ActiveRecord::Base
3
+ self.table_name = "ak4r_api_keys"
4
+ end
5
+ end
6
+
@@ -0,0 +1,31 @@
1
+ module Ak4r
2
+ class Configuration
3
+ SETTINGS = [:salt, :header_key, :url_restriction, :url_exclusion]
4
+
5
+ SETTINGS.each do |setting|
6
+ attr_accessor setting
7
+
8
+ define_method "#{setting}?" do
9
+ ![nil, false, []].include? send(setting)
10
+ end
11
+ end
12
+
13
+ def initialize
14
+ @salt = "API_KEY_SALT"
15
+ @header_key = "HTTP_X_API_KEY"
16
+ @url_restriction = [/api/]
17
+ @url_exclusion = [/api\/status/]
18
+ end
19
+
20
+ def update(settings_hash)
21
+ settings_hash.each do |setting, value|
22
+ unless SETTINGS.include? setting.to_sym
23
+ raise ArgumentError, "invalid setting: #{setting}"
24
+ end
25
+
26
+ public_send "#{setting}=", value
27
+ end
28
+ end
29
+ end
30
+ end
31
+
@@ -0,0 +1,79 @@
1
+ require 'ak4r'
2
+
3
+ require 'ak4r/api_key'
4
+ require 'ak4r/token_generator'
5
+ require 'ak4r/api_exception'
6
+
7
+ module Ak4r
8
+ class Middleware
9
+ ##
10
+ # ==== Options
11
+ #
12
+ # * +:salt+ - Salt to generate API keys.
13
+ #
14
+ # * +:header_key+ - A way to override the header's name used to store the API key.
15
+ # The value given here should reflect how Rack interprets the
16
+ # header. For example if the client passes "X-API-KEY" Rack
17
+ # transforms interprets it as "HTTP_X_API_KEY". The default
18
+ # value is "HTTP_X_API_KEY".
19
+ #
20
+ # * +:url_restriction+ - A way to restrict specific URLs that should pass through
21
+ # the rack-api-key middleware. In order to use pass an Array of Regex patterns.
22
+ # If left unspecified all requests will pass through the rack-api-key
23
+ # middleware.
24
+ #
25
+ # * +:url_exclusion+ - A way to exclude specific URLs that should not pass through the
26
+ # the rack-api-middleware. In order to use, pass an Array of Regex patterns.
27
+ #
28
+ # ==== Example
29
+ # use Ak4r,
30
+ # :salt => "API_KEY_SALT"
31
+ # :header_key => "HTTP_X_API_KEY",
32
+ # :url_restriction => [/api/],
33
+ # :url_exclusion => [/api\/status/]
34
+ def initialize(app, config = {})
35
+ @app = app
36
+ Ak4r.config.update config
37
+ end
38
+
39
+ def call(env)
40
+ if constraint?(:url_exclusion) && url_matches(:url_exclusion, env)
41
+ @app.call(env)
42
+ elsif constraint?(:url_restriction)
43
+ url_matches(:url_restriction, env) ? process_request(env) : @app.call(env)
44
+ else
45
+ process_request(env)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def process_request(env)
52
+ api_key_string = env[Ak4r.config.header_key]
53
+ raise Ak4r::ApiException.new(403, "API Key required") if(api_key_string.nil?)
54
+
55
+ api_key_prefix, api_key_secret = api_key_string.split('.')
56
+ api_key = Ak4r::ApiKey.find_by(prefix: api_key_prefix)
57
+ raise Ak4r::ApiException.new(403, "API Key invalid") if(api_key.nil?)
58
+
59
+ raise Ak4r::ApiException.new(403, "API Key expired") if(api_key.valid_until && api_key.valid_until < Time.now)
60
+
61
+ api_key_hash = Ak4r::TokenGenerator.digest(api_key_secret)
62
+ raise Ak4r::ApiException.new(403, "API Key invalid") if(api_key_hash != api_key.hash)
63
+
64
+ request = Rack::Request.new(env)
65
+ scope = "#{request.request_method}:#{request.path}"
66
+ raise Ak4r::ApiException.new(403, "API Key not allowed for scope #{scope}") unless(api_key.scopes.include?(scope))
67
+ @app.call(env)
68
+ end
69
+
70
+ def constraint?(key)
71
+ !(Ak4r.config.public_send(key).nil? || Ak4r.config.public_send(key).empty?)
72
+ end
73
+
74
+ def url_matches(key, env)
75
+ path = Rack::Request.new(env).fullpath
76
+ Ak4r.config.public_send(key).select { |url_regex| path.match(url_regex) }.empty? ? false : true
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,15 @@
1
+ module Ak4r
2
+ class Railtie < Rails::Railtie
3
+ config.ak4r = ActiveSupport::OrderedOptions.new
4
+
5
+ initializer 'ak4r.initialize' do |app|
6
+ require 'ak4r/middleware'
7
+ app.middleware.use Ak4r::Middleware, config.ak4r
8
+ end
9
+
10
+ rake_tasks do
11
+ load File.expand_path('../../tasks/ak4r.rake', __FILE__)
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ require 'ak4r/api_key'
6
+
7
+ # Adapted from Devise::TokenGenerator
8
+ module Ak4r
9
+ class TokenGenerator
10
+ DIGEST = "SHA256"
11
+
12
+ def self.digest(value)
13
+ key = generate_key
14
+ value.present? && OpenSSL::HMAC.hexdigest(DIGEST, key, value.to_s)
15
+ end
16
+
17
+ def self.generate
18
+ key = generate_key
19
+ loop do
20
+ raw = self.friendly_token
21
+ enc = OpenSSL::HMAC.hexdigest(DIGEST, key, raw)
22
+ break [raw, enc] unless Ak4r::ApiKey.where(hash: enc).any?
23
+ end
24
+ end
25
+
26
+ def self.generate_key
27
+ return Rails.application.key_generator.generate_key(Ak4r.config.salt)
28
+ end
29
+
30
+ def self.friendly_token(length = 20)
31
+ # To calculate real characters, we must perform this operation.
32
+ # See SecureRandom.urlsafe_base64
33
+ rlength = (length * 3) / 4
34
+ SecureRandom.urlsafe_base64(rlength).tr('lIO0', 'sxyz')
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ class Ak4rMigrationGenerator < Rails::Generators::Base
5
+ include Rails::Generators::Migration
6
+
7
+ desc 'Creates a new migration for Ak4r API keys'
8
+
9
+ def self.source_root
10
+ File.expand_path('../templates', __FILE__)
11
+ end
12
+
13
+ def self.next_migration_number(dirname)
14
+ if ActiveRecord::Base.timestamped_migrations
15
+ migration_number = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
16
+ migration_number += 1
17
+ migration_number.to_s
18
+ else
19
+ "%.3d" % (current_migration_number(dirname) + 1)
20
+ end
21
+ end
22
+
23
+ def create_migration_file
24
+ migration_template 'migration.rb', 'db/migrate/create_ak4r_api_key.rb'
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ class CreateAk4rApiKey < ActiveRecord::Migration[4.2]
2
+ def self.up
3
+ create_table :ak4r_api_keys do |t|
4
+ t.string :name
5
+ t.string :prefix
6
+ t.string :hash
7
+ t.string :scopes, array: true
8
+ t.timestamp :valid_until
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :ak4r_api_keys, :prefix
13
+ add_index :ak4r_api_keys, :hash
14
+ end
15
+
16
+ def self.down
17
+ drop_table :ak4r_api_keys
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ require 'ak4r/api_key'
2
+ require 'ak4r/token_generator'
3
+
4
+ namespace :ak4r do
5
+ desc "List all API Key"
6
+ task :list => :environment do
7
+ Ak4r::ApiKey.all.each do |api_key|
8
+ puts "#{api_key.name}\t#{api_key.prefix}\t#{api_key.scopes.join(";")}"
9
+ end
10
+ end
11
+ desc "Create new API Key"
12
+ task :create, [:name, :scopes] => :environment do
13
+ secret, hash = Ak4r::TokenGenerator.generate
14
+ api_key = Ak4r::ApiKey.create(
15
+ name: args[:name],
16
+ hash: hash,
17
+ prefix: Ak4r::TokenGenerator.friendly_token(7),
18
+ scopes: args[:scopes].split(';')
19
+ )
20
+ puts "#{api_key.prefix}.#{secret}"
21
+ end
22
+ end
23
+
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ak4r
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Stefano Salvador
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-02-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.2'
27
+ description: Middleware for adding api keys validation to API
28
+ email: stefano.salvador@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - README.md
34
+ - lib/ak4r.rb
35
+ - lib/ak4r/api_exception.rb
36
+ - lib/ak4r/api_key.rb
37
+ - lib/ak4r/configuration.rb
38
+ - lib/ak4r/middleware.rb
39
+ - lib/ak4r/railtie.rb
40
+ - lib/ak4r/token_generator.rb
41
+ - lib/generators/ak4r_migration_generator.rb
42
+ - lib/generators/templates/migration.rb
43
+ - lib/tasks/ak4r.rake
44
+ homepage: https://github.com/stefanosalvador/ak4r
45
+ licenses:
46
+ - MIT
47
+ metadata: {}
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 2.0.0
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.1.2
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: API Keys for Ruby on Rails
67
+ test_files: []