strongmind-auth 1.0.11 → 1.0.13

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: 5744571fb4c34f46eab563a74d0f09ff0da1a5d88be521d785d42bf11537b354
4
- data.tar.gz: '036694d87a468271a854342b34823357649def9b7ee12fbccbaa9e1384c90a70'
3
+ metadata.gz: 5f8024e8a6ede6f16c3ab5a900c19e0aa736a703ac36d6668287b2a27ec61928
4
+ data.tar.gz: 7d63c841f85f811124902d077989ea5bc683923258048164f10d85f79ca41b06
5
5
  SHA512:
6
- metadata.gz: 56e34ffb177bf0949cf0d8e670dd407e8c554ad88138432054b9bd1439ca989b6d5a020a461c9dc9204b5ead7dd81ef852e89114b7ea2d534dca7c44e6e8d610
7
- data.tar.gz: 144c638476dabe423587f743b175a49a0e3a325ac64db51af008aab7ef3282e37e0471e99ff7c9698721bd74852e50d246cc7bf75e5fa6f4e5d4a5fc111dbc28
6
+ metadata.gz: 836af65fff974d2ec9b16166a72e0203f41eb7a3b59cee6054bd32b53fb7e1980b6e9f4295586731a54ab20c20d5205ba4a91687cdfe68c01d269d979d74bbd3
7
+ data.tar.gz: d4195a3e2c67eeb856e6374d0d9386af87001fa730708ba578b3f3a92ee7a15b26fd3965df6597f91680ce660744d1642f2cb1b931937e3bdc609229fe905e93
@@ -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 Strongmind::Exceptions::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
@@ -1,4 +1,5 @@
1
1
  require "strongmind/common_nav_fetcher"
2
+ require "strongmind/exceptions"
2
3
 
3
4
  module StrongMindNav
4
5
  extend ActiveSupport::Concern
@@ -10,7 +11,7 @@ module StrongMindNav
10
11
  @top_navbar_html = navbar[:top_navbar_html]
11
12
  @bottom_navbar_html = navbar[:bottom_navbar_html]
12
13
  @theme_css = navbar[:theme_css]
13
- rescue Strongmind::CommonNavFetcher::TokenNotFoundError, Strongmind::CommonNavFetcher::UserNotFoundError => e
14
+ rescue Strongmind::Exceptions::TokenNotFoundError, Strongmind::Exceptions::UserNotFoundError => e
14
15
  Sentry.capture_exception(e)
15
16
  Rails.logger.error(e)
16
17
  flash[:alert] = e.inspect if Rails.env.development?
@@ -18,6 +19,7 @@ module StrongMindNav
18
19
  render 'logins/index'
19
20
  rescue Exception => e
20
21
  Sentry.capture_exception(e)
22
+ Rails.logger.error(e)
21
23
  @top_navbar_html = render_to_string(partial: 'layouts/loading_navbar').html_safe
22
24
  end
23
25
  end
@@ -10,6 +10,8 @@ module Users
10
10
  User.auth_token_cache = auth
11
11
  @user = User.with_credentials(auth)
12
12
 
13
+ render plain: "You do not have permission to access this application.", status: :unauthorized and return if @user.nil?
14
+
13
15
  session[:refresh_token] = request.env['omniauth.auth'].credentials['refresh_token']
14
16
  flash.delete(:notice)
15
17
 
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Users
4
+
4
5
  class SessionsController < Devise::SessionsController
6
+ include JwtUtilities
7
+
5
8
  skip_before_action :fetch_common_nav
9
+ skip_before_action :verify_authenticity_token, only: :endsession
6
10
 
7
11
  def login
8
12
  redirect_to user_strongmind_omniauth_authorize_url
@@ -12,5 +16,31 @@ module Users
12
16
  redirect_to user_strongmind_omniauth_authorize_url
13
17
  end
14
18
 
