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 +4 -4
- data/app/controllers/concerns/jwt_utilities.rb +90 -0
- data/app/controllers/concerns/strong_mind_nav.rb +3 -1
- data/app/controllers/users/omniauth_callbacks_controller.rb +2 -0
- data/app/controllers/users/sessions_controller.rb +30 -0
- data/app/models/user_base.rb +10 -0
- data/config/routes.rb +1 -1
- data/lib/generators/strongmind/install_generator.rb +15 -3
- data/lib/generators/strongmind/templates/{add_uid_to_user.rb → add_uid_and_session_token_to_user.rb} +2 -1
- data/lib/strongmind/auth/version.rb +1 -1
- data/lib/strongmind/common_nav_fetcher.rb +6 -12
- data/lib/strongmind/exceptions.rb +9 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5f8024e8a6ede6f16c3ab5a900c19e0aa736a703ac36d6668287b2a27ec61928
|
4
|
+
data.tar.gz: 7d63c841f85f811124902d077989ea5bc683923258048164f10d85f79ca41b06
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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::
|
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
|
data/app/models/user_base.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
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 "
|
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)
|
data/lib/generators/strongmind/templates/{add_uid_to_user.rb → add_uid_and_session_token_to_user.rb}
RENAMED
@@ -1,8 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class
|
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
|
# 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 =
|
82
|
+
url = item[:url]
|
87
83
|
{
|
88
84
|
name: item[:name],
|
89
85
|
icon: item[:icon],
|
90
|
-
url
|
91
|
-
|
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
|
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.
|
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-
|
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/
|
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
|