henshin-belt 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-09-02
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 kotarominami
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # Hensin Belt on Grape API
2
+
3
+ Hensin Belt is a Grape middleware to connect your API resources with your API authenticator.
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'henshin-belt'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install henshin-belt
21
+
22
+ ## Usage
23
+
24
+ ### Install generator
25
+
26
+ On your first install, run this generator :
27
+
28
+ ```ruby
29
+ rails g henshin_belt:install
30
+ ```
31
+
32
+ ### Usage with Grape
33
+
34
+ You will need to use the middleware in your main API :
35
+
36
+ ```ruby
37
+ # use middleware
38
+ use ::HensinBelt::Oauth2
39
+ ```
40
+
41
+ You could also use the helpers :
42
+
43
+ ```ruby
44
+ # use helpers
45
+ helpers ::HensinBelt::Helpers
46
+ ```
47
+
48
+ ### Protecting your endpoint
49
+
50
+ In your endpoint you need to define which protected endpoint by adding this DSL :
51
+
52
+ 1. `oauth2`
53
+ 2. `oauth2(:email)`
54
+
55
+ Example :
56
+
57
+ ```ruby
58
+ desc "Your protected endpoint"
59
+ oauth2
60
+ get :protected do
61
+ # your code goes here
62
+ end
63
+ ```
64
+
65
+ ```ruby
66
+ desc "Your protected endpoint with defined scope"
67
+ oauth2(:email)
68
+ get :protected do
69
+ # your code goes here
70
+ end
71
+ ```
72
+
73
+ ## Nice feature
74
+
75
+ From your protected endpoint you could get :
76
+
77
+ 1. `resource_token` => Your access token
78
+ 2. `resource_credential` => Full credentials
79
+ 3. `resource_owner` => Current Object
80
+ 4. `me` => Current Object
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HenshinBelt
4
+ class InstallGenerator < Rails::Generators::Base
5
+ source_root File.expand_path('../../templates', __FILE__)
6
+
7
+ def copy_initializer
8
+ template 'initializer.rb', 'config/initializers/henshin_belt.rb'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ HenshinBelt.setup do |config|
2
+ # your authentication server
3
+ config.is_custom_scopes = false
4
+ config.resources = "Models::Auth"
5
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grape'
4
+ require_relative 'henshin_belt/version'
5
+
6
+ require 'henshin_belt/configuration'
7
+
8
+ require 'henshin_belt/oauth2'
9
+ require 'henshin_belt/extension'
10
+ require 'henshin_belt/helpers'
11
+
12
+ require 'henshin_belt/base_strategy'
13
+ require 'henshin_belt/auth_strategies/hub'
14
+ require 'henshin_belt/auth_methods'
15
+
16
+ require 'henshin_belt/errors/invalid_token'
17
+ require 'henshin_belt/errors/invalid_scope'
18
+ require 'henshin_belt/errors/expired_token'
19
+
20
+ module HenshinBelt
21
+ extend HenshinBelt::Configuration
22
+ define_setting :auth_strategy, 'hub'
23
+ define_setting :resources, 'Models::Auth'
24
+ define_setting :is_custom_scopes, false
25
+
26
+ def self.config_resources
27
+ resources
28
+ end
29
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HenshinBelt
4
+ module AuthMethods
5
+ attr_accessor :me, :resource_token, :resource_owner, :resource_credentials
6
+
7
+ # rubocop:disable Lint/DuplicateMethods
8
+ def protected_endpoint=(protected)
9
+ @protected_endpoint = protected
10
+ end
11
+
12
+ def protected_endpoint?
13
+ @protected_endpoint || false
14
+ end
15
+
16
+ def resource_token
17
+ @_resource_token
18
+ end
19
+
20
+ def resource_token=(token)
21
+ @_resource_token = token
22
+ end
23
+
24
+ def me=(resource)
25
+ @_me = resource
26
+ end
27
+
28
+ def me
29
+ @_me
30
+ end
31
+
32
+ def resource_owner=(resource)
33
+ @_resource_owner = resource
34
+ end
35
+
36
+ def resource_owner
37
+ @_resource_owner
38
+ end
39
+
40
+ def resource_credentials=(credentials)
41
+ @_resource_credentials = credentials
42
+ end
43
+
44
+ def resource_credentials
45
+ @_resource_credentials
46
+ end
47
+
48
+ # rubocop:enable Lint/DuplicateMethods
49
+ end
50
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HenshinBelt
4
+ module AuthStrategies
5
+ class Hub < HenshinBelt::BaseStrategy
6
+ def endpoint_protected?
7
+ !!endpoint_authorizations
8
+ end
9
+
10
+ def has_auth_scopes?
11
+ !!endpoint_authorizations &&
12
+ endpoint_authorizations.key?(:scopes) &&
13
+ !endpoint_authorizations[:scopes].empty?
14
+ end
15
+
16
+ def auth_scopes
17
+ endpoint_authorizations[:scopes].map { |s| s.is_a?(String) || s.is_a?(Symbol) ? s.to_sym : s }
18
+ end
19
+
20
+ private
21
+
22
+ def endpoint_authorizations
23
+ api_context.options[:route_options][:auth]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HenshinBelt
4
+ class BaseStrategy
5
+ attr_accessor :api_context
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HenshinBelt
4
+ module Configuration
5
+ def setup
6
+ yield self
7
+ end
8
+
9
+ def define_setting(name, default = nil)
10
+ class_variable_set("@@#{name}", default)
11
+
12
+ define_class_method "#{name}=" do |value|
13
+ class_variable_set("@@#{name}", value)
14
+ end
15
+
16
+ define_class_method name do
17
+ class_variable_get("@@#{name}")
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def define_class_method(name, &block)
24
+ (class << self; self; end).instance_eval do
25
+ define_method name, &block
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HenshinBelt
4
+ module Errors
5
+ class ExpiredToken < StandardError
6
+ def initialize(msg = 'Expired token')
7
+ @code = 401
8
+ super
9
+ end
10
+ attr_reader :code
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HenshinBelt
4
+ module Errors
5
+ class InvalidScope < StandardError
6
+ def initialize(msg = 'Invalid scope')
7
+ @code = 401
8
+ super
9
+ end
10
+ attr_reader :code
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HenshinBelt
4
+ module Errors
5
+ class InvalidToken < StandardError
6
+ def initialize(msg = 'Invalid token')
7
+ @code = 401
8
+ super
9
+ end
10
+ attr_reader :code
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HenshinBelt
4
+ module Extension
5
+ def oauth2(*scopes)
6
+ scopes = Doorkeeper.configuration.default_scopes.all if scopes.all? { |x| x.nil? }
7
+ if respond_to?(:route_setting) # >= grape-0.10.0
8
+ description = route_setting(:description) || route_setting(:description, {})
9
+ else
10
+ description = @last_description ||= {}
11
+ end
12
+ # case WineBouncer.configuration.auth_strategy
13
+ # when :default
14
+ description[:auth] = { scopes: scopes }
15
+ # when :swagger
16
+ description[:authorizations] = { oauth2: scopes.map { |x| { scope: x } } }
17
+ # end
18
+ end
19
+
20
+ # Grape::API::Instance is defined in grape 1.2.0 or above
21
+ grape_api = defined?(Grape::API::Instance) ? Grape::API::Instance : Grape::API
22
+ grape_api.extend self
23
+ end
24
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HenshinBelt
4
+ module Helpers
5
+ end
6
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/auth/abstract/request'
4
+
5
+ module HenshinBelt
6
+ class Oauth2 < Grape::Middleware::Base
7
+ attr_reader :auth_strategy
8
+
9
+ def context
10
+ env['api.endpoint']
11
+ end
12
+
13
+ def the_request=(env)
14
+ @_the_request = ActionDispatch::Request.new(env)
15
+ end
16
+
17
+ def request
18
+ @_the_request
19
+ end
20
+
21
+ def token
22
+ if request.headers['Authorization'].present?
23
+ if request.headers['Authorization'].include?('bearer')
24
+ token = request.headers['Authorization'].try('split', 'bearer').try(:last).try(:strip)
25
+ elsif request.headers['Authorization'].include?('Bearer')
26
+ token = request.headers['Authorization'].try('split', 'Bearer').try(:last).try(:strip)
27
+ else
28
+ token = request.headers['Authorization']
29
+ end
30
+ else
31
+ token = request.parameters['access_token']
32
+ end
33
+ token
34
+ end
35
+
36
+ ############
37
+ # Authorization control.
38
+ ############
39
+ def endpoint_protected?
40
+ auth_strategy.endpoint_protected?
41
+ end
42
+
43
+ def args
44
+ results = {}
45
+ auth_strategy.auth_scopes.map { |s| (results = results.merge(s)) if s.is_a?(Hash) }
46
+ results
47
+ end
48
+
49
+ def sync_scopes_from(resource, to:)
50
+ to.update(scopes: resource.scopes.join(',')) rescue nil
51
+ end
52
+
53
+ def scopes
54
+ results = []
55
+ auth_strategy.auth_scopes.map { |s| (results << s) unless s.is_a?(Hash) }
56
+ results.map!(&:to_sym)
57
+ end
58
+
59
+ def access_scopes(access)
60
+ if HenshinBelt.is_custom_scopes
61
+ access.scopes.map!(&:to_sym) rescue []
62
+ else
63
+ access.scopes.all[0].split(',').map!(&:to_sym) rescue []
64
+ end
65
+ end
66
+
67
+ def is_args_include_validate?
68
+ if args.key?(:validate) && ![true, false].include?(args[:validate])
69
+ raise HenshinBelt::Errors::InvalidScope.new("Not valid scope '#{args[:validate]}' in `oauth2 scope`")
70
+ end
71
+ args.key?(:validate)
72
+ end
73
+
74
+ def scope_authorize!(access)
75
+ if scopes.present? && access
76
+ unless (scopes & (access_scopes access)).present?
77
+ raise HenshinBelt::Errors::InvalidScope.new('OAuth Scope is disallowed')
78
+ end
79
+ end
80
+ end
81
+
82
+ def token_optional?
83
+ is_args_include_validate? && [true, false].include?(args[:validate]) && args[:validate].eql?(false)
84
+ end
85
+
86
+ def token_required?
87
+ is_args_include_validate? && [true, false].include?(args[:validate]) && args[:validate].eql?(true) || is_args_include_validate?.blank?
88
+ end
89
+
90
+ def authorize!
91
+ access = Doorkeeper::AccessToken.find_by(token: token)
92
+ if access.present?
93
+ if access.expired?
94
+ raise HenshinBelt::Errors::ExpiredToken
95
+ end
96
+ if access.revoked?
97
+ raise HenshinBelt::Errors::InvalidToken
98
+ end
99
+ else
100
+ raise HenshinBelt::Errors::InvalidToken
101
+ end
102
+ # rubocop:disable Security/Eval
103
+ resource = eval(HenshinBelt.resources).where(id: access.resource_owner_id).last rescue nil
104
+ # rubocop:enable Security/Eval
105
+
106
+ sync_scopes_from(resource, to: access)
107
+ if HenshinBelt.is_custom_scopes
108
+ scope_authorize! resource
109
+ else
110
+ scope_authorize! access
111
+ end
112
+ {
113
+ token: access.token,
114
+ resource_owner: resource,
115
+ resource_credential: {
116
+ access_token: access.token,
117
+ scopes: access_scopes(access),
118
+ token_type: 'bearer',
119
+ expires_in: access.expires_in,
120
+ refresh_token: access.refresh_token,
121
+ created_at: access.created_at.to_i
122
+ }
123
+ }
124
+ end
125
+
126
+ ############
127
+ # Grape middleware methods
128
+ ############
129
+
130
+ def before
131
+ set_auth_strategy(HenshinBelt.auth_strategy)
132
+ auth_strategy.api_context = context
133
+ context.extend(HenshinBelt::AuthMethods)
134
+ context.protected_endpoint = endpoint_protected?
135
+
136
+ return unless context.protected_endpoint?
137
+
138
+ self.the_request = env
139
+ if token_optional? && context.protected_endpoint?
140
+ context.resource_token = nil
141
+ context.resource_owner = nil
142
+ context.resource_credentials = nil
143
+ response = authorize! rescue nil
144
+ if response.present?
145
+ context.resource_owner = response[:resource_owner] rescue nil
146
+ context.resource_credentials = nil
147
+ end
148
+ elsif token.present? && token_required? && context.protected_endpoint?
149
+ response = authorize!
150
+ context.resource_token = response[:token]
151
+ context.resource_owner = response[:resource_owner] rescue nil
152
+ context.me = response[:resource_owner] rescue nil
153
+ context.resource_credentials = response[:resource_credential] rescue nil
154
+ elsif context.resource_owner.nil? && context.protected_endpoint?
155
+ raise HenshinBelt::Errors::InvalidToken
156
+ else
157
+ raise HenshinBelt::Errors::InvalidToken
158
+ end
159
+ end
160
+
161
+ private
162
+
163
+ def set_auth_strategy(strategy)
164
+ @auth_strategy = HenshinBelt::AuthStrategies.const_get(strategy.to_s.capitalize.to_s).new
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HenshinBelt
4
+ VERSION = '0.0.1'
5
+ public_constant :VERSION
6
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: henshin-belt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - kotarominami
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-09-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.5.18
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.5.18
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 13.2.1
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 13.2.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 3.13.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 3.13.0
55
+ description: Hensin Belt is a Grape middleware to connect your API resources with
56
+ your API authenticator.
57
+ email:
58
+ - kotaroisme@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".rspec"
64
+ - ".rubocop.yml"
65
+ - CHANGELOG.md
66
+ - LICENSE.txt
67
+ - README.md
68
+ - Rakefile
69
+ - lib/generators/henshin_belt/install_generator.rb
70
+ - lib/generators/templates/initializer.rb
71
+ - lib/henshin-belt.rb
72
+ - lib/henshin_belt/auth_methods.rb
73
+ - lib/henshin_belt/auth_strategies/hub.rb
74
+ - lib/henshin_belt/base_strategy.rb
75
+ - lib/henshin_belt/configuration.rb
76
+ - lib/henshin_belt/errors/expired_token.rb
77
+ - lib/henshin_belt/errors/invalid_scope.rb
78
+ - lib/henshin_belt/errors/invalid_token.rb
79
+ - lib/henshin_belt/extension.rb
80
+ - lib/henshin_belt/helpers.rb
81
+ - lib/henshin_belt/oauth2.rb
82
+ - lib/henshin_belt/version.rb
83
+ homepage: https://github.com/kotaroisme/henshin-belt
84
+ licenses:
85
+ - MIT
86
+ metadata:
87
+ source_code_uri: https://github.com/kotaroisme/henshin-belt
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 3.0.0
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubygems_version: 3.5.18
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: Hensin Belt is a Grape middleware to connect your API.
107
+ test_files: []