gamora 0.1.0

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: 2cd6b80cc6b032dda4a916030c007de815ab7bbe86b9206797310fad36c7a70f
4
+ data.tar.gz: aad80d59d2dcf567445aade639916019cbc3e6570d3f9a73bde6bfd4647f26fe
5
+ SHA512:
6
+ metadata.gz: 98b0c4b20abaecc2ed9042e6d04d565325b3fbc71a37f152520aec9f9f56043429843d13dd879415f961593dca7ee6f405b92f08ef85a122fe51813448e203bf
7
+ data.tar.gz: 6518ea2fdb5ed87f69afc422bd38ca1a82a3aea6b313ea6abef5adb057366b5d0f6fc16d11b002ca6b8bede834c90cb126f38880b19dc53bcb8d0e67e27584ea
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Alejandro Gutiérrez
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # Gamora - OIDC Relying Party
2
+
3
+ Gamora aims to provide most of the functionality that is commonly
4
+ required in an OpenID Connect Relying Party. An OIDC Relying Party is
5
+ an OAuth 2.0 Client application that requires user authentication and
6
+ claims from an OpenID Connect Provider (IdP). More information about
7
+ [OpenID Connect](https://openid.net/connect/).
8
+
9
+ ## Installation
10
+
11
+ Add `Gamora` to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem "gamora"
15
+ ```
16
+
17
+ And then install gamora:
18
+
19
+ ```bash
20
+ rails g gamora:install
21
+ ```
22
+
23
+ ## Configuration
24
+
25
+ Provide required configuration in `config/initializers/gamora.rb`:
26
+
27
+ ```ruby
28
+ Gamora.setup do |config|
29
+ # ===> Required OAuth2 configuration options
30
+ config.client_id = "CLIENT_ID"
31
+ config.client_secret = "CLIENT_SECRET"
32
+ config.site = "IDENTITY_PROVIDER"
33
+
34
+ ...
35
+ end
36
+ ```
37
+
38
+ To see the full list of configuration options please check your gamora
39
+ initializer.
40
+
41
+ ## User authentication
42
+
43
+ ### Web-based applications
44
+
45
+ To authenticate the user against the Identity Provider before each request
46
+ using an access token stored in the session you should include
47
+ `Gamora::Authentication::Session` in your application controller and use the
48
+ before_action `authenticate_user!` in the actions you need to protect.
49
+ In case the access token has expired or is invalid. it will redirect the
50
+ user to the IdP to authenticate again.
51
+
52
+ ```ruby
53
+ class ApplicationController < ActionController::Base
54
+ include Gamora::Authentication::Session
55
+ ...
56
+
57
+ before_action :authenticate_user!
58
+ end
59
+ ```
60
+
61
+ ### JSON API applications
62
+
63
+ In the other hand, if your application is an JSON API you probably want
64
+ to authenticate the user using the access token in the request headers.
65
+ To do that, you should include `Gamora::Authentication::Headers` in your
66
+ application controller and use the before_action `authenticate_user!` in
67
+ the actions you need to protect. In case the access token has expired or
68
+ is invalid. it will return an error json with unauthorized status.
69
+
70
+ ```ruby
71
+ class ApplicationController < ActionController::Base
72
+ include Gamora::Authentication::Headers
73
+ ...
74
+
75
+ before_action :authenticate_user!
76
+ end
77
+ ```
78
+
79
+ Optionally, if you want to do something different when authentication
80
+ fails, you just need to override the `user_authentication_failed!`
81
+ method in you controller and customize it as you wish.
82
+
83
+ ## Contributing
84
+ Contribution directions go here.
85
+
86
+ ## License
87
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+
5
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
6
+ load "rails/tasks/engine.rake"
7
+
8
+ load "rails/tasks/statistics.rake"
9
+
10
+ require "bundler/gem_tasks"
11
+ require "rspec/core/rake_task"
12
+
13
+ RSpec::Core::RakeTask.new(:spec)
14
+
15
+ require "rubocop/rake_task"
16
+
17
+ RuboCop::RakeTask.new
18
+
19
+ task default: %i[spec rubocop]
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamora
4
+ class ApplicationController < ActionController::Base
5
+ end
6
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamora
4
+ class AuthenticationController < ApplicationController
5
+ def show
6
+ redirect_to authorization_url, allow_other_host: true
7
+ end
8
+
9
+ private
10
+
11
+ def authorization_url
12
+ Client.from_config.auth_code.authorize_url({
13
+ scope: Configuration.default_scope,
14
+ prompt: Configuration.default_prompt,
15
+ strategy: Configuration.default_strategy,
16
+ ui_locales: Configuration.ui_locales.call
17
+ }.merge(authorization_params).compact_blank)
18
+ end
19
+
20
+ def authorization_params
21
+ params.permit(
22
+ :scope,
23
+ :state,
24
+ :prompt,
25
+ :max_age,
26
+ :strategy,
27
+ :ui_locales
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamora
4
+ class CallbackController < ApplicationController
5
+ def show
6
+ access_token = access_token_from_auth_code
7
+ session[:access_token] = access_token.token
8
+ session[:refresh_token] = access_token.refresh_token
9
+ redirect_to session.delete("gamora.origin") || main_app.root_path
10
+ rescue OAuth2::Error
11
+ render plain: "Invalid authorization code"
12
+ end
13
+
14
+ private
15
+
16
+ def access_token_from_auth_code
17
+ Client.from_config.auth_code.get_token(params[:code])
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamora
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gamora::Engine.routes.draw do
4
+ get "amco", to: "authentication#show", as: :authentication
5
+ get "amco/callback", to: "callback#show", as: :callback
6
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamora
4
+ module Authentication
5
+ module Base
6
+ def authenticate_user!
7
+ claims = resource_owner_claims(access_token)
8
+ assign_current_user_from_claims(claims) if claims.present?
9
+ validate_authentication!
10
+ end
11
+
12
+ def current_user
13
+ @current_user
14
+ end
15
+
16
+ private
17
+
18
+ def validate_authentication!
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def access_token
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def user_authentication_failed!
27
+ raise NotImplementedError
28
+ end
29
+
30
+ def assign_current_user_from_claims(claims)
31
+ attrs = user_attributes_from_claims(claims)
32
+ @current_user = User.new(attrs)
33
+ end
34
+
35
+ def user_attributes_from_claims(claims)
36
+ claims.transform_keys do |key|
37
+ case key
38
+ when :sub then :id
39
+ when :email then :email
40
+ when :given_name then :first_name
41
+ when :family_name then :last_name
42
+ when :phone_number then :phone_number
43
+ else key
44
+ end
45
+ end
46
+ end
47
+
48
+ def resource_owner_claims(access_token)
49
+ return {} if access_token.blank?
50
+ oauth_client.userinfo(access_token)
51
+ end
52
+
53
+ def oauth_client
54
+ Client.from_config
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamora
4
+ module Authentication
5
+ module Headers
6
+ include Base
7
+
8
+ private
9
+
10
+ def validate_authentication!
11
+ return if current_user.present?
12
+ user_authentication_failed!
13
+ end
14
+
15
+ def access_token
16
+ pattern = /^Bearer /
17
+ header = request.headers["Authorization"]
18
+ return unless header&.match(pattern)
19
+ header.gsub(pattern, "")
20
+ end
21
+
22
+ def user_authentication_failed!
23
+ render json: { error: "Access token invalid" }, status: :unauthorized
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamora
4
+ module Authentication
5
+ module Session
6
+ include Base
7
+
8
+ def self.included(base)
9
+ base.helper_method :current_user
10
+ end
11
+
12
+ private
13
+
14
+ def validate_authentication!
15
+ return if current_user.present?
16
+ session["gamora.origin"] = request.original_url
17
+ user_authentication_failed!
18
+ end
19
+
20
+ def access_token
21
+ session[:access_token]
22
+ end
23
+
24
+ def user_authentication_failed!
25
+ redirect_to gamora.authentication_path
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamora
4
+ class Client < OAuth2::Client
5
+ def self.from_config
6
+ new(
7
+ Configuration.client_id,
8
+ Configuration.client_secret,
9
+ {
10
+ site: Configuration.site,
11
+ token_url: Configuration.token_url,
12
+ token_method: Configuration.token_method,
13
+ redirect_uri: Configuration.redirect_uri,
14
+ userinfo_url: Configuration.userinfo_url,
15
+ authorize_url: Configuration.authorize_url
16
+ }
17
+ )
18
+ end
19
+
20
+ def userinfo(access_token)
21
+ response = userinfo_request(access_token)
22
+ JSON.parse(response.body).symbolize_keys
23
+ rescue OAuth2::Error
24
+ {}
25
+ end
26
+
27
+ private
28
+
29
+ def userinfo_request(access_token)
30
+ opts = userinfo_request_options(access_token)
31
+ request(:post, options[:userinfo_url], opts)
32
+ end
33
+
34
+ def userinfo_request_options(access_token)
35
+ {
36
+ body: { access_token: access_token }.to_json,
37
+ headers: { "Content-Type": "application/json" }
38
+ }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamora
4
+ module Configuration
5
+ mattr_accessor :client_id, default: nil
6
+ mattr_accessor :client_secret, default: nil
7
+ mattr_accessor :site, default: nil
8
+ mattr_accessor :token_url, default: "/oauth2/token"
9
+ mattr_accessor :authorize_url, default: "/oauth2/authorize"
10
+ mattr_accessor :userinfo_url, default: "/oauth2/userinfo"
11
+ mattr_accessor :token_method, default: :post
12
+ mattr_accessor :redirect_uri, default: nil
13
+ mattr_accessor :default_scope, default: "openid profile email"
14
+ mattr_accessor :default_prompt, default: nil
15
+ mattr_accessor :default_strategy, default: "default"
16
+ mattr_accessor :ui_locales, default: -> { I18n.locale }
17
+
18
+ def setup
19
+ yield(self) if block_given?
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamora
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Gamora
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamora
4
+ class User
5
+ include ActiveModel::Model
6
+
7
+ attr_accessor :id,
8
+ :email,
9
+ :last_name,
10
+ :first_name,
11
+ :phone_number
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamora
4
+ VERSION = "0.1.0"
5
+ end
data/lib/gamora.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "oauth2"
4
+ require "gamora/version"
5
+ require "gamora/engine"
6
+ require "gamora/configuration"
7
+
8
+ module Gamora
9
+ extend Configuration
10
+
11
+ autoload :User, "gamora/user"
12
+ autoload :Client, "gamora/client"
13
+
14
+ module Authentication
15
+ autoload :Base, "gamora/authentication/base"
16
+ autoload :Headers, "gamora/authentication/headers"
17
+ autoload :Session, "gamora/authentication/session"
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamora
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+ desc "Creates gamora initializer."
8
+
9
+ def create_initializer
10
+ template "gamora.rb", "config/initializers/gamora.rb"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gamora.setup do |config|
4
+ # ===> Required OAuth2 configuration options
5
+ config.client_id = "CLIENT_ID"
6
+ config.client_secret = "CLIENT_SECRET"
7
+ config.site = "IDENTITY_PROVIDER_URL"
8
+
9
+ # ===> Optional OAuth2 configuration options and its defaults.
10
+ # config.token_url = "/oauth2/token"
11
+ # config.authorize_url = "/oauth2/authorize"
12
+ # config.userinfo_url = "/oauth2/userinfo"
13
+ # config.token_method = :post
14
+ # config.redirect_uri = nil
15
+ # config.default_scope = "openid profile email"
16
+ # config.default_prompt = nil
17
+ # config.default_strategy = "default"
18
+ # config.ui_locales = -> { I18n.locale }
19
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # desc "Explaining what the task does"
4
+ # task :gamora do
5
+ # # Task goes here
6
+ # end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gamora
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alejandro Gutiérrez
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-05-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: oauth2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '6.0'
41
+ description: Gamora aims to provide most of the functionality that is commonly required
42
+ in an OIDC Client.
43
+ email:
44
+ - alejandrodevs@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - MIT-LICENSE
50
+ - README.md
51
+ - Rakefile
52
+ - app/controllers/gamora/application_controller.rb
53
+ - app/controllers/gamora/authentication_controller.rb
54
+ - app/controllers/gamora/callback_controller.rb
55
+ - app/models/gamora/application_record.rb
56
+ - config/routes.rb
57
+ - lib/gamora.rb
58
+ - lib/gamora/authentication/base.rb
59
+ - lib/gamora/authentication/headers.rb
60
+ - lib/gamora/authentication/session.rb
61
+ - lib/gamora/client.rb
62
+ - lib/gamora/configuration.rb
63
+ - lib/gamora/engine.rb
64
+ - lib/gamora/user.rb
65
+ - lib/gamora/version.rb
66
+ - lib/generators/gamora/install_generator.rb
67
+ - lib/generators/gamora/templates/gamora.rb
68
+ - lib/tasks/gamora_tasks.rake
69
+ homepage: https://github.com/amco/gamora_rb
70
+ licenses:
71
+ - MIT
72
+ metadata:
73
+ homepage_uri: https://github.com/amco/gamora_rb
74
+ source_code_uri: https://github.com/amco/gamora_rb
75
+ changelog_uri: https://github.com/amco/gamora_rb/blob/master/CHANGELOG.md
76
+ rubygems_mfa_required: 'true'
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 2.7.0
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.3.7
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: OpenID Connect Relying Party for rails apps.
96
+ test_files: []