keycloak_rails 1.0.0.pre.beta

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: f02d892062dce2d81c45f4de13d30c78201eb376675a47bb91b1292ac8963487
4
+ data.tar.gz: a8272329fa865c85099399b1f819c525aa65483c0cd13cf6e711a18525d9f866
5
+ SHA512:
6
+ metadata.gz: df5bf2f2e77dacbf7c6211ccdf70adb1046398a46d94f31777af3c2987085b1db30fc95daead3dcde8b838105cd69f1ee42d1f407ea3c7f35eb869af6fd58e42
7
+ data.tar.gz: 4d43eaffc5aafd0822c399a6ee45e31e135e01abd72b4fdd79c1beef773c14672d814eda9edf9336b61c97a05ec1fba1cb13431d91c2966d1a9e2cceff55dd8b
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Nucleus Healthcare LLC
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,184 @@
1
+ # KeycloakRails
2
+ Keycloak_rails is an api wrapper for open source project [Keycloak](https://www.keycloak.org/)
3
+
4
+ * the gem assumes that you have a configured and ready to use keycloak server
5
+ * the gem is still in beta and the docs does not reflect the latest updates, multiple bugs might occur
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "keycloak_rails"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install keycloak_rails
22
+ ```
23
+
24
+ ## Getting started
25
+ to generate keycloack_rails initializer execute:
26
+ ```bash
27
+ $ bundle exec rails g keycloak_rails:config
28
+ ```
29
+
30
+ go to `config/initializers/keycloack_rails.rb`
31
+
32
+ where you will find
33
+ ```ruby
34
+ # frozen_string_literal: true
35
+
36
+ # Keycloak Rails initializer
37
+
38
+ KeycloakRails.configure do |config|
39
+ ####################################################
40
+ # Rails app controllers to manage auth
41
+ # config.sessions_controller = 'sessions'
42
+ # config.registrations_controller = 'registrations'
43
+ # config.unlocks_controller = 'unlocks'
44
+ # config.passwords_controller = 'passwords'
45
+ # config.omniauth_controller = 'omniauth'
46
+ ####################################################
47
+ # keyclaok rails need your user model name
48
+ # config.user_model = 'user'
49
+ ####################################################
50
+ # Auth server info
51
+ # config.auth_server_url = ''
52
+ # config.realm = 'realm'
53
+ # config.public_key = "public_key"
54
+ # config.secret = ''
55
+ # config.client_id = 'client_id'
56
+ ####################################################
57
+ end
58
+ ```
59
+ uncomment config options and enter your apps info
60
+
61
+ **Note** do not uncomment controller config if you just want to use keycloak_rails user/client helpers
62
+
63
+ ## use
64
+ ### with controller helpers
65
+ if you decided to use all of keycloak rails functionallity (pass controller options) keycloack rails will automatically hook up to named controllers and extend the base classes with our controller concerns which will provide the following methods
66
+ #### KeycloakRails::Controller::Helpers
67
+ ***
68
+ This concern will be inherited by all controllers as it extends application controller
69
+
70
+ the following helpers will be added to your app
71
+ ```ruby
72
+ ensure_active_session # redirects to root if user not logged in
73
+ ensure_no_active_session # redirects to root if user is logged in
74
+ current_user # returns current user by session cookie
75
+ user_has_active_sso_session? # returns true if current user has an active session in auth server
76
+ ```
77
+
78
+ #### KeycloakRails::Controller::Sessions
79
+ ***
80
+ extends the controller passed to `KeycloakRails.config.sessions_controller`
81
+
82
+ In your app
83
+
84
+
85
+ `keycloak_rails.rb`
86
+ ```ruby
87
+ KeycloakRails.configure do |config|
88
+ config.sessions_controller = 'sessions'
89
+ end
90
+ ```
91
+
92
+ `app/controllers/sessions_controller.rb`
93
+ ```ruby
94
+ class SessionsController < ApplicationController
95
+ skip_before_action :ensure_active_session, only: %i[new log_in]
96
+ before_action :ensure_no_active_session, only: %i[new log_in]
97
+
98
+ def new; end
99
+
100
+ def log_in
101
+ start_sso_session(params[:email], params[:password])
102
+ # keycloak_rails will take care of setting the session cookie & current_user for you
103
+ end
104
+
105
+ def log_out
106
+ end_sso_session
107
+ end
108
+ end
109
+ ```
110
+
111
+
112
+ #### KeycloakRails::Controller::Registrations
113
+ ***
114
+ The main idea behind keycloack_rails is to make adding sso easy to an existing rails app thats already in prod, and the registrations module is the backbone to achive that.
115
+
116
+ In your app
117
+
118
+
119
+ `keycloak_rails.rb`
120
+ ```ruby
121
+ KeycloakRails.configure do |config|
122
+ config.registrations_controller = 'registrations'
123
+ end
124
+ ```
125
+
126
+ `app/controllers/registrations_controller.rb`
127
+ ```ruby
128
+ class RegistrationsController < ApplicationController
129
+ skip_before_action :ensure_active_session, only: %i[new create_user]
130
+ before_action :ensure_no_active_session, only: %i[new create_user]
131
+
132
+ def new; end
133
+
134
+ def sign_up
135
+ sso_user = create_sso_user(email: params[:email], password: params[:password],
136
+ first_name: params[:first_name], last_name: params[:last_name])
137
+ user = User.create!(sso_user)
138
+ # sso_user = { sso_sub: user_keycloak_sub,
139
+ # email: params[:email],
140
+ # first_name: params[:first_name],
141
+ # last_name: params[:last_name] }
142
+ # as shown above the sso_sub returned from will need to be added to the DB user record
143
+ # the sso sub is a uniqe identifier generated by keycloak auth server
144
+ # it can be used to link multiple apps together
145
+ if user
146
+ render json: user
147
+ else
148
+ render json: user.errors
149
+ end
150
+ end
151
+
152
+
153
+ end
154
+ ```
155
+
156
+ #### KeycloakRails::Controller::Passwords
157
+ ***
158
+ #### KeycloakRails::Controller::Unlocks
159
+ ***
160
+ #### KeycloakRails::Controller::Omniauth
161
+ ***
162
+
163
+ ### without controller helpers
164
+ #### KeycloakRails::User
165
+
166
+ #### KeycloakRails::Client
167
+
168
+
169
+ ## Architecte plan
170
+
171
+ ### Engine Strecture
172
+ <img width="573" alt="Screen Shot 2022-11-20 at 1 11 50 AM" src="https://user-images.githubusercontent.com/84993125/202890379-b7f8abe9-105c-4d7d-bdf8-c5768f4111af.png">
173
+
174
+ ### Some use cases
175
+ |auth request|redirect|protected route request
176
+ |:-:|:-:|:-:|
177
+ |![auth request](https://user-images.githubusercontent.com/84993125/202890457-7d58c789-368a-4423-9064-4c50a8ffa296.png)|![redirect](https://user-images.githubusercontent.com/84993125/202890476-2420025e-0f23-4102-8a63-e961411eff16.png)|![protected route request](https://user-images.githubusercontent.com/84993125/202890490-854bda28-2dd3-41b8-8f06-ce80de4825e9.png)
178
+
179
+ ## Contributing
180
+ Contribution directions go here.
181
+
182
+ ## License
183
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
184
+
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,13 @@
1
+ module KeycloakRails
2
+ module SsoRecipient
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_one :keycloak_rails_sso, as: :recipient, class_name: "::KeycloakRails::Sso"
7
+
8
+ def sub
9
+ keycloak_rails_sso.sub
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module KeycloakRails
2
+ class Sso < ActiveRecord::Base
3
+ belongs_to :recipient, polymorphic: true
4
+ end
5
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeycloakRails
4
+ module Generators
5
+ class ConfigGenerator < Rails::Generators::Base
6
+ source_root(File.expand_path(File.dirname(__FILE__)))
7
+ def copy_initializer
8
+ copy_file '../keycloak_rails.rb', 'config/initializers/keycloak_rails.rb'
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeycloakRails
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root(File.expand_path(File.dirname(__FILE__)))
7
+
8
+ TABLE_NAME = 'keycloak_rails_sso'.freeze
9
+
10
+ desc "Generates a name space SSO model to store user subs."
11
+
12
+ def generate_keycloak_rails_model
13
+ generate :migration, "create_#{TABLE_NAME}", "recipient:references{polymorphic}", "sub:string:index"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Keycloak Rails initializer
4
+
5
+ KeycloakRails.configure do |config|
6
+ ####################################################
7
+ # config options
8
+ # decode token strategy can be :local or :cloud
9
+ # config.decode_token_strategy = :local
10
+ # config.signature_algo = 'RS256'
11
+ # config.allow_magic_links = true
12
+ ####################################################
13
+ # Rails app controllers to manage auth
14
+ # config.sessions_controller = 'sessions'
15
+ # config.registrations_controller = 'registrations'
16
+ # config.unlocks_controller = 'unlocks'
17
+ # config.passwords_controller = 'passwords'
18
+ # config.omniauth_controller = 'omniauth'
19
+ ####################################################
20
+ # Rails app models
21
+ # config.user_model = 'user'
22
+ ####################################################
23
+ # Auth server info
24
+ # config.auth_server_url = ''
25
+ # config.realm = 'realm'
26
+ # only needed if decode_token_strategy = :local
27
+ # config.public_key = "public_key"
28
+ # config.secret = ''
29
+ # config.client_id = 'client_id'
30
+ ####################################################
31
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeycloakRails
4
+ # client lvl access to sso server established with client id and secret
5
+ # can use basic auth or bearer client_token
6
+ # perms for this lvl of access are controllered by sso server client roles
7
+ class Client
8
+ def initialize
9
+ @curl = KeycloakRails::Curl.new
10
+ end
11
+
12
+ def create_user(email:, password:, first_name:, last_name:)
13
+ request = @curl.post(path: "admin/realms/#{KeycloakRails.realm}/users",
14
+ headers: { 'Authorization': client_token, 'Content-Type': 'application/json' },
15
+ body: { username: email, email: email, firstName: first_name, lastName: last_name,
16
+ attributes: {}, groups: [], enabled: true }.to_json)
17
+ raise StandardError, request[:response] unless request[:status] == :ok
18
+
19
+ set_perm_password(email, password)
20
+ end
21
+
22
+ def current_user_has_active_session?
23
+ KeycloakRails.currnet_session_cookie && current_cookie_active?
24
+ end
25
+
26
+ def current_cookie_active?
27
+ token_introspection['active'] ? true : KeycloakRails.destroy_auth_cookies
28
+ end
29
+
30
+ # private
31
+
32
+ def token_introspection
33
+ request = @curl.post(path: KeycloakRails.openid_config['introspection_endpoint'],
34
+ headers: { 'Authorization': basic_auth_token,
35
+ 'Content-Type': 'application/x-www-form-urlencoded' },
36
+ body: { "token": KeycloakRails.currnet_session_cookie })
37
+ raise StandardError, request[:response] unless request[:status] == :ok
38
+
39
+ request[:response]
40
+ end
41
+
42
+ def verify_email(user_id)
43
+ request = @curl.put(path: "/admin/realms/#{KeycloakRails.realm}/users/#{user_id}",
44
+ headers: { 'Authorization': client_token, 'Content-Type': 'application/json' },
45
+ body: { "emailVerified": true }.to_json)
46
+ raise StandardError, request[:response] unless request[:status] == :ok
47
+
48
+ request[:response]
49
+ end
50
+
51
+ def update_user_attributes(user_id, attributes)
52
+ request = @curl.put(path: "/admin/realms/#{KeycloakRails.realm}/users/#{user_id}",
53
+ headers: { 'Authorization': client_token, 'Content-Type': 'application/json' },
54
+ body: attributes.to_json)
55
+ raise StandardError, request[:response] unless request[:status] == :ok
56
+
57
+ request[:response]
58
+ end
59
+
60
+ def require_set_otp(user_email)
61
+ user = user_by_username(user_email)
62
+ required_actions = user['requiredActions'].push("CONFIGURE_TOTP")
63
+ request = @curl.put(path: "/admin/realms/#{KeycloakRails.realm}/users/#{user['id']}",
64
+ headers: { 'Authorization': client_token, 'Content-Type': 'application/json' },
65
+ body: { "requiredActions": required_actions }.to_json)
66
+ raise StandardError, request[:response] unless request[:status] == :ok
67
+
68
+ request[:response]
69
+ end
70
+
71
+ def set_perm_password(email, password)
72
+ user = user_by_username(email)
73
+ request = @curl.put(path: "/admin/realms/#{KeycloakRails.realm}/users/#{user['id']}/reset-password",
74
+ headers: { 'Authorization': client_token, 'Content-Type': 'application/json' },
75
+ body: { 'type': 'password', 'temporary': false, 'value': password }.to_json)
76
+ raise StandardError, request[:response] unless request[:status] == :ok
77
+
78
+ request[:response]
79
+ end
80
+
81
+ def get_magic_link(email:, redirect_uri:, expiration_seconds: 3600, force_create: false, send_email: false, client_id: KeycloakRails.client_id)
82
+ request = @curl.post(path: "/auth/realms/#{KeycloakRails.realm}/magic-link",
83
+ headers: { 'Authorization': client_token, 'Content-Type': 'application/json' },
84
+ body: { "email": email, "client_id": client_id,
85
+ "redirect_uri": redirect_uri, "expiration_seconds": expiration_seconds,
86
+ "force_create": force_create, "update_profile": force_create,
87
+ "send_email": send_email }.to_json)
88
+ raise StandardError, request[:response] unless request[:status] == :ok
89
+
90
+ request[:response]
91
+ end
92
+
93
+ def user_by_username(email)
94
+ request = @curl.get(path: "admin/realms/#{KeycloakRails.realm}/users?username=#{email}&exact=true",
95
+ headers: { 'Authorization': client_token, 'Content-Type': 'application/json' },
96
+ body: { username: email, exact: true }.to_json)
97
+
98
+ raise StandardError, request[:response] unless request[:status] == :ok
99
+
100
+ request[:response]&.first
101
+ end
102
+
103
+ def basic_auth_token
104
+ "Basic #{Base64.strict_encode64("#{KeycloakRails.client_id}:#{KeycloakRails.secret}")}"
105
+ end
106
+
107
+ # <---- USE WISELY!!!! ----->
108
+ def client_token
109
+ "bearer #{fetch_client_token['access_token']}"
110
+ end
111
+
112
+ def fetch_client_token
113
+ request = @curl.post(path: "realms/#{KeycloakRails.realm}/protocol/openid-connect/token",
114
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
115
+ body: { 'grant_type': 'client_credentials', 'client_id': KeycloakRails.client_id,
116
+ 'client_secret': KeycloakRails.secret })
117
+ raise StandardError, request[:response] unless request[:status] == :ok
118
+
119
+ request[:response]
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeycloakRails
4
+ # name space
5
+ module Controller
6
+ # controller helpers added to ActionController::Base class
7
+ module Helpers
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ helper_method :generate_magic_link, :current_user
12
+
13
+ def initialize
14
+ kick_start_auth_server_connection
15
+ set_auth_cookies_procs
16
+ set_destroy_auth_cookies
17
+ super
18
+ end
19
+
20
+ def ensure_active_session(accept_magic_link_handshake: false)
21
+ redirect_to root_path unless user_has_active_sso_session?(accept_magic_link_handshake: accept_magic_link_handshake)
22
+ end
23
+
24
+ def ensure_no_active_session
25
+ redirect_to root_path if user_has_active_sso_session?
26
+ end
27
+
28
+ def user_has_active_sso_session?(accept_magic_link_handshake: false)
29
+ (keycloak_client.current_user_has_active_session? && current_user) ||
30
+ (accept_magic_link_handshake && ensure_magic_link_code)
31
+ end
32
+
33
+ def keycloak_client
34
+ @keycloak_client ||= KeycloakRails::Client.new
35
+ end
36
+
37
+ def keycloak_user
38
+ @keycloak_user ||= KeycloakRails::User.new
39
+ end
40
+
41
+ # need to think of away to capture the user model if we wanna set current_user
42
+ # initial thought: use Dry::Configurable object defined on the Keycloak module to capture the model name and meta program my way from there
43
+ def current_user
44
+ @current_user ||= KeycloakRails::Sso.includes(:recipient).find_by(sub: keycloak_user.active_user_sub)&.recipient
45
+ # have a join table keycloak_rails_subs sso_sub:string #{KeycloakRails.user_model}_id:refrence
46
+ # to KeycloakRails.user_model.rb and add has_one :keycloak_rails_sub
47
+ # delegate sso_sub to {KeycloakRails.user_model}
48
+ #
49
+ end
50
+
51
+ def destroy_current_user
52
+ @current_user = nil
53
+ end
54
+
55
+ def destroy_session_cookie
56
+ cookies.permanent[:keycloak_session_token] = nil
57
+ end
58
+
59
+ def kick_start_auth_server_connection
60
+ keycloak_client
61
+ keycloak_user
62
+ end
63
+
64
+ def set_auth_cookies_procs
65
+ KeycloakRails.session_cookie_proc = -> { cookies.encrypted.permanent[:keycloak_session_token] }
66
+ KeycloakRails.refresh_cookie_proc = -> { cookies.encrypted.permanent[:keycloak_refresh_token] }
67
+ end
68
+
69
+ def set_auth_cookies(tokens)
70
+ cookies.encrypted.permanent[:keycloak_session_token] = tokens['access_token']
71
+ cookies.encrypted.permanent[:keycloak_refresh_token] = tokens['refresh_token']
72
+ true
73
+ end
74
+
75
+ def set_destroy_auth_cookies
76
+ KeycloakRails.destroy_session_cookie_proc = -> { cookies.permanent[:keycloak_session_token] = nil }
77
+ KeycloakRails.destroy_refresh_cookie_proc = -> { cookies.permanent[:keycloak_refresh_token] = nil }
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeycloakRails
4
+ # name space
5
+ module Controller
6
+ # controller helpers added to ActionController::Base class
7
+ # only loaded if KeycloakRails.allow_magic_link = true
8
+ module MagicLinks
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+
13
+ def generate_magic_link(url:, email: , expiration_seconds: 3600, force_create: false, send_email: false, client_id: KeycloakRails.client_id)
14
+ magic_link_obj = keycloak_client.get_magic_link(email: email,
15
+ redirect_uri: url,
16
+ expiration_seconds: expiration_seconds,
17
+ force_create: force_create,
18
+ send_email: send_email,
19
+ client_id: client_id)
20
+ magic_link_obj.except(force_create ? nil : 'user_id')
21
+ .except(send_email ? nil : 'sent')
22
+ end
23
+
24
+ def ensure_magic_link_code
25
+ return false unless magic_link_params[:code] && magic_link_params[:session_state]
26
+ return false unless login_by_handshake
27
+
28
+ true
29
+ end
30
+
31
+ def login_by_handshake(persist_session: true)
32
+ redirect_uri = url_for(only_path: false, overwrite_params: nil)
33
+ tokens = keycloak_user.fetch_tokens_by_handshake(code: magic_link_params[:code], redirect_uri: redirect_uri)
34
+ persist_session ? set_auth_cookies(tokens) : tokens
35
+ end
36
+
37
+ def client_user?
38
+ !!current_user
39
+ end
40
+
41
+ def magic_link_params
42
+ params.permit(:session_state, :code)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeycloakRails
4
+ # name space
5
+ module Controller
6
+ # controller helpers added to KeycloakRails.omniauth controller
7
+ module Omniauth
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeycloakRails
4
+ # name space
5
+ module Controller
6
+ # controller helpers added to KeycloakRails passwords controller
7
+ module Passwords
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ def reset_password(new_password:, new_password_confirmation:, email: current_user.email)
12
+ raise StandardError, 'Passwords must match' unless new_password == new_password_confirmation
13
+
14
+ keycloak_client.set_perm_password(email, new_password)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeycloakRails
4
+ # name space
5
+ module Controller
6
+ # controller helpers added to KeycloakRails.registrations controller
7
+ module Registrations
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ def create_or_find_sso_user(email:, password:, first_name:, last_name:, password_confirmation: nil, set_session: true)
12
+ user = keycloak_client.user_by_username(email)
13
+ if user
14
+ { sso_sub: user['id'], email: email,
15
+ first_name: first_name, last_name: last_name }
16
+ else
17
+ create_sso_user(email: email, password: password, first_name: first_name, last_name: last_name, password_confirmation: password_confirmation, set_session: set_session)
18
+ end
19
+ end
20
+
21
+ def create_sso_user(email:, password:, first_name:, last_name:, password_confirmation: nil, set_session: true)
22
+ raise StandardError, 'Passwords must match' if password_confirmation && password != password_confirmation
23
+
24
+ keycloak_client.create_user(email: email,
25
+ password: password,
26
+ first_name: first_name,
27
+ last_name: last_name)
28
+ if set_session
29
+ tokens = keycloak_user.fetch_tokens(email: email, password: password)
30
+ set_auth_cookies(tokens)
31
+ end
32
+ { sso_sub: keycloak_user.active_user_sub, email: email,
33
+ first_name: first_name, last_name: last_name }
34
+ end
35
+
36
+ def update_sso_record_attributes(user_sub, attributes)
37
+ attributes = attributes.transform_keys { |key| key.to_s.camelize(:lower) }
38
+ keycloak_client.update_user_attributes(user_sub, attributes)
39
+ end
40
+
41
+ def mark_email_verified(user_sub)
42
+ keycloak_client.verify_email(user_sub)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeycloakRails
4
+ # name space
5
+ module Controller
6
+ # controller helpers added to KeycloakRails.sessions controller
7
+ module Sessions
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ def start_sso_session(email, password)
12
+ tokens = keycloak_user.fetch_tokens(email: email, password: password)
13
+ set_auth_cookies(tokens)
14
+ current_user
15
+ redirect_to_app_root
16
+ end
17
+
18
+ def end_sso_session
19
+ redirect_uri = url_for(only_path: false, overwrite_params: nil)
20
+ keycloak_user.end_session(redirect_uri)
21
+ destroy_current_user
22
+ KeycloakRails.destroy_auth_cookies
23
+ redirect_to_app_root
24
+ end
25
+
26
+ def redirect_to_app_root
27
+ # redirect_back(fallback_location: root_path)
28
+ redirect_to root_path
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeycloakRails
4
+ # name space
5
+ module Controller
6
+ # controller helpers added to KeycloakRails.unlocks controller
7
+ module Unlocks
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeycloakRails
4
+ # https client with farady v > 2.0.0
5
+ class Curl
6
+ def initialize
7
+ @faraday = Faraday.new(url: KeycloakRails.auth_server_url)
8
+ end
9
+
10
+ def post(path: '', headers: { 'Content-Type': 'application/json' }, body: {})
11
+ request = @faraday.post(path) do |req|
12
+ req.headers = headers
13
+ req.body = body
14
+ end
15
+ extract_response(request)
16
+ end
17
+
18
+ def get(path: '', headers: { 'Content-Type': 'application/json' }, body: {})
19
+ request = @faraday.get(path) do |req|
20
+ req.headers = headers
21
+ req.body = body
22
+ end
23
+ extract_response(request)
24
+ end
25
+
26
+ def patch(path: '', headers: { 'Content-Type': 'application/json' }, body: {})
27
+ request = @faraday.patch(path) do |req|
28
+ req.headers = headers
29
+ req.body = body
30
+ end
31
+ extract_response(request)
32
+ end
33
+
34
+ def put(path: '', headers: { 'Content-Type': 'application/json' }, body: {})
35
+ request = @faraday.put(path) do |req|
36
+ req.headers = headers
37
+ req.body = body
38
+ end
39
+ extract_response(request)
40
+ end
41
+
42
+ private
43
+
44
+ def extract_response(request)
45
+ case request.status
46
+ when 200..299 then response_to request, message: 'succeeded', status: :ok
47
+ when 300..399 then response_to request, message: 'succeeded', status: :ok
48
+ when 400..499 then response_to request, message: 'something went wrong', status: :unprocessed
49
+ when 500..599 then response_to request, message: 'something went wrong', status: :unprocessed
50
+ else response_to request, message: 'something went wrong', status: :unprocessed
51
+ end
52
+ end
53
+
54
+ def response_to(request, message: "", status: :ok)
55
+ { response: request.body && request.body != '' ? JSON.parse(request.body) : {}, message: message, status: status }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeycloakRails
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace(KeycloakRails)
6
+
7
+ initializer('keycloack_rails', after: :load_config_initializers) do
8
+ ActionController::Base.include KeycloakRails::Controller::Helpers
9
+ ActionController::Base.include KeycloakRails::Controller::Sessions
10
+ ActionController::Base.include KeycloakRails::Controller::Registrations
11
+ ActionController::Base.include KeycloakRails::Controller::Passwords
12
+ ActionController::Base.include KeycloakRails::Controller::MagicLinks if KeycloakRails.allow_magic_links
13
+ end
14
+
15
+ config.after_initialize do
16
+ if KeycloakRails.user_model
17
+ user_klass = KeycloakRails.user_model
18
+ user_klass.singularize.classify.constantize.include KeycloakRails::SsoRecipient
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeycloakRails
4
+ # User lvl access to sso server established session_token after auth with username and password
5
+ # min perms
6
+ class User
7
+ def initialize
8
+ @curl = KeycloakRails::Curl.new
9
+ end
10
+
11
+ def fetch_tokens(email:, password:, otp_password: nil)
12
+ request = @curl.post(path: KeycloakRails.openid_config['token_endpoint'],
13
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
14
+ body: { 'grant_type': 'password', 'client_id': KeycloakRails.client_id,
15
+ 'client_secret': KeycloakRails.secret, 'username': email,
16
+ 'password': password }.merge((otp_password ? { 'totp': otp_password } : {})))
17
+ raise StandardError, request[:response] unless request[:status] == :ok
18
+
19
+ request[:response]
20
+ end
21
+
22
+ def fetch_tokens_by_handshake(code:, redirect_uri:)
23
+ request = @curl.post(path: KeycloakRails.openid_config['token_endpoint'],
24
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
25
+ body: { 'grant_type': 'authorization_code', 'client_id': KeycloakRails.client_id,
26
+ 'client_secret': KeycloakRails.secret, code: code,
27
+ "redirect_uri": redirect_uri })
28
+ raise StandardError, request[:response] unless request[:status] == :ok
29
+
30
+ request[:response]
31
+ end
32
+
33
+ def fetch_current_user_info
34
+ request = @curl.post(path: KeycloakRails.openid_config['userinfo_endpoint'],
35
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
36
+ body: { access_token: access_token })
37
+ raise StandardError, request[:response] unless request[:status] == :ok
38
+
39
+ request[:response]
40
+ end
41
+
42
+ def end_session(redirect_uri)
43
+ request = @curl.post(path: KeycloakRails.openid_config['end_session_endpoint'],
44
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
45
+ body: { 'client_id': KeycloakRails.client_id,
46
+ 'client_secret': KeycloakRails.secret,
47
+ 'refresh_token': KeycloakRails.current_refresh_cookie,
48
+ 'post_logout_redirect_uri': redirect_uri })
49
+ raise StandardError, request[:response] unless request[:status] == :ok
50
+
51
+ request[:response]
52
+ end
53
+
54
+ # gets user sub by making an api call to auth server
55
+ def fetch_active_user_sub
56
+ fetch_current_user_info['sub']
57
+ end
58
+
59
+ # gets user sub by decoding the session_cookie
60
+ def decode_active_user_sub
61
+ decoded_access_token.first['sub']
62
+ end
63
+
64
+ def active_user_sub
65
+ return unless access_token
66
+ case KeycloakRails.decode_token_strategy
67
+ when :local then decode_active_user_sub
68
+ when :cloud then fetch_active_user_sub
69
+ end
70
+ end
71
+
72
+ # private
73
+
74
+ def access_token
75
+ KeycloakRails.currnet_session_cookie
76
+ end
77
+
78
+ def decoded_access_token
79
+ decode(access_token)
80
+ end
81
+
82
+ def decoded_refresh_token
83
+ decode(refresh_token)
84
+ end
85
+
86
+ def decode(token)
87
+ JWT.decode token, KeycloakRails.public_key, false, { algorithm: KeycloakRails.signature_algo }
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,3 @@
1
+ module KeycloakRails
2
+ VERSION = '1.0.0-beta'
3
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # requires all dependencies
4
+ require 'faraday'
5
+ require 'jwt'
6
+ require 'dry/configurable'
7
+
8
+ # requires all modules and classes
9
+ require 'keycloak_rails/version'
10
+ require 'keycloak_rails/engine'
11
+ require 'keycloak_rails/client'
12
+ require 'keycloak_rails/user'
13
+ require 'keycloak_rails/curl'
14
+ require 'keycloak_rails/controller/helpers'
15
+ require 'keycloak_rails/controller/magic_links'
16
+ require 'keycloak_rails/controller/omniauth'
17
+ require 'keycloak_rails/controller/passwords'
18
+ require 'keycloak_rails/controller/registrations'
19
+ require 'keycloak_rails/controller/sessions'
20
+ require 'keycloak_rails/controller/unlocks'
21
+ require 'app/models/keycloak_rails/sso'
22
+ require 'app/models/keycloak_rails/concerns/sso_recipient'
23
+
24
+ module KeycloakRails
25
+ extend Dry::Configurable
26
+
27
+ setting :sessions_controller, reader: true
28
+ setting :registrations_controller, reader: true
29
+ setting :unlocks_controller, reader: true
30
+ setting :passwords_controller, reader: true
31
+ setting :omniauth_controller, reader: true
32
+ setting :user_model, reader: true
33
+ setting :realm, reader: true
34
+ setting :public_key, reader: true
35
+ setting :auth_server_url, reader: true
36
+ setting :secret, reader: true
37
+ setting :client_id, reader: true
38
+ setting :decode_token_strategy, reader: true, default: :local
39
+ setting :signature_algo, reader: true
40
+ setting :allow_magic_links, reader: true, default: false
41
+
42
+ class << self
43
+ attr_accessor :session_cookie_proc, :destroy_session_cookie_proc, :refresh_cookie_proc, :destroy_refresh_cookie_proc
44
+
45
+ def currnet_session_cookie
46
+ session_cookie_proc.call
47
+ end
48
+
49
+ def current_refresh_cookie
50
+ refresh_cookie_proc.call
51
+ end
52
+
53
+ def destroy_auth_cookies
54
+ destroy_session_cookie_proc.call
55
+ destroy_refresh_cookie_proc.call
56
+ end
57
+
58
+ def openid_config
59
+ @openid_config ||= fetch_openid_configuration
60
+ end
61
+
62
+ def fetch_openid_configuration
63
+ request = Curl.new.get(path: "realms/#{realm}/.well-known/openid-configuration",
64
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' })
65
+ raise StandardError, request[:response] unless request[:status] == :ok
66
+
67
+ request[:response]
68
+ end
69
+ end
70
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: keycloak_rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.pre.beta
5
+ platform: ruby
6
+ authors:
7
+ - Omar Luqman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-12-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-configurable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.16'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 2.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 2.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: jwt
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '2.4'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '2.4'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 6.0.3
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 6.0.3
69
+ description: A rails wrapper for open source SSO project Keycloak.
70
+ email:
71
+ - oluqman@nucleushealthcare.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - MIT-LICENSE
77
+ - README.md
78
+ - Rakefile
79
+ - lib/app/models/keycloak_rails/concerns/sso_recipient.rb
80
+ - lib/app/models/keycloak_rails/sso.rb
81
+ - lib/generators/keycloak_rails/config/config_generator.rb
82
+ - lib/generators/keycloak_rails/install/install_generator.rb
83
+ - lib/generators/keycloak_rails/keycloak_rails.rb
84
+ - lib/keycloak_rails.rb
85
+ - lib/keycloak_rails/client.rb
86
+ - lib/keycloak_rails/controller/helpers.rb
87
+ - lib/keycloak_rails/controller/magic_links.rb
88
+ - lib/keycloak_rails/controller/omniauth.rb
89
+ - lib/keycloak_rails/controller/passwords.rb
90
+ - lib/keycloak_rails/controller/registrations.rb
91
+ - lib/keycloak_rails/controller/sessions.rb
92
+ - lib/keycloak_rails/controller/unlocks.rb
93
+ - lib/keycloak_rails/curl.rb
94
+ - lib/keycloak_rails/engine.rb
95
+ - lib/keycloak_rails/user.rb
96
+ - lib/keycloak_rails/version.rb
97
+ homepage: https://github.com/Laborocity/keycloak_rails
98
+ licenses:
99
+ - MIT
100
+ metadata:
101
+ homepage_uri: https://github.com/Laborocity/keycloak_rails
102
+ source_code_uri: https://github.com/Laborocity/keycloak_rails
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">"
115
+ - !ruby/object:Gem::Version
116
+ version: 1.3.1
117
+ requirements: []
118
+ rubygems_version: 3.3.7
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: "%q{API wrapper for Key Cloak SSO server.}"
122
+ test_files: []