strongmind-auth 1.0.10 → 1.0.12

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f48f262a82aa122f2c2e02f24a80af05e8ddc16c47b03f74e2ae6cd1e75f7b02
4
- data.tar.gz: 178b4506c5e76422842f944e74f4fb24e94785085be8a3fa62699a3d12d1b88c
3
+ metadata.gz: 9f0de84646fc0bb458a34c17e3b6ba8e8f5754252cb5da1957822e75a7c52d4f
4
+ data.tar.gz: 3f944f111eb1b254ed97a13baf7b5e0d74401bb4edce2326ed36a94742af4499
5
5
  SHA512:
6
- metadata.gz: 0fc9d5270f0f4c4f94db091278e00b928c3ba566d1a6eaa0c400ca6aa5b2ed59cbe1bf3169d69ad22906394a9c0a9cfe9195d4e65069cb465db54321eb87edb0
7
- data.tar.gz: 3e3a8968049708aa7cf9db09d2a08387c95c59b92a19f84bdeaf070319ea85e7c3b19eb46160cb6ecc6cfe0acb75d34d244ee451e6dd90afdc3786f054e8812f
6
+ metadata.gz: cbba7ce9b16417331e9ec33a7b25225bb2ff3d3bc9651ac36936543197f1e95caec5c842100965b6564150c92015d46af23dfab26999db0f4847755d112eb775
7
+ data.tar.gz: 48fa3ec4e620252c4f1dfb88cd3f58df46f0054defe2f348d3282cfde28dc58829b628b6dfa5f054c5e2681b105b92b31e5c984a5711fdb68cfd1abd5b6cc7f0
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mix-in for handling JWTs
4
+ module JwtUtilities
5
+ extend ActiveSupport::Concern
6
+
7
+ def jwt_valid?(jwt, condition_key = nil, scopes = [])
8
+ begin
9
+ payload, _header = JWT.decode(jwt, public_key, true, {
10
+ verify_iat: true,
11
+ verify_iss: true,
12
+ verify_aud: true,
13
+ verify_sub: true,
14
+ algorithm: 'RS256',
15
+ leeway: 60
16
+ })
17
+ rescue JWT::DecodeError => e
18
+ Rails.logger.error e.message
19
+ return false
20
+ end
21
+
22
+ payload = payload.with_indifferent_access
23
+
24
+ unless !scopes.empty? && payload['scope'].present? && payload['scope'].all? { |elem| scopes.include?(elem) }
25
+ return false
26
+ end
27
+
28
+ return false unless payload['nonce'].nil?
29
+
30
+ return false unless condition_key.nil? || payload['events'].key?(condition_key)
31
+
32
+ true
33
+ end
34
+
35
+ def public_key
36
+ Rails.cache.fetch('jwt_utilities_public_key', expires_in: 1.day) do
37
+ x5c_val = OpenIDConnect::Discovery::Provider::Config.discover!(ENV['IDENTITY_BASE_URL']).jwks.first['x5c'].first
38
+ cert = OpenSSL::X509::Certificate.new(Base64.decode64(x5c_val))
39
+ cert.public_key
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def fetch_user_token_info
46
+ user_jwt(session)
47
+ end
48
+
49
+ def user_access_token(session_data)
50
+ tokens = user_jwt(session_data)
51
+ tokens[:access_token]
52
+ end
53
+
54
+ def user_jwt(session_data)
55
+ tokens = current_user.nil? ? nil : Rails.cache.read(current_user&.uid)
56
+ validate_tokens(tokens) unless tokens.nil?
57
+
58
+ if tokens.nil?
59
+ tokens = generate_tokens(session_data)
60
+ validate_tokens(tokens)
61
+
62
+ unless current_user.nil?
63
+ tokens[:expires_in] = 1.hour.to_i if tokens[:expires_in].nil?
64
+ Rails.cache.write(current_user&.uid, tokens)
65
+ end
66
+ end
67
+ session_data[:refresh_token] = tokens[:refresh_token]
68
+
69
+ tokens
70
+ end
71
+
72
+ def validate_tokens(tokens)
73
+ return unless tokens[:error] == 'invalid_grant' || !tokens[:refresh_token]
74
+
75
+ raise RefreshTokenExpired, tokens[:error]
76
+ end
77
+
78
+ def generate_tokens(session_data)
79
+ identity_base_url = ENV['IDENTITY_BASE_URL']
80
+ identity_client_id = ENV['IDENTITY_CLIENT_ID']
81
+ response = Faraday.post("#{identity_base_url}/connect/token", {
82
+ client_id: identity_client_id,
83
+ client_secret: ENV['IDENTITY_CLIENT_SECRET'],
84
+ grant_type: 'refresh_token',
85
+ refresh_token: session_data[:refresh_token]
86
+ })
87
+
88
+ JSON.parse(response.body, symbolize_names: true)
89
+ end
90
+ end
@@ -12,6 +12,9 @@ module StrongMindNav
12
12
  @theme_css = navbar[:theme_css]