19
+ def endsession
20
+ headers = { 'Cache-Control' => 'no-store' }
21
+ if jwt_valid?(params[:logout_token], 'http://schemas.openid.net/event/backchannel-logout')
22
+ payload, _header = JWT.decode(params[:logout_token], nil, false)
23
+ user_identity = payload['sub']
24
+ user = User.find_by(uid: user_identity)
25
+ user.invalidate_all_sessions!
26
+ render json: {}, status: :ok, headers:
27
+ else
28
+ render json: {}, status: :bad_request, headers:
29
+ end
30
+ end
31
+
32
+ def initiate_backchannel_logout
33
+ user_token_info = fetch_user_token_info
34
+
35
+ id_token_hint = user_token_info[:id_token]
36
+ token = user_token_info[:access_token]
37
+ current_user&.invalidate_all_sessions!
38
+ identity_base_url = ENV['IDENTITY_BASE_URL']
39
+ redirect_to "#{identity_base_url}/connect/endsession?id_token_hint=#{id_token_hint}", headers: {
40
+ 'Content-Type' => 'application/json',
41
+ 'Authorization' => "Bearer #{token}"
42
+ }, allow_other_host: true
43
+ end
44
+
15
45
  end
16
46
  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
data/config/routes.rb CHANGED
@@ -8,7 +8,7 @@ Rails.application.routes.draw do
8
8
  }
9
9
 
10
10
  devise_scope :user do
11
- post 'users/sign_out', to: 'devise/sessions#destroy'
11
+ get 'users/sign_out', to: 'users/sessions#initiate_backchannel_logout'
12
12
 
13
13
  unauthenticated do
14
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.11"
3
+ VERSION = "1.0.13"
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
-
2
+ require "strongmind/exceptions"
3
3
  require 'platform_sdk'
4
4
 
5
5
  module Strongmind
@@ -9,12 +9,8 @@ module Strongmind
9
9
 
10
10
  include Rails.application.routes.url_helpers
11
11
 
12
- class TokenNotFoundError < StandardError; end
13
-
14
- class UserNotFoundError < StandardError; end
15
-
16
12
  def initialize(user, request)
17
- raise UserNotFoundError, 'User not found' unless user.present?
13
+ raise Strongmind::Exceptions::UserNotFoundError, 'User not found' unless user.present?
18
14
  raise ArgumentError, 'Request not found' unless request.present?
19
15
 
20
16
  @user = user
@@ -54,7 +50,7 @@ module Strongmind
54
50
  cache_data = Rails.cache.fetch(user.uid)
55
51
  cache_missing_message = " - check your caching settings (switch to file or redis)" if Rails.env.development?
56
52
  unless cache_data&.key?(:access_token)
57
- raise TokenNotFoundError, "Token not found for user #{user.uid}#{cache_missing_message}"
53
+ raise Strongmind::Exceptions::TokenNotFoundError, "Token not found for user #{user.uid}#{cache_missing_message}"
58
54
  end
59
55
 
60
56
  cache_data[:access_token]
@@ -83,14 +79,12 @@ module Strongmind
83
79
  end
84
80
 
85
81
  def nav_item_data(item)
86
- url = send(item[:path_method])
82
+ url = item[:url]
87
83
  {
88
84
  name: item[:name],
89
85
  icon: item[:icon],
90
- url:,
91
- is_disabled: item[:feature_flag] ? !user.feature_flag_enabled?(item[:feature_flag]) : false,
92
- is_active: current_page?(url),
93
- is_external: false
86
+ url: url,
87
+ is_active: current_page?(url)
94
88
  }
95
89
  end
96
90
  end
@@ -0,0 +1,9 @@
1
+ module Strongmind
2
+ module Exceptions
3
+ class TokenNotFoundError < StandardError; end
4
+
5
+ class UserNotFoundError < StandardError; end
6
+
7
+ class RefreshTokenExpiredError < StandardError; end
8
+ end
9
+ 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.11
4
+ version: 1.0.13
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,6 +105,7 @@ 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
@@ -120,13 +121,14 @@ files:
120
121
  - config/routes.rb
121
122
  - lib/generators/strongmind/USAGE
122
123
  - lib/generators/strongmind/install_generator.rb
123
- - lib/generators/strongmind/templates/add_uid_to_user.rb
124
+ - lib/generators/strongmind/templates/add_uid_and_session_token_to_user.rb
124
125
  - lib/generators/strongmind/templates/env
125
126
  - lib/generators/strongmind/templates/user.rb
126
127
  - lib/strongmind/auth.rb
127
128
  - lib/strongmind/auth/engine.rb
128
129
  - lib/strongmind/auth/version.rb
129
130
  - lib/strongmind/common_nav_fetcher.rb
131
+ - lib/strongmind/exceptions.rb
130
132
  - lib/tasks/rails/auth_tasks.rake
131
133
  - lib/tasks/strongmind/auth_tasks.rake
132
134
  homepage: https://www.strongmind.com