henshin-belt 0.0.1

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.
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: []