forest_liana 5.3.0 → 6.0.0.pre.beta.2

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/forest_liana/actions_controller.rb +0 -82
  3. data/app/controllers/forest_liana/application_controller.rb +1 -7
  4. data/app/controllers/forest_liana/authentication_controller.rb +122 -0
  5. data/app/controllers/forest_liana/base_controller.rb +4 -0
  6. data/app/controllers/forest_liana/router.rb +2 -2
  7. data/app/controllers/forest_liana/sessions_controller.rb +1 -1
  8. data/app/controllers/forest_liana/stats_controller.rb +5 -5
  9. data/app/helpers/forest_liana/adapter_helper.rb +1 -1
  10. data/app/models/forest_liana/model/action.rb +1 -2
  11. data/app/serializers/forest_liana/schema_serializer.rb +2 -2
  12. data/app/services/forest_liana/apimap_sorter.rb +1 -2
  13. data/app/services/forest_liana/authentication.rb +59 -0
  14. data/app/services/forest_liana/authorization_getter.rb +12 -20
  15. data/app/services/forest_liana/forest_api_requester.rb +14 -5
  16. data/app/services/forest_liana/ip_whitelist_checker.rb +1 -1
  17. data/app/services/forest_liana/login_handler.rb +3 -11
  18. data/app/services/forest_liana/oidc_client_manager.rb +34 -0
  19. data/app/services/forest_liana/oidc_configuration_retriever.rb +12 -0
  20. data/app/services/forest_liana/oidc_dynamic_client_registrator.rb +67 -0
  21. data/app/services/forest_liana/permissions_checker.rb +1 -1
  22. data/app/services/forest_liana/query_stat_getter.rb +5 -5
  23. data/app/services/forest_liana/resources_getter.rb +3 -3
  24. data/app/services/forest_liana/token.rb +27 -0
  25. data/config/initializers/error-messages.rb +20 -0
  26. data/config/routes.rb +5 -2
  27. data/lib/forest_liana.rb +1 -0
  28. data/lib/forest_liana/bootstrapper.rb +1 -20
  29. data/lib/forest_liana/collection.rb +2 -2
  30. data/lib/forest_liana/engine.rb +9 -0
  31. data/lib/forest_liana/json_printer.rb +1 -1
  32. data/lib/forest_liana/schema_file_updater.rb +0 -1
  33. data/lib/forest_liana/version.rb +1 -1
  34. data/spec/dummy/config/initializers/forest_liana.rb +1 -0
  35. data/spec/requests/authentications_spec.rb +107 -0
  36. data/spec/requests/sessions_spec.rb +55 -0
  37. data/spec/services/forest_liana/apimap_sorter_spec.rb +4 -6
  38. metadata +57 -9
  39. data/app/helpers/forest_liana/is_same_data_structure_helper.rb +0 -44
  40. data/spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb +0 -87
  41. data/spec/requests/actions_controller_spec.rb +0 -136
@@ -5,33 +5,42 @@ module ForestLiana
5
5
  def self.get(route, query: nil, headers: {})
6
6
  begin
7
7
  HTTParty.get("#{forest_api_url}#{route}", {
8
+ :verify => Rails.env.production?,
8
9
  headers: base_headers.merge(headers),
9
10
  query: query,
10
11
  }).response
11
12
  rescue
12
- raise 'Cannot reach Forest API, it seems to be down right now.'
13
+ raise "Cannot reach Forest API at #{forest_api_url}#{route}, it seems to be down right now."
13
14
  end
14
15
  end
15
16
 
16
17
  def self.post(route, body: nil, query: nil, headers: {})
17
18
  begin
