ak4r 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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: []