Skip to content

Commit 4898b73

Browse files
authored
feat(auth): authenticate using oidc (#400)
BREAKING CHANGE: Introduces a new authentication system. - The application_url property is required to initialize ForestLiana, - CORS rules must be adapted (to allow null origins).
1 parent 3806b73 commit 4898b73

35 files changed

+653
-89
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ build/
3333
# for a library or gem, you might want to ignore these files since the code is
3434
# intended to run in multiple environments; otherwise, check them in:
3535
# Gemfile.lock
36-
# .ruby-version
3736
# .ruby-gemset
3837

3938
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
@@ -43,3 +42,7 @@ node_modules/
4342

4443
# IDE
4544
/.idea/
45+
46+
# rbenv
47+
.ruby-version
48+

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ gem 'base32', '0.3.2'
3333
gem 'rotp', '3.1'
3434
gem 'httparty', '0.13.7'
3535
gem 'ipaddress', '0.8.3'
36+
gem 'openid_connect', '1.2.0'
37+
gem 'json'
38+
gem 'json-jwt', '1.12.0'

Gemfile.lock

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ PATH
88
groupdate (= 2.5.2)
99
httparty
1010
ipaddress
11+
json
12+
json-jwt
1113
jsonapi-serializers (>= 0.14.0)
1214
jwt
15+
openid_connect
1316
rack-cors
1417
rails (>= 4.0)
1518
rotp
@@ -53,44 +56,70 @@ GEM
5356
minitest (~> 5.1)
5457
thread_safe (~> 0.3, >= 0.3.4)
5558
tzinfo (~> 1.1)
59+
aes_key_wrap (1.1.0)
5660
arel (6.0.4)
5761
arel-helpers (2.10.0)
5862
activerecord (>= 3.1.0, < 7)
63+
attr_required (1.0.1)
5964
base32 (0.3.2)
60-
bcrypt (3.1.10)
61-
builder (3.2.3)
62-
byebug (8.2.2)
63-
concurrent-ruby (1.1.5)
65+
bcrypt (3.1.16)
66+
bindata (2.4.8)
67+
builder (3.2.4)
68+
byebug (11.0.1)
69+
concurrent-ruby (1.1.7)
6470
crass (1.0.6)
65-
diff-lcs (1.3)
71+
diff-lcs (1.4.4)
6672
erubis (2.7.0)
67-
globalid (0.4.1)
73+
globalid (0.4.2)
6874
activesupport (>= 4.2.0)
6975
groupdate (2.5.2)
7076
activesupport (>= 3)
7177
httparty (0.13.7)
7278
json (~> 1.8)
7379
multi_xml (>= 0.5.2)
80+
httpclient (2.8.3)
7481
i18n (0.9.5)
7582
concurrent-ruby (~> 1.0)
7683
ipaddress (0.8.3)
7784
json (1.8.6)
85+
json-jwt (1.12.0)
86+
activesupport (>= 4.2)
87+
aes_key_wrap
88+
bindata
7889
jsonapi-serializers (1.0.1)
7990
activesupport
80-
jwt (1.5.4)
81-
loofah (2.8.0)
91+
jwt (2.2.2)
92+
loofah (2.7.0)
8293
crass (~> 1.0.2)
8394
nokogiri (>= 1.5.9)
84-
mail (2.7.0)
95+
mail (2.7.1)
8596
mini_mime (>= 0.1.1)
86-
mini_mime (1.0.0)
97+
mini_mime (1.0.2)
8798
mini_portile2 (2.4.0)
88-
minitest (5.11.3)
99+
minitest (5.14.2)
89100
multi_xml (0.6.0)
90101
nokogiri (1.10.10)
91102
mini_portile2 (~> 2.4.0)
92-
rack (1.6.10)
93-
rack-cors (0.4.0)
103+
openid_connect (1.2.0)
104+
activemodel
105+
attr_required (>= 1.0.0)
106+
json-jwt (>= 1.5.0)
107+
rack-oauth2 (>= 1.6.1)
108+
swd (>= 1.0.0)
109+
tzinfo
110+
validate_email
111+
validate_url
112+
webfinger (>= 1.0.1)
113+
public_suffix (4.0.6)
114+
rack (1.6.13)
115+
rack-cors (1.0.6)
116+
rack (>= 1.6.0)
117+
rack-oauth2 (1.12.0)
118+
activesupport
119+
attr_required
120+
httpclient
121+
json-jwt (>= 1.11.0)
122+
rack (< 2.1)
94123
rack-test (0.6.3)
95124
rack (>= 1.0)
96125
rails (4.2.7.1)
@@ -119,12 +148,12 @@ GEM
119148
thor (>= 0.18.1, < 2.0)
120149
rake (13.0.1)
121150
rotp (3.1.0)
122-
rspec-core (3.8.0)
151+
rspec-core (3.8.2)
123152
rspec-support (~> 3.8.0)
124-
rspec-expectations (3.8.2)
153+
rspec-expectations (3.8.6)
125154
diff-lcs (>= 1.2.0, < 2.0)
126155
rspec-support (~> 3.8.0)
127-
rspec-mocks (3.8.0)
156+
rspec-mocks (3.8.2)
128157
diff-lcs (>= 1.2.0, < 2.0)
129158
rspec-support (~> 3.8.0)
130159
rspec-rails (3.8.2)
@@ -135,20 +164,33 @@ GEM
135164
rspec-expectations (~> 3.8.0)
136165
rspec-mocks (~> 3.8.0)
137166
rspec-support (~> 3.8.0)
138-
rspec-support (3.8.0)
167+
rspec-support (3.8.3)
139168
sprockets (3.7.2)
140169
concurrent-ruby (~> 1.0)
141170
rack (> 1, < 3)
142-
sprockets-rails (3.2.1)
171+
sprockets-rails (3.2.2)
143172
actionpack (>= 4.0)
144173
activesupport (>= 4.0)
145174
sprockets (>= 3.0.0)
146175
sqlite3 (1.3.13)
147-
thor (0.20.0)
176+
swd (1.2.0)
177+
activesupport (>= 3)
178+
attr_required (>= 0.0.5)
179+
httpclient (>= 2.4)
180+
thor (1.0.1)
148181
thread_safe (0.3.6)
149-
tzinfo (1.2.5)
182+
tzinfo (1.2.8)
150183
thread_safe (~> 0.1)
151-
useragent (0.16.5)
184+
useragent (0.16.10)
185+
validate_email (0.1.6)
186+
activemodel (>= 3.0)
187+
mail (>= 2.2.5)
188+
validate_url (1.0.13)
189+
activemodel (>= 3.0.0)
190+
public_suffix
191+
webfinger (1.1.0)
192+
activesupport
193+
httpclient (>= 2.4)
152194

153195
PLATFORMS
154196
ruby
@@ -162,8 +204,11 @@ DEPENDENCIES
162204
groupdate (= 2.5.2)
163205
httparty (= 0.13.7)
164206
ipaddress (= 0.8.3)
207+
json
208+
json-jwt (= 1.12.0)
165209
jsonapi-serializers (= 1.0.1)
166210
jwt
211+
openid_connect (= 1.2.0)
167212
rack-cors
168213
rails (= 4.2.7.1)
169214
rake

app/controllers/forest_liana/application_controller.rb

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33

44
module ForestLiana
55
class ApplicationController < ForestLiana::BaseController
6-
REGEX_COOKIE_SESSION_TOKEN = /forest_session_token=([^;]*)/;
7-
86
def self.papertrail?
97
Object.const_get('PaperTrail::Version').is_a?(Class) rescue false
108
end
@@ -64,7 +62,7 @@ def authenticate_user_from_jwt
6462
token = request.headers['Authorization'].split.second
6563
# NOTICE: Necessary for downloads authentication.
6664
elsif request.headers['cookie']
67-
match = REGEX_COOKIE_SESSION_TOKEN.match(request.headers['cookie'])
65+
match = ForestLiana::Token::REGEX_COOKIE_SESSION_TOKEN.match(request.headers['cookie'])
6866
token = match[1] if match && match[1]
6967
end
7068

@@ -97,10 +95,6 @@ def get_smart_action_context
9795
end
9896
end
9997

100-
def route_not_found
101-
head :not_found
102-
end
103-
10498
def internal_server_error
10599
head :internal_server_error
106100
end
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
require 'uri'
2+
require 'json'
3+
4+
module ForestLiana
5+
class AuthenticationController < ForestLiana::BaseController
6+
START_AUTHENTICATION_ROUTE = 'authentication'
7+
CALLBACK_AUTHENTICATION_ROUTE = 'authentication/callback'
8+
LOGOUT_ROUTE = 'authentication/logout';
9+
PUBLIC_ROUTES = [
10+
"/#{START_AUTHENTICATION_ROUTE}",
11+
"/#{CALLBACK_AUTHENTICATION_ROUTE}",
12+
"/#{LOGOUT_ROUTE}",
13+
]
14+
15+
def initialize
16+
@authentication_service = ForestLiana::Authentication.new()
17+
end
18+
19+
def get_callback_url
20+
URI.join(ForestLiana.application_url, "/forest/#{CALLBACK_AUTHENTICATION_ROUTE}").to_s
21+
rescue => error
22+
raise "application_url is not valid or not defined" if error.is_a?(ArgumentError)
23+
end
24+
25+
def get_and_check_rendering_id
26+
if !params.has_key?('renderingId')
27+
raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:MISSING_RENDERING_ID]
28+
end
29+
30+
rendering_id = params[:renderingId]
31+
32+
if !(rendering_id.instance_of?(String) || rendering_id.instance_of?(Numeric)) || (rendering_id.instance_of?(Numeric) && rendering_id.nan?)
33+
raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:INVALID_RENDERING_ID]
34+
end
35+
36+
return rendering_id.to_i
37+
end
38+
39+
def start_authentication
40+
begin
41+
rendering_id = get_and_check_rendering_id()
42+
callback_url = get_callback_url()
43+
44+
result = @authentication_service.start_authentication(
45+
callback_url,
46+
{ 'renderingId' => rendering_id },
47+
)
48+
49+
redirect_to(result['authorization_url'])
50+
rescue => error
51+
render json: { errors: [{ status: 500, detail: error.message }] },
52+
status: :internal_server_error, serializer: nil
53+
end
54+
end
55+
56+
def authentication_callback
57+
begin
58+
callback_url = get_callback_url()
59+
60+
token = @authentication_service.verify_code_and_generate_token(
61+
callback_url,
62+
params,
63+
)
64+
65+
response.set_cookie(
66+
'forest_session_token',
67+
{
68+
value: token,
69+
httponly: true,
70+
secure: true,
71+
expires: ForestLiana::Token.expiration_in_days,
72+
samesite: 'none',
73+
path: '/'
74+
},
75+
)
76+
77+
response_body = {
78+
tokenData: JWT.decode(token, ForestLiana.auth_secret, true, { algorithm: 'HS256' })[0]
79+
}
80+
81+
# The token is sent decoded, because we don't want to share the whole, signed token
82+
# that is used to authenticate people
83+
# but the token itself contains interesting values, such as its expiration date
84+
response_body[:token] = token if !ForestLiana.application_url.start_with?('https://')
85+
86+
render json: response_body, status: 200
87+
88+
rescue => error
89+
render json: { errors: [{ status: 500, detail: error.message }] },
90+
status: :internal_server_error, serializer: nil
91+
end
92+
end
93+
94+
def logout
95+
begin
96+
if cookies.has_key?(:forest_session_token)
97+
forest_session_token = cookies[:forest_session_token]
98+
99+
if forest_session_token
100+
response.set_cookie(
101+
'forest_session_token',
102+
{
103+
value: forest_session_token,
104+
httponly: true,
105+
secure: true,
106+
expires: Time.at(0),
107+
samesite: 'none',
108+
path: '/'
109+
},
110+
)
111+
end
112+
end
113+
114+
render json: {}, status: 204
115+
rescue => error
116+
render json: { errors: [{ status: 500, detail: error.message }] },
117+
status: :internal_server_error, serializer: nil
118+
end
119+
end
120+
121+
end
122+
end