13
13
  rescue Strongmind::CommonNavFetcher::TokenNotFoundError, Strongmind::CommonNavFetcher::UserNotFoundError => e
14
14
  Sentry.capture_exception(e)
15
+ Rails.logger.error(e)
16
+ flash[:alert] = e.inspect if Rails.env.development?
17
+ @stop_redirect = true if Rails.env.development?
15
18
  render 'logins/index'
16
19
  rescue Exception => e
17
20
  Sentry.capture_exception(e)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Users
4
+ class RefreshTokenExpired < StandardError
5
+ end
6
+
7
+ class SessionsController < Devise::SessionsController
8
+ include JwtUtilities
9
+
10
+ skip_before_action :fetch_common_nav
11
+ skip_before_action :verify_authenticity_token, only: :endsession
12
+
13
+ def login
14
+ redirect_to user_strongmind_omniauth_authorize_url
15
+ end
16
+
17
+ def new
18
+ redirect_to user_strongmind_omniauth_authorize_url
19
+ end
20
+
21
+ def endsession
22
+ headers = { 'Cache-Control' => 'no-store' }
23
+ if jwt_valid?(params[:logout_token], 'http://schemas.openid.net/event/backchannel-logout')
24
+ payload, _header = JWT.decode(params[:logout_token], nil, false)
25
+ user_identity = payload['sub']
26
+ user = User.find_by(uid: user_identity)
27
+ user.invalidate_all_sessions!
28
+ render json: {}, status: :ok, headers:
29
+ else
30
+ render json: {}, status: :bad_request, headers:
31
+ end
32
+ end
33
+
34
+ def initiate_backchannel_logout
35
+ user_token_info = fetch_user_token_info
36
+
37
+ id_token_hint = user_token_info[:id_token]
38
+ token = user_token_info[:access_token]
39
+ current_user&.invalidate_all_sessions!
40
+ identity_base_url = ENV['IDENTITY_BASE_URL']
41
+ redirect_to "#{identity_base_url}/connect/endsession?id_token_hint=#{id_token_hint}", headers: {
42
+ 'Content-Type' => 'application/json',
43
+ 'Authorization' => "Bearer #{token}"
44
+ }, allow_other_host: true
45
+ end
46
+
47
+ end
48
+ end
@@ -28,4 +28,14 @@ class UserBase < ApplicationRecord
28
28
  def auth_token_cache
29
29
  Rails.cache.read(uid)
30
30
  end
31
+
32
+ def authenticatable_salt
33
+ return super unless session_token
34
+
35
+ "#{super}#{session_token}"
36
+ end
37
+
38
+ def invalidate_all_sessions!
39
+ update_attribute(:session_token, SecureRandom.hex)
40
+ end
31
41
  end
@@ -22,13 +22,22 @@
22
22
 
23
23
  // Submit the form on load
24
24
  window.addEventListener("load", (event) => {
25
+ <% if @stop_redirect %>
26
+ return;
27
+ <% end %>
25
28
  submitForm();
26
29
  });
27
30
 
28
31
  </script>
29
32
  <div id="loading">
33
+ <% flash.each do |type, message| %>
34
+ <div class="alert alert-<%= type %>"><%= message %></div>
35
+ <% end %>
36
+ <% flash.clear %>
30
37
  <div class="sm-loader">
31
- <img src="https://prod-backpack-ui.strongmind.com/assets/images/strongmind-loader.svg">
38
+ <% unless @stop_redirect %>
39
+ <img src="https://prod-backpack-ui.strongmind.com/assets/images/strongmind-loader.svg">
40
+ <% end %>
32
41
  </div>
