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