app/controllers/forest_liana/base_controller.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ class BaseController < ::ActionController::Base
44
wrap_parameters false
55
before_action :reject_unauthorized_ip
66

7+
def route_not_found
8+
head :not_found
9+
end
10+
711
private
812

913
def reject_unauthorized_ip

app/controllers/forest_liana/router.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def call(env)
77
if resource.nil?
88
FOREST_LOGGER.error "Routing error: Resource not found for collection #{collection_name}."
99
FOREST_LOGGER.error "If this is a Smart Collection, please ensure your Smart Collection routes are defined before the mounted ForestLiana::Engine?"
10-
ForestLiana::ApplicationController.action(:route_not_found).call(env)
10+
ForestLiana::BaseController.action(:route_not_found).call(env)
1111
else
1212
begin
1313
component_prefix = ForestLiana.component_prefix(resource)
@@ -40,7 +40,7 @@ def call(env)
4040
controller.action(action.to_sym).call(env)
4141
rescue NoMethodError => exception
4242
FOREST_LOGGER.error "Routing error: #{exception}\n#{exception.backtrace.join("\n\t")}"
43-
ForestLiana::ApplicationController.action(:route_not_found).call(env)
43+
ForestLiana::BaseController.action(:route_not_found).call(env)
4444
end
4545
end
4646
end