33
42
 
34
43
  </div>
@@ -1,32 +1,129 @@
1
- Your StrongMind identity client does not appear to be configured correctly.
2
- <br/>
3
- Please follow these steps:
4
- <br/>
5
- <br/>
6
1
  <%
7
- app_name = Rails.application.class.name.split("::").first
8
- app_url = ENV['APP_BASE_URL']
9
- stage_url = "https://devlogin.strongmind.com/Clients/Create?ClientID=#{app_name}&RedirectURL=#{app_url}/users/auth/strongmind/callback"
10
- prod_url = "https://login.strongmind.com/Clients/Create?ClientID=#{app_name}&RedirectURL=#{app_url}/users/auth/strongmind/callback"
2
+ require 'json'
3
+
4
+ if Rails.env.development?
5
+ app_name = Rails.application.class.name.split("::").first.underscore.dasherize
6
+ if app_name == "app"
11
7
  %>
8
+ Please set the name of your application in the module line of config/application.rb and restart your server.
9
+ <%
10
+ else
11
+ %>
12
+ <script>
13
+ function toggleInstructions() {
14
+ if (document.getElementById('new_app_yes').checked) {
15
+ document.getElementById('new_app_instructions').style.display = 'block';
16
+ document.getElementById('existing_app_instructions').style.display = 'none';
17
+ } else {
18
+ document.getElementById('new_app_instructions').style.display = 'none';
19
+ document.getElementById('existing_app_instructions').style.display = 'block';
20
+ }
21
+ }
22
+ </script>
23
+ <div>
24
+ Your StrongMind Identity client does not appear to be configured correctly.
25
+ </div>
26
+ <div>
27
+ Is this a brand new app that needs to be setup in StrongMind Identity?
28
+ </div>
29
+ <div class="flex">
30
+ <input type="radio" name="new_app" value="yes" id="new_app_yes" onclick="toggleInstructions()">
31
+ <label for="new_app_yes" style="margin-left: 5px">Yes</label>
32
+ </div>
33
+ <div class="flex">
34
+ <input type="radio" name="new_app" value="no" id="new_app_no" onclick="toggleInstructions()">
35
+ <label for="new_app_no" style="margin-left: 5px">No</label>
36
+ </div>
37
+
38
+ <div id="existing_app_instructions" style="display: none; margin-top: 10px">
39
+ Grab the .env file from Bitwarden and place it in the root of your project. Restart your server.
40
+ </div>
41
+ <div id="new_app_instructions" style="display: none; margin-top: 10px">
42
+ <div>
43
+ Please follow these steps:
44
+ </div>
45
+ <%
46
+ local_app_url = "http://localhost:3000"
47
+ stage_app_url = "https://stage-#{app_name}.strongmind.com"
48
+ prod_app_url = "https://#{app_name}.strongmind.com"
49
+ local_redirect_url = "#{local_app_url}/users/auth/strongmind/callback"
50
+ stage_redirect_url = "#{stage_app_url}/users/auth/strongmind/callback"
51
+ prod_redirect_url = "#{prod_app_url}/users/auth/strongmind/callback"
52
+ stage_post_logout_redirect_url = "https://stage-#{app_name}.strongmind.com"
53
+ prod_post_logout_redirect_url = "https://#{app_name}.strongmind.com"
54
+ stage_backchannel_logout_url = "https://stage-#{app_name}.strongmind.com/users/endsession"
55
+ prod_backchannel_logout_url = "https://#{app_name}.strongmind.com/users/endsession"
56
+
57
+ stage_login_base_url = "https://devlogin.strongmind.com"
58
+ prod_login_base_url = "https://login.strongmind.com"
59
+ stage_secret = SecureRandom.hex(16)
60
+ prod_secret = SecureRandom.hex(16)
61
+ stage_url = "#{stage_login_base_url}/Clients/Create?"
62
+ stage_url += "ClientID=#{app_name}&"
63
+ stage_url += "RedirectURL=#{local_redirect_url}&"
64
+ stage_url += "RedirectURL=#{stage_redirect_url}&"
65
+ stage_url += "PostLogoutRedirectURL=#{stage_post_logout_redirect_url}&"
66
+ stage_url += "BackChannelLogoutUri=#{stage_backchannel_logout_url}&"
67
+ stage_url += "ClientSecret=#{stage_secret}"
68
+
69
+ prod_url = "#{prod_login_base_url}/Clients/Create?"
70
+ prod_url += "ClientID=#{app_name}&"
71
+ prod_url += "RedirectURL=#{prod_redirect_url}&"
72
+ prod_url += "PostLogoutRedirectURL=#{prod_post_logout_redirect_url}&"
73
+ prod_url += "BackChannelLogoutUri=#{prod_backchannel_logout_url}&"
74
+ prod_url += "ClientSecret=#{prod_secret}"
75
+
76
+ env_file_additions = "IDENTITY_CLIENT_ID=#{app_name}\nIDENTITY_CLIENT_SECRET=#{stage_secret}\n# Production\n#IDENTITY_CLIENT_SECRET=#{prod_secret}"
77
+ %>
78
+
79
+ <ol style="list-style: decimal">
80
+ <li>
81
+ <%= link_to "Create Client in Staging Identity Server", stage_url, { target: "_blank" } %>
82
+ </li>
83
+ <li>
84
+ <%= link_to "Create Client in Production Identity Server", prod_url, { target: "_blank" } %>
85
+ </li>
86
+ <li>
87
+ <div>
88
+ Set the following environment variables in your .env file:
89
+ </div>
90
+ <textarea style="width: 100%; height: 200px"><%= env_file_additions %></textarea>
91
+ <br/>
92
+ <button onclick="navigator.clipboard.writeText(document.querySelector('textarea').value)">
93
+ Copy to clipboard
94
+ </button>
95
+ <br/><br/>
96
+ </li>
97
+ <li>
98
+ Save the .env file into a new Bitwarden item called "<%= app_name %> .env"
99
+ </li>
100
+ <li>
101
+ Restart your server.
102
+ </li>
103
+ </ol>
104
+ </div>
105
+ <%
106
+ end
107
+ else %>
108
+ This application is not configured properly.
109
+ <br/>
110
+ Please contact your nearest engineer using a ticket.
111
+ <br/>
112
+
113
+ Provide them this information:
114
+ <%
115
+ info = {
116
+ url: request.url,
117
+ error: request.env['omniauth.error']
118
+ }
119
+ %>
120
+ <textarea style="width: 100%; height: 200px"><%= JSON.pretty_generate(info) %></textarea>
121
+ <!-- copy to clipboard -->
122
+ <button onclick="navigator.clipboard.writeText(document.querySelector('textarea').value)">
123
+ Copy to clipboard
124
+ </button>
12
125
 
