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 +7 -0
- data/README.md +91 -0
- data/lib/ak4r.rb +12 -0
- data/lib/ak4r/api_exception.rb +9 -0
- data/lib/ak4r/api_key.rb +6 -0
- data/lib/ak4r/configuration.rb +31 -0
- data/lib/ak4r/middleware.rb +79 -0
- data/lib/ak4r/railtie.rb +15 -0
- data/lib/ak4r/token_generator.rb +37 -0
- data/lib/generators/ak4r_migration_generator.rb +26 -0
- data/lib/generators/templates/migration.rb +19 -0
- data/lib/tasks/ak4r.rake +23 -0
- metadata +67 -0
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
data/lib/ak4r/api_key.rb
ADDED
@@ -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
|
data/lib/ak4r/railtie.rb
ADDED
@@ -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
|
data/lib/tasks/ak4r.rake
ADDED
@@ -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: []
|