18
- HTTParty.post("#{forest_api_url}#{route}", {
19
+ if route.start_with?('https://')
20
+ post_route = route
21
+ else
22
+ post_route = "#{forest_api_url}#{route}"
23
+ end
24
+
25
+ HTTParty.post(post_route, {
26
+ :verify => Rails.env.production?,
19
27
  headers: base_headers.merge(headers),
20
28
  query: query,
21
29
  body: body.to_json,
22
30
  }).response
23
31
  rescue
24
- raise 'Cannot reach Forest API, it seems to be down right now.'
32
+ raise "Cannot reach Forest API at #{post_route}, it seems to be down right now."
25
33
  end
26
34
  end
27
35
 
28
36
  private
29
37
 
30
38
  def self.base_headers
31
- {
39
+ base_headers = {
32
40
  'Content-Type' => 'application/json',
33
- 'forest-secret-key' => ForestLiana.env_secret,
34
41
  }
42
+ base_headers['forest-secret-key'] = ForestLiana.env_secret if !ForestLiana.env_secret.nil?
43
+ return base_headers
35
44
  end
36
45
 
37
46
  def self.forest_api_url
@@ -58,7 +58,7 @@ module ForestLiana
58
58
  ip_range_maximum = (IPAddress rule['ip_maximum']).to_i
59
59
  ip_value = (IPAddress ip).to_i
60
60
 
61
- return ip_value >= ip_range_minimum && ip_value <= ip_range_maximum;
61
+ return ip_value >= ip_range_minimum && ip_value <= ip_range_maximum
62
62
  end
63
63
 
64
64
  def self.is_ip_match_subnet(ip, subnet)
@@ -19,12 +19,12 @@ module ForestLiana
19
19
  end
20
20
 
21
21
  def perform
22
- user = ForestLiana::AuthorizationGetter.new(
22
+ user = ForestLiana::AuthorizationGetter.authenticate(
23
23
  @rendering_id,
24
24
  @use_google_authentication,
25
25
  @auth_data,
26
26
  @two_factor_registration
27
- ).perform
27
+ )
28
28
 
29
29
  if user['two_factor_authentication_enabled']
30
30
  if !@two_factor_token.nil?
@@ -93,15 +93,7 @@ module ForestLiana
93
93
  end
94
94
 
95
95
  def create_token(user, rendering_id)
96
- JWT.encode({
97
- id: user['id'],
98
- email: user['email'],
99
- first_name: user['first_name'],
100
- last_name: user['last_name'],
101
- team: user['teams'][0],
102
- rendering_id: rendering_id,
103
- exp: Time.now.to_i + 2.weeks.to_i
104
- }, ForestLiana.auth_secret, 'HS256')
96
+ ForestLiana::Token.create_token(user, rendering_id)
105
97
  end
106
98
  end
107
99
  end
@@ -0,0 +1,34 @@
1
+ require 'openid_connect'
2
+
3
+ module ForestLiana
4
+ class OidcClientManager
5
+ def self.get_client_for_callback_url(callback_url)
6
+ begin
7
+ client_data = Rails.cache.read(callback_url) || nil
8
+ if client_data.nil?
9
+ configuration = ForestLiana::OidcConfigurationRetriever.retrieve()
10
+
11
+ client_credentials = ForestLiana::OidcDynamicClientRegistrator.register({
12
+ token_endpoint_auth_method: 'none',
13
+ redirect_uris: [callback_url],
14
+ registration_endpoint: configuration['registration_endpoint']
15
+ })
16
+
17
+ client_data = { :client_id => client_credentials['client_id'], :issuer => configuration['issuer'] }
18
+ Rails.cache.write(callback_url, client_data)
19
+ end
20
+
21
+ OpenIDConnect::Client.new(
22
+ identifier: client_data[:client_id],
23
+ redirect_uri: callback_url,
24
+ host: "#{client_data[:issuer].sub(/^https?\:\/\/(www.)?/,'')}",
25
+ authorization_endpoint: '/oidc/auth',
26
+ token_endpoint: '/oidc/token',
27
+ )
28
+ rescue => error
29
+ Rails.cache.delete(callback_url)
30
+ raise error
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ module ForestLiana
2
+ class OidcConfigurationRetriever
3
+ def self.retrieve()
4
+ response = ForestLiana::ForestApiRequester.get('/oidc/.well-known/openid-configuration')
5
+ if response.is_a?(Net::HTTPOK)
6
+ return JSON.parse(response.body)
7
+ else
8
+ raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:OIDC_CONFIGURATION_RETRIEVAL_FAILED]
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,67 @@
1
+ require 'json'
2
+ require 'json/jwt'
3
+
4
+ module ForestLiana
5
+ class OidcDynamicClientRegistrator
6
+ def self.is_standard_body_error(response)
7
+ result = false
8
+ begin
9
+ jsonbody
10
+
11
+ if (!response['body'].is_a?(Object) || response['body'].is_a?(StringIO))
12
+ jsonbody = JSON.parse(response['body'])
13
+ else
14
+ jsonbody = response['body']
15
+ end
16
+
17
+ result = jsonbody['error'].is_a?(String) && jsonbody['error'].length > 0
18
+
19
+ if (result)
20
+ response['body'] = jsonbody
21
+ end
22
+ rescue
23
+ {}
24
+ end
25
+
26
+ return result
27
+ end
28
+
29
+ def self.process_response(response, expected = {})
30
+ statusCode = expected[:statusCode] || 200
31
+ body = expected[:body] || true
32
+
33
+ if (response.code.to_i != statusCode.to_i)
34
+ if (is_standard_body_error(response))
35
+ raise response['body']
36
+ end
37
+
38
+ raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:REGISTRATION_FAILED] + response.body
39
+ end
40
+
41
+ if (body && !response.body)
42
+ raise ForestLiana::MESSAGES[:SERVER_TRANSACTION][:REGISTRATION_FAILED] + response.body
43
+ end
44
+
45
+ return response.body
46
+ end
47
+
48
+ def self.authorization_header_value(token, tokenType = 'Bearer')
49
+ return "#{tokenType} #{token}"
50
+ end
51
+
52
+ def self.register(metadata)
53
+ initial_access_token = ForestLiana.env_secret
54
+
55
+ response = ForestLiana::ForestApiRequester.post(
56
+ metadata[:registration_endpoint],
57
+ body: metadata,
58
+ headers: initial_access_token ? {
59
+ Authorization: authorization_header_value(initial_access_token),
60
+ } : {},
61
+ )
62
+
63
+ responseBody = process_response(response, { :statusCode => 201, :bearer => true })
64
+ return JSON.parse(responseBody)
65
+ end
66
+ end
67
+ end
@@ -44,7 +44,7 @@ module ForestLiana
44
44
  @allowed = @smart_action_permissions['allowed']
45
45
  @users = @smart_action_permissions['users']
46
46
 
47
- return @allowed && (@users.nil?|| @users.include?(@user_id.to_i));
47
+ return @allowed && (@users.nil?|| @users.include?(@user_id.to_i))
48
48
  end
49
49
 
50
50
  def collection_list_allowed?(scope_permissions)
@@ -2,11 +2,11 @@ module ForestLiana
2
2
  class QueryStatGetter
3
3
  attr_accessor :record
4
4
 
5
- CHART_TYPE_VALUE = 'Value';
6
- CHART_TYPE_PIE = 'Pie';
7
- CHART_TYPE_LINE = 'Line';
8
- CHART_TYPE_LEADERBOARD = 'Leaderboard';
9
- CHART_TYPE_OBJECTIVE = 'Objective';
5
+ CHART_TYPE_VALUE = 'Value'
6
+ CHART_TYPE_PIE = 'Pie'
7
+ CHART_TYPE_LINE = 'Line'
8
+ CHART_TYPE_LEADERBOARD = 'Leaderboard'
9
+ CHART_TYPE_OBJECTIVE = 'Objective'
10
10
 
11
11
  def initialize(params)
12
12
  @params = params
@@ -12,11 +12,11 @@ module ForestLiana
12
12
  @collection = get_collection(@collection_name)
13
13
  @fields_to_serialize = get_fields_to_serialize
14
14
  @field_names_requested = field_names_requested
15
- get_segment
16
- compute_includes
15
+ get_segment()
16
+ compute_includes()
17
17
  @search_query_builder = SearchQueryBuilder.new(@params, @includes, @collection)
18
18
 
19
- prepare_query
19
+ prepare_query()
20
20
  end
21
21
 
22
22
  def self.get_ids_from_request(params)
@@ -0,0 +1,27 @@
1
+ EXPIRATION_IN_SECONDS = 14.days
2
+
3
+ module ForestLiana
4
+ class Token
5
+ REGEX_COOKIE_SESSION_TOKEN = /forest_session_token=([^;]*)/;
6
+
7
+ def self.expiration_in_days
8
+ Time.current + EXPIRATION_IN_SECONDS
9
+ end
10
+
11
+ def self.expiration_in_seconds
12
+ return Time.now.to_i + EXPIRATION_IN_SECONDS
13
+ end
14
+
15
+ def self.create_token(user, rendering_id)
16
+ return JWT.encode({
17
+ id: user['id'],
18
+ email: user['email'],
19
+ first_name: user['first_name'],
20
+ last_name: user['last_name'],
21
+ team: user['teams'][0],
22
+ rendering_id: rendering_id,
23
+ exp: expiration_in_seconds()
24
+ }, ForestLiana.auth_secret, 'HS256')
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ module ForestLiana
2
+ MESSAGES = {
3
+ CONFIGURATION: {
4
+ AUTH_SECRET_MISSING: "Your Forest authSecret seems to be missing. Can you check that you properly set a Forest authSecret in the Forest initializer?",
5
+ },
6
+ SERVER_TRANSACTION: {
7
+ SECRET_AND_RENDERINGID_INCONSISTENT: "Cannot retrieve the project you're trying to unlock. The envSecret and renderingId seems to be missing or inconsistent.",
8
+ SERVER_DOWN: "Cannot retrieve the data from the Forest server. Forest API seems to be down right now.",
9
+ SECRET_NOT_FOUND: "Cannot retrieve the data from the Forest server. Can you check that you properly copied the Forest envSecret in the Liana initializer?",
10
+ UNEXPECTED: "Cannot retrieve the data from the Forest server. An error occured in Forest API.",
11
+ INVALID_STATE_MISSING: "Invalid response from the authentication server: the state parameter is missing",
12
+ INVALID_STATE_FORMAT: "Invalid response from the authentication server: the state parameter is not at the right format",
13
+ INVALID_STATE_RENDERING_ID: "Invalid response from the authentication server: the state does not contain a renderingId",
14
+ MISSING_RENDERING_ID: "Authentication request must contain a renderingId",
15
+ INVALID_RENDERING_ID: "The parameter renderingId is not valid",
16
+ REGISTRATION_FAILED: "The registration to the authentication API failed, response: ",
17
+ OIDC_CONFIGURATION_RETRIEVAL_FAILED: "Failed to retrieve the provider's configuration.",
18
+ }
19
+ }
20
+ end
@@ -4,6 +4,11 @@ ForestLiana::Engine.routes.draw do
4
4
  # Onboarding
5
5
  get '/' => 'apimaps#index'
6
6
 
7
+ # Authentication
8
+ post 'authentication' => 'authentication#start_authentication'
9
+ get 'authentication/callback' => 'authentication#authentication_callback'
10
+ post 'authentication/logout' => 'authentication#logout'
11
+
7
12
  # Session
8
13
  post 'sessions' => 'sessions#create_with_password'
9
14
  post 'sessions-google' => 'sessions#create_with_google'
@@ -57,6 +62,4 @@ ForestLiana::Engine.routes.draw do
57
62
 
58
63
  # Smart Actions forms value
59
64
  post 'actions/:action_name/values' => 'actions#values'
60
- post 'actions/:action_name/hooks/load' => 'actions#load'
61
- post 'actions/:action_name/hooks/change' => 'actions#change'
62
65
  end
@@ -16,6 +16,7 @@ module ForestLiana
16
16
 
17
17
  mattr_accessor :env_secret
18
18
  mattr_accessor :auth_secret
19
+ mattr_accessor :application_url
19
20
  mattr_accessor :integrations
20
21
  mattr_accessor :apimap
21
22
  mattr_accessor :allowed_users
@@ -38,14 +38,6 @@ module ForestLiana
38
38
 
39
39
  private
40
40
 
41
- def get_collection(collection_name)
42
- ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
43
- end
44
-
45
- def get_action(collection, action_name)
46
- collection.actions.find {|action| action.name == action_name}
47
- end
48
-
49
41
  def generate_apimap
50
42
  create_apimap
51
43
  require_lib_forest_liana
@@ -53,17 +45,6 @@ module ForestLiana
53
45
 
54
46
  if Rails.env.development?
55
47
  @collections_sent = ForestLiana.apimap.as_json
56
-
57
- @collections_sent.each do |collection|
58
- collection['actions'].each do |action|
59
- c = get_collection(collection['name'])
60
- a = get_action(c, action['name'])
61
- load = !a.hooks.nil? && a.hooks.key?(:load) && a.hooks[:load].is_a?(Proc)
62
- change = !a.hooks.nil? && a.hooks.key?(:change) && a.hooks[:change].is_a?(Hash) ? a.hooks[:change].keys : []
63
- action['hooks'] = {:load => load, :change => change}
64
- end
65
- end
66
-
67
48
  @meta_sent = ForestLiana.meta
68
49
  SchemaFileUpdater.new(SCHEMA_FILENAME, @collections_sent, @meta_sent).perform()
69
50
  else
@@ -640,7 +621,7 @@ module ForestLiana
640
621
  end
641
622
 
642
623
  def forest_url
643
- ENV['FOREST_URL'] || 'https://api.forestadmin.com';
624
+ ENV['FOREST_URL'] || 'https://api.forestadmin.com'
644
625
  end
645
626
 
646
627
  def database_type
@@ -148,7 +148,7 @@ module ForestLiana::Collection
148
148
  # TODO: Remove once lianas prior to 2.0.0 are not supported anymore.
149
149
  model = ForestLiana.models.find { |collection| collection.try(:table_name) == collection_name.to_s }
150
150
  if model
151
- collection_name_new = ForestLiana.name_for(model);
151
+ collection_name_new = ForestLiana.name_for(model)
152
152
  FOREST_LOGGER.warn "DEPRECATION WARNING: Collection names are now based on the models " \
153
153
  "names. Please rename the collection '#{collection_name.to_s}' of your Forest " \
154
154
  "customisation in '#{collection_name_new}'."
@@ -158,7 +158,7 @@ module ForestLiana::Collection
158
158
  # TODO: Remove once lianas prior to 2.0.0 are not supported anymore.
159
159
  model = ForestLiana.names_old_overriden.invert[collection_name.to_s]
160
160
  if model
161
- collection_name_new = ForestLiana.name_for(model);
161
+ collection_name_new = ForestLiana.name_for(model)
162
162
  FOREST_LOGGER.warn "DEPRECATION WARNING: Collection names are now based on the models " \
163
163
  "names. Please rename the collection '#{collection_name.to_s}' of your Forest " \
164
164
  "customisation in '#{collection_name_new}'."
@@ -16,8 +16,17 @@ module ForestLiana
16
16
  begin
17
17
  rack_cors_class = Rack::Cors
18
18
  rack_cors_class = 'Rack::Cors' if Rails::VERSION::MAJOR < 5
19
+ null_regex = Regexp.new(/\Anull\z/)
19
20
 
20
21
  config.middleware.insert_before 0, rack_cors_class do
22
+ allow do
23
+ hostnames = [null_regex, 'localhost:4200', /\A.*\.forestadmin\.com\z/]
24
+ hostnames += ENV['CORS_ORIGINS'].split(',') if ENV['CORS_ORIGINS']
25
+
26
+ origins hostnames
27
+ resource ForestLiana::AuthenticationController::PUBLIC_ROUTES[1], headers: :any, methods: :any, credentials: true, max_age: 86400 # NOTICE: 1 day
28
+ end
29
+
21
30
  allow do
22
31
  hostnames = ['localhost:4200', /\A.*\.forestadmin\.com\z/]
23
32
  hostnames += ENV['CORS_ORIGINS'].split(',') if ENV['CORS_ORIGINS']
@@ -20,7 +20,7 @@ module ForestLiana
20
20
  result << ", "
21
21
  end
22
22
 
23
- result << pretty_print(item, is_primary_value ? "#{indentation} " : indentation);
23
+ result << pretty_print(item, is_primary_value ? "#{indentation} " : indentation)
24
24
  end
25
25
 
26
26
  result << "\n#{indentation}" if is_primary_value && !is_small
@@ -50,7 +50,6 @@ module ForestLiana
50
50
  'redirect',
51
51
  'download',
52
52
  'fields',
53
- 'hooks',
54
53
  ]
