Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[56] Implement shared session between Rails <=> Phoenix #128

Merged
merged 18 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯


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
Loading