app/controllers/forest_liana/sessions_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def process_login(
8585
# NOTICE: Set a cookie to ensure secure authentication using export feature.
8686
# NOTICE: The token is empty at first authentication step if the 2FA option is active.
8787
if reponse_data[:token]
88-
response.set_cookie("forest_session_token", { value: reponse_data[:token], expires: (Time.current + 14.days) })
88+
response.set_cookie("forest_session_token", { value: reponse_data[:token], expires: (ForestLiana::Token.expiration_in_days) })
8989
end
9090

9191
render(json: reponse_data, serializer: nil)

app/controllers/forest_liana/stats_controller.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ class StatsController < ForestLiana::ApplicationController
66
before_action :find_resource, except: [:get_with_live_query]
77
end
88

9-
CHART_TYPE_VALUE = 'Value';
10-
CHART_TYPE_PIE = 'Pie';
11-
CHART_TYPE_LINE = 'Line';
12-
CHART_TYPE_LEADERBOARD = 'Leaderboard';
13-
CHART_TYPE_OBJECTIVE = 'Objective';
9+
CHART_TYPE_VALUE = 'Value'
10+
CHART_TYPE_PIE = 'Pie'
11+
CHART_TYPE_LINE = 'Line'
12+
CHART_TYPE_LEADERBOARD = 'Leaderboard'
13+
CHART_TYPE_OBJECTIVE = 'Objective'
1414

1515
def get
1616
case params[:type]

0 commit comments

Comments
 (0)