Skip to content

Commit

Permalink
Merge pull request #128 from GSA/56/shared-rails-phoenix-session
Browse files Browse the repository at this point in the history
[56] Implement shared session between Rails <=> Phoenix
  • Loading branch information
cpreisinger authored Sep 23, 2024
2 parents 583a417 + 3c990e0 commit 6cfe6b8
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 35 deletions.
6 changes: 5 additions & 1 deletion .env_login
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# local dev env vars for login.gov
export LOGIN_CLIENT_ID=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:challenge_gov_portal_eval_dev
export LOGIN_REDIRECT_EVAL_URL=http://localhost:3000/auth/result
export LOGOUT_REDIRECT_EVAL_URL=http://localhost:3000/
export LOGOUT_REDIRECT_EVAL_URL=http://localhost:3000/

export PHOENIX_URI="http://localhost:4000"
export LOGIN_SECRET="f4d3c40a00a8e6ed72fae5204d9ddacd40f087865d40803a6fcfb935591a271838533f06081067dac24c0085c74123e7e1c8b3e0ab562c6645b17eb769854d0d"
export JWT_SECRET="fc28c5738ca45162f61126e770a8fbdbd938d0fedcfe8fbb9f851b855b0264866364a9130e96aca8b1977e9f58edf064f1aa435ceccf415ff22fd3c24adba320"
2 changes: 2 additions & 0 deletions .simplecov
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ SimpleCov.start 'rails' do
add_filter '/app/mailers/application_mailer.rb'
add_filter '/app/models/application_record.rb'

add_filter '/app/controllers/sandbox_controller.rb'

merge_timeout 1800
end
65 changes: 64 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
class ApplicationController < ActionController::Base
helper_method :current_user, :logged_in?

before_action :check_session_expiration
before_action :check_session_expiration, except: [:sign_out]

def current_user
return unless session[:userinfo]
Expand All @@ -19,15 +19,21 @@ def logged_in?
def sign_in(login_userinfo)
user = User.user_from_userinfo(login_userinfo)

user_jwt = generate_user_jwt(user)
send_user_jwt_to_phoenix(user_jwt)

@current_user = user
renew_session
session[:userinfo] = login_userinfo
end

def sign_out
@current_user = nil

session.delete(:userinfo)
session.delete(:session_timeout_at)

delete_phoenix_session_cookie
end

def renew_session
Expand All @@ -50,4 +56,61 @@ def redirect_if_logged_in(path = "/dashboard")

redirect_to path, notice: I18n.t("already_logged_in_notice")
end

def generate_user_jwt(user)
payload = {
email: user.email,
sub: user.token,
exp: 24.hours.from_now.to_i
}

JWT.encode(payload, Rails.configuration.phx_interop[:jwt_secret], 'HS256')
end

# :nocov:
def send_user_jwt_to_phoenix(jwt)
res = phoenix_external_login_request(jwt)
phoenix_cookie = extract_phoenix_cookie_from_response(res)
phoenix_session_cookie(phoenix_cookie)

res.code == '200'
rescue StandardError => e
Rails.logger.error(e)
end

# rubocop:disable Metrics/AbcSize
def phoenix_external_login_request(jwt)
uri = URI("#{Rails.configuration.phx_interop[:phx_uri]}/api/external_login")

req = Net::HTTP::Post.new(uri)
req['Login-Secret'] = Rails.configuration.phx_interop[:login_secret]
req['User-JWT'] = jwt
req['Remote-IP'] = request.remote_ip

Net::HTTP.start(uri.hostname, uri.port) do |http|
http.request(req)
end
end
# rubocop:enable Metrics/AbcSize

def extract_phoenix_cookie_from_response(res)
cookie_header = res['Set-Cookie']
cookie_value = cookie_header.split(';').first.split('=').last

{ value: cookie_value }
end

def phoenix_session_cookie(phoenix_cookie)
cookies[:_challenge_gov_key] = {
value: phoenix_cookie[:value],
secure: true,
httponly: true,
same_site: :lax
}
end
# :nocov:

def delete_phoenix_session_cookie
cookies.delete(:_challenge_gov_key)
end
end
32 changes: 16 additions & 16 deletions app/helpers/dashboard_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@ module DashboardHelper
def dashboard_cards_by_role
{
challenge_manager: [
{ image_path: 'emoji_events', href: 'http://localhost:4000/', alt: 'challenges', title: 'Challenges',
subtitle: 'Create and manage challenges.' },
{ image_path: 'people', href: 'manage_submissions', alt: 'submissions', title: 'Submissions',
subtitle: 'Manage submissions, evaluations, and evaluators.' },
{ image_path: 'content_copy', href: 'evaluation_forms', alt: 'evaluation forms', title: 'Evaluation Forms',
subtitle: 'Create and manage evaluation forms.' },
{ image_path: 'map', href: 'user_guide', alt: 'resources', title: 'Resources',
subtitle: 'Learn how to make the most of the platform.' },
{ image_path: 'support_agent', href: 'federal-agency-faqs', alt: 'help', title: 'Help',
subtitle: 'Get support on the Challenge.Gov platform.' }
{ image_path: 'emoji_events', href: Rails.configuration.phx_interop[:phx_uri],
alt: 'challenges', title: 'Challenges', subtitle: 'Create and manage challenges.' },
{ image_path: 'people', href: 'manage_submissions',
alt: 'submissions', title: 'Submissions', subtitle: 'Manage submissions, evaluations, and evaluators.' },
{ image_path: 'content_copy', href: 'evaluation_forms',
alt: 'evaluation forms', title: 'Evaluation Forms', subtitle: 'Create and manage evaluation forms.' },
{ image_path: 'map', href: 'user_guide',
alt: 'resources', title: 'Resources', subtitle: 'Learn how to make the most of the platform.' },
{ image_path: 'support_agent', href: 'federal-agency-faqs',
alt: 'help', title: 'Help', subtitle: 'Get support on the Challenge.Gov platform.' }
],
evaluator: [
{ image_path: 'content_copy', href: 'manage_submissions', alt: 'submissions', title: 'Submissions',
subtitle: 'Evaluate my assigned submissions.' },
{ image_path: 'map', href: 'user_guide', alt: 'user guides', title: 'Resources',
subtitle: 'Learn how to make the most of the platform.' },
{ image_path: 'support_agent', href: 'federal-agency-faqs', alt: 'help', title: 'Help',
subtitle: 'Get support on the Challenge.Gov platform.' }
{ image_path: 'content_copy', href: 'manage_submissions',
alt: 'submissions', title: 'Submissions', subtitle: 'Evaluate my assigned submissions.' },
{ image_path: 'map', href: 'user_guide',
alt: 'user guides', title: 'Resources', subtitle: 'Learn how to make the most of the platform.' },
{ image_path: 'support_agent', href: 'federal-agency-faqs',
alt: 'help', title: 'Help', subtitle: 'Get support on the Challenge.Gov platform.' }
],
solver: []
}
Expand Down
2 changes: 1 addition & 1 deletion app/views/layouts/_utility_menu.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="bg-primary-darker w-full display-flex flex-justify-center desktop:flex-justify-end padding-1 font-body-3xs desktop:font-body-2xs text-white">
<%= utility_menu_link('grid_view', 'dashboard', 'dashboard', 'Dashboard') %>
<% if current_user.role == "challenge_manager" %>
<%= utility_menu_link('emoji_events', 'http://localhost:4000/', 'challenges', 'Challenges') %>
<%= utility_menu_link('emoji_events', Rails.configuration.phx_interop[:phx_uri], 'challenges', 'Challenges') %>
<%= utility_menu_link('content_copy', 'evaluation_forms', 'Evaluation Forms', 'Evaluations') %>
<%= utility_menu_link('folder_open', 'manage_submissions', 'Manage Submissions', 'Submissions') %>
<% end %>
Expand Down
6 changes: 6 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ class Application < Rails::Application
private_key_path: ENV.fetch("LOGIN_PRIVATE_KEY_PATH", "config/private.pem"),
}

config.phx_interop = {
phx_uri: ENV.fetch("PHOENIX_URI", nil),
login_secret: ENV.fetch("LOGIN_SECRET", "login_secret_123"),
jwt_secret: ENV.fetch("JWT_SECRET", "jwt_secret_123")
}

config.assets.initialize_on_precompile = false
end
end
17 changes: 6 additions & 11 deletions spec/requests/sessions_request_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,10 @@
end

it "get /auth/result successful" do
user = User.new(email: "[email protected]", token: SecureRandom.uuid)
code = "ABC123"
login_gov = instance_double(LoginGov)
allow(LoginGov).to receive(:new).and_return(login_gov)
allow(login_gov).to receive(:exchange_token_from_auth_result).with(code).and_return(
[{ email: "[email protected]", sub: "sub" }]
)
mock_login_gov(user, code)

get "/auth/result", params: { code: }
expect(response).to have_http_status(:redirect)
expect(response).to redirect_to("/dashboard")
Expand All @@ -47,14 +45,11 @@
email = "[email protected]"
token = SecureRandom.uuid

User.create!({ email:, token: })
user = User.create!({ email:, token: })

code = "ABC123"
login_gov = instance_double(LoginGov)
allow(LoginGov).to receive(:new).and_return(login_gov)
allow(login_gov).to receive(:exchange_token_from_auth_result).with(code).and_return(
[{ email:, sub: token }]
)
mock_login_gov(user, code)

get "/auth/result", params: { code: }

expect(session[:userinfo]).not_to be_nil
Expand Down
18 changes: 13 additions & 5 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@
def create_and_log_in_user(user_attrs = {})
user = create_user(user_attrs)
code = "ABC123"
login_gov = instance_double(LoginGov)
allow(LoginGov).to receive(:new).and_return(login_gov)
allow(login_gov).to receive(:exchange_token_from_auth_result).with(code).and_return(
[{ email: user.email, sub: user.token }]
)
mock_login_gov(user, code)

get "/auth/result", params: { code: }
user
Expand All @@ -36,3 +32,15 @@ def create_user(user_attrs = {})
user_attrs = { email:, token: }.merge(user_attrs)
User.create!(user_attrs)
end

def mock_login_gov(user, code = "ABC123") # rubocop:disable Metrics/AbcSize
login_gov = instance_double(LoginGov)
allow(LoginGov).to receive(:new).and_return(login_gov)
allow(login_gov).to receive(:exchange_token_from_auth_result).with(code).and_return(
[{ email: user.email, sub: user.token }]
)

allow_any_instance_of(SessionsController).to( # rubocop:disable RSpec/AnyInstance
receive(:send_user_jwt_to_phoenix).with(instance_of(String)).and_return(true)
)
end

0 comments on commit 6cfe6b8

Please sign in to comment.