13
- <ol style="list-style: decimal">
14
- <li>
15
- <%= link_to "Create Client in Staging Identity Server", stage_url %>
16
- </li>
17
- <li>
18
- <%= link_to "Create Client in Production Identity Server", prod_url %>
19
- </li>
20
- <li>
21
- Set the following environment variables in your .env file:
22
- <br/>
23
- <br/>
24
- <pre>
25
- IDENTITY_CLIENT_ID=<%= app_name %><br/>IDENTITY_CLIENT_SECRET={use the secret you generated for the client}
26
- </pre>
27
-
28
- </li>
29
- <li>
30
- Restart your server.
31
- </li>
32
- </ol>
126
+ <div>
127
+ <%= link_to "Back to Home", "/", data: { turbo: false } %>
128
+ </div>
129
+ <% end %>
data/config/routes.rb CHANGED
@@ -3,11 +3,12 @@ Rails.application.routes.draw do
3
3
  return if defined? Rails::Generators
4
4
 
5
5
  devise_for :users, controllers: {
6
- omniauth_callbacks: "users/omniauth_callbacks"
6
+ omniauth_callbacks: 'users/omniauth_callbacks',
7
+ sessions: 'users/sessions'
7
8
  }
8
9
 
9
10
  devise_scope :user do
10
- post 'users/sign_out', to: 'devise/sessions#destroy'
11
+ get 'users/sign_out', to: 'users/sessions#initiate_backchannel_logout'
11
12
 
12
13
  unauthenticated do
13
14
  root 'logins#index', as: :unauthenticated_root
@@ -13,13 +13,25 @@ module Strongmind
13
13
 
14
14
  def protect_app_files_and_add_nav
15
15
  inject_into_file "app/controllers/application_controller.rb", after: "class ApplicationController < ActionController::Base\n" do