55
54
  KEYS_ACTION_FIELD = [
56
55
  'field',
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "5.3.0"
2
+ VERSION = "6.0.0-beta.2"
3
3
  end
@@ -1,2 +1,3 @@
1
1
  ForestLiana.env_secret = 'env_secret_test'
2
2
  ForestLiana.auth_secret = 'auth_secret_test'
3
+ ForestLiana.application_url = 'http://localhost:3000'
@@ -0,0 +1,107 @@
1
+ require 'rails_helper'
2
+ require 'openid_connect'
3
+ require 'json'
4
+
5
+ describe "Authentications", type: :request do
6
+ before do
7
+ allow(ForestLiana::IpWhitelist).to receive(:retrieve) { true }
8
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
9
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
10
+ allow(ForestLiana::OidcConfigurationRetriever).to receive(:retrieve) {
11
+ JSON.parse('{
12
+ "registration_endpoint": "https://api.forestadmin.com/oidc/registration",
13
+ "issuer": "api.forestadmin.com"
14
+ }', :symbolize_names => false)
15
+ }
16
+ allow(ForestLiana::ForestApiRequester).to receive(:post) {
17
+ instance_double(HTTParty::Response, body: '{ "client_id": "random_id" }', code: 201)
18
+ }
19
+ allow_any_instance_of(OpenIDConnect::Client).to receive(:access_token!) {
20
+ OpenIDConnect::AccessToken.new(access_token: 'THE-ACCESS-TOKEN', client: instance_double(OpenIDConnect::Client))
21
+ }
22
+ end
23
+
24
+ after do
25
+ Rails.cache.delete(URI.join(ForestLiana.application_url, ForestLiana::Engine.routes.url_helpers.authentication_callback_path).to_s)
26
+ end
27
+
28
+ headers = {
29
+ 'Accept' => 'application/json',
30
+ 'Content-Type' => 'application/json',
31
+ }
32
+
33
+ describe "POST /authentication" do
34
+ before() do
35
+ post ForestLiana::Engine.routes.url_helpers.authentication_path, { :renderingId => 42 }, :headers => headers
36
+ end
37
+
38
+ it "should respond with a 302 code" do
39
+ expect(response).to have_http_status(302)
40
+ end
41
+
42
+ it "should return a valid authentication url" do
43
+ expect(response.headers['Location']).to eq('https://api.forestadmin.com/oidc/auth?client_id=random_id&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fforest%2Fauthentication%2Fcallback&response_type=code&scope=openid%20email%20profile&state=%7B%22renderingId%22%3D%3E42%7D')
44
+ end
45
+ end
46
+
47
+ describe "GET /authentication/callback" do
48
+ before() do
49
+ response = '{"data":{"id":666,"attributes":{"first_name":"Alice","last_name":"Doe","email":"alice@forestadmin.com","teams":[1,2,3]}}}'
50
+ allow(ForestLiana::ForestApiRequester).to receive(:get).with(
51
+ "/liana/v2/renderings/42/authorization", { :headers => { "forest-token" => "THE-ACCESS-TOKEN" }, :query=> {} }
52
+ ).and_return(
53
+ instance_double(HTTParty::Response, :body => response, :code => 200)
54
+ )
55
+
56
+ get ForestLiana::Engine.routes.url_helpers.authentication_callback_path + "?code=THE-CODE&state=#{URI.escape('{"renderingId":42}', Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}"
57
+ end
58
+
59
+ it "should respond with a 200 code" do
60
+ expect(response).to have_http_status(200)
61
+ end
62
+
63
+ it "should return a valid authentication token" do
64
+ session_cookie = response.headers['set-cookie']
65
+ expect(session_cookie).to match(/^forest_session_token=[^;]+; path=\/; expires=[^;]+; secure; HttpOnly$/)
66
+
67
+ token = session_cookie.match(/^forest_session_token=([^;]+);/)[1]
68
+ decoded = JWT.decode(token, ForestLiana.auth_secret, true, { algorithm: 'HS256' })[0]
69
+
70
+ expected_token_data = {
71
+ "id" => 666,
72
+ "email" => 'alice@forestadmin.com',
73
+ "rendering_id" => "42",
74
+ "first_name" => 'Alice',
75
+ "last_name" => 'Doe',
76
+ "team" => 1,
77
+ }
78
+
79
+ expect(decoded).to include(expected_token_data)
80
+ expect(JSON.parse(response.body, :symbolize_names => true)).to eq({ token: token, tokenData: decoded.deep_symbolize_keys! })
81
+ expect(response).to have_http_status(200)
82
+ end
83
+ end
84
+
85
+ describe "POST /authentication/logout" do
86
+ before() do
87
+ cookies['forest_session_token'] = {
88
+ value: 'eyJhbGciOiJIUzI1NiJ9.eyJpZCI6NjY2LCJlbWFpbCI6ImFsaWNlQGZvcmVzdGFkbWluLmNvbSIsImZpcnN0X25hbWUiOiJBbGljZSIsImxhc3RfbmFtZSI6IkRvZSIsInRlYW0iOjEsInJlbmRlcmluZ19pZCI6IjQyIiwiZXhwIjoxNjA4MDQ5MTI2fQ.5xaMxjUjE3wKldBsj3wW0BP9GHnnMqQi2Kpde8cIHEw',
89
+ path: '/',
90
+ expires: Time.now.to_i + 14.days,
91
+ secure: true,
92
+ httponly: true
93
+ }
94
+ post ForestLiana::Engine.routes.url_helpers.authentication_logout_path, { :renderingId => 42 }, :headers => headers
95
+ cookies.delete('forest_session_token')
96
+ end
97
+
98
+ it "should respond with a 204 code" do
99
+ expect(response).to have_http_status(204)
100
+ end
101
+
102
+ it "should invalidate token from browser" do
103
+ invalidated_session_cookie = response.headers['set-cookie']
104
+ expect(invalidated_session_cookie).to match(/^forest_session_token=[^;]+; path=\/; expires=Thu, 01 Jan 1970 00:00:00 -0000; secure; HttpOnly$/)
105
+ end
106
+ end
107
+ end