strongmind-auth 1.0.10 → 1.0.12

Sign up to get free protection for your applications and to get access to all the features.
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