16
- " include StrongMindNav\n before_action :authenticate_user!\n before_action :fetch_common_nav\n
16
+ " include StrongMindNav
17
+ before_action :authenticate_user!
18
+ before_action :fetch_common_nav
19
+
20
+ rescue_from Exceptions::RefreshTokenExpiredError do
21
+ current_user&.invalidate_all_sessions!
22
+ redirect_to \"#{ENV['IDENTITY_BASE_URL']}/connect/endsession\", headers: {
23
+ 'Content-Type' => 'application/json'
24
+ }, allow_other_host: true
25
+ end
26
+
17
27
  # Implement the list of menu items for the application
18
28
  # def menu_items
19
29
  # [
20
30
  # { name: 'Home', icon: 'fa-solid fa-house', path_method: :root_path }
21
31
  # ]
22
- # end\n\n"
32
+ # end
33
+
34
+ "
23
35
 
24
36
  end
25
37
  end
@@ -38,7 +50,7 @@ devise_scope :user do
38
50
  end
39
51
 
40
52
  def uid_migration
41
- migration_template "add_uid_to_user.rb", "db/migrate/add_uid_to_user.rb"
53
+ migration_template "add_uid_and_session_token_to_user.rb", "db/migrate/add_uid_and_session_token_to_user.rb"
42
54
  end
43
55
 
44
56
  def self.next_migration_number(path)
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class AddUidToUser < ActiveRecord::Migration[7.0]
3
+ class AddUidAndSessionTokenToUser < ActiveRecord::Migration[7.0]
4
4
  def change
5
5
  add_column :users, :uid, :string
6
+ add_column :users, :session_token, :string
6
7
  add_index :users, :uid, unique: true
7
8
  end
8
9
  end
@@ -1,5 +1,5 @@
1
1
  module Strongmind
2
2
  module Auth
3
- VERSION = "1.0.10"
3
+ VERSION = "1.0.12"
4
4
  end
5
5
  end
@@ -52,7 +52,10 @@ module Strongmind
52
52
 
53
53
  def token
54
54
  cache_data = Rails.cache.fetch(user.uid)
55
- raise TokenNotFoundError, "Token not found for user #{user.id}" unless cache_data&.key?(:access_token)
55
+ cache_missing_message = " - check your caching settings (switch to file or redis)" if Rails.env.development?
56
+ unless cache_data&.key?(:access_token)
57
+ raise TokenNotFoundError, "Token not found for user #{user.uid}#{cache_missing_message}"
58
+ end
56
59
 
57
60
  cache_data[:access_token]
58
61
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strongmind-auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.10
4
+ version: 1.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Team Belding
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-15 00:00:00.000000000 Z
11
+ date: 2024-03-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -105,12 +105,14 @@ files:
105
105
  - Rakefile
106
106
  - app/assets/config/strongmind_auth_manifest.js
107
107
  - app/assets/stylesheets/strongmind/auth/application.css
108
+ - app/controllers/concerns/jwt_utilities.rb
108
109
  - app/controllers/concerns/strong_mind_nav.rb
109
110
  - app/controllers/logins_controller.rb
110
111
  - app/controllers/users/omniauth_callbacks_controller.rb
112
+ - app/controllers/users/sessions_controller.rb
111
113
  - app/helpers/strongmind/auth/application_helper.rb
112
- - app/jobs/rails/auth/application_job.rb
113
- - app/mailers/rails/auth/application_mailer.rb
114
+ - app/jobs/strongmind/auth/application_job.rb
115
+ - app/mailers/strongmind/auth/application_mailer.rb
114
116
  - app/models/user_base.rb
115
117
  - app/views/layouts/_loading_navbar.html.erb
116
118
  - app/views/logins/index.html.erb
@@ -119,7 +121,7 @@ files:
119
121
  - config/routes.rb
120
122
  - lib/generators/strongmind/USAGE
121
123
  - lib/generators/strongmind/install_generator.rb
122
- - lib/generators/strongmind/templates/add_uid_to_user.rb
124
+ - lib/generators/strongmind/templates/add_uid_and_session_token_to_user.rb
123
125
  - lib/generators/strongmind/templates/env
124
126
  - lib/generators/strongmind/templates/user.rb
125
127
  - lib/strongmind/auth.rb