diff --git a/Gemfile b/Gemfile index e5e2fc56..730eac26 100644 --- a/Gemfile +++ b/Gemfile @@ -14,6 +14,12 @@ gem "sqlite3", "~> 1.4" # Use the Puma web server [https://github.com/puma/puma] gem "puma", ">= 5.0" +# Use the popular Faraday HTTP library +gem "faraday" + +# Use the JWT gem for JSON Web Tokens +gem "jwt" + # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] gem "importmap-rails" diff --git a/Gemfile.lock b/Gemfile.lock index 38142e6c..142a3d1f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -101,6 +101,11 @@ GEM reline (>= 0.3.8) drb (2.2.1) erubi (1.13.0) + faraday (2.10.0) + faraday-net_http (>= 2.0, < 3.2) + logger + faraday-net_http (3.1.0) + net-http globalid (1.2.1) activesupport (>= 6.1) i18n (1.14.5) @@ -116,6 +121,9 @@ GEM jbuilder (2.12.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) + jwt (2.8.2) + base64 + logger (1.6.0) loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -130,6 +138,8 @@ GEM minitest (5.23.1) msgpack (1.7.2) mutex_m (0.2.0) + net-http (0.4.1) + uri net-imap (0.4.12) date net-protocol @@ -225,6 +235,7 @@ GEM railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + uri (0.13.0) web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) @@ -248,8 +259,10 @@ DEPENDENCIES bootsnap capybara debug + faraday importmap-rails jbuilder + jwt puma (>= 5.0) rails (~> 7.1.3, >= 7.1.3.4) selenium-webdriver diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 00000000..295f0a0d --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,60 @@ +class SessionsController < ApplicationController + + before_action :handle_result_error, :well_known_configuration, :decode_token, only: [:result] + + def new + # TODO: handle redirect to login page due to inactivity + end + + def create + login_gov = LoginGov.new + redirect_to(login_gov.authorization_url, allow_other_host: true) + end + + def delete + login_gov = LoginGov.new + # TODO: update user session status, clear out JWT + # TODO: add session duration to the security log + # TODO: delete session locally and Phoenix + redirect_to(login_gov.logout_url) + end + + def result + + end + + private + + def handle_result_error + if params[:error] + Rails.logger.error("Login.gov authentication error: #{params[:error]}") + flash[:error] = "There was an issue with logging in. Please try again." + redirect_to new_session_path + end + + if params[:code].nil? && params[:state].nil? + Rails.logger.error("Login.gov unknown error") + flash[:error] = "Please try again." + redirect_to new_session_path + end + end + + def well_known_configuration + @login_gov = LoginGov.new + status, body = @login_gov.get_well_known_configuration + + if status != 200 + Rails.logger.error("well-known/openid-configuration error: code=#{status} - body:\n#{body}") + flash[:error] = "There was an issue logging in." + redirect_to new_session_path + return + end + + Rails.logger.debug("well_known_config response body=#{body}") + @openid_config = body + end + + def decode_token + private_key = @login_gov.private_key + end +end diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb new file mode 100644 index 00000000..309f8b2e --- /dev/null +++ b/app/helpers/sessions_helper.rb @@ -0,0 +1,2 @@ +module SessionsHelper +end diff --git a/app/models/login_gov.rb b/app/models/login_gov.rb new file mode 100644 index 00000000..0c67c451 --- /dev/null +++ b/app/models/login_gov.rb @@ -0,0 +1,57 @@ +class LoginGov + + # :acr_value, + # :client_id, + # :idp_host, + # :login_redirect_uri, + # :logout_redirect_uri, + # :logout_uri, + # :private_key_password, + # :private_key_path, + # :token_endpoint, + attr_reader :config + + def initialize(config = Rails.configuration.login_gov_oidc) + @config = config.freeze.dup + end + + def authorization_url + query = { + client_id: config[:client_id], + response_type: "code", + acr_values: config[:acr_value], + scope: "openid email", + redirect_uri: config[:login_redirect_uri], + state: SecureRandom.hex(16), + nonce: SecureRandom.hex(16), + prompt: "select_account" + }.to_query + + "#{config[:idp_host]}/openid_connect/authorize?#{query}" + end + + def logout_url + query = { + client_id: config[:client_id], + post_logout_redirect_uri: config[:logout_redirect_uri] + }.to_query + + "#{config[:logout_uri]}?#{query}" + end + + def well_known_configuration_url + config[:idp_host] + "/.well-known/openid-configuration" + end + + def get_well_known_configuration + response = Faraday.get(well_known_configuration_url) + [response.status, response.body] + end + + def private_key + key_path = config[:private_key_path] + key_pass = config[:private_key_password] + Rails.logger.info("Get private key from .pem file=#{key_path} pass=#{key_pass}") + "🔐" + end +end \ No newline at end of file diff --git a/app/views/sessions/create.html.erb b/app/views/sessions/create.html.erb new file mode 100644 index 00000000..6e13ad38 --- /dev/null +++ b/app/views/sessions/create.html.erb @@ -0,0 +1,2 @@ +

Session#create

+

Find me in app/views/session/create.html.erb

diff --git a/app/views/sessions/delete.html.erb b/app/views/sessions/delete.html.erb new file mode 100644 index 00000000..bea2c927 --- /dev/null +++ b/app/views/sessions/delete.html.erb @@ -0,0 +1,2 @@ +

Session#delete

+

Find me in app/views/session/delete.html.erb

diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb new file mode 100644 index 00000000..90386bdc --- /dev/null +++ b/app/views/sessions/new.html.erb @@ -0,0 +1,14 @@ +
+
+
+

You must accept the U.S. Government System terms to sign into this website

+

+ This is a U.S. General Services Administration Federal Government + computer system that is "FOR OFFICIAL USE ONLY." This System is subject + to monitoring. Individuals found performing unauthorized activities are + subject to disciplinary action including criminal prosecution. +

+ <%= button_to("Accept and Sign-in", session_path, class: "usa-button center-btn") %> +
+
+
diff --git a/app/views/sessions/result.html.erb b/app/views/sessions/result.html.erb new file mode 100644 index 00000000..d15b313c --- /dev/null +++ b/app/views/sessions/result.html.erb @@ -0,0 +1,2 @@ +

Session#result

+

Find me in app/views/session/result.html.erb

diff --git a/config/environments/development.rb b/config/environments/development.rb index 2e7fb486..498de12e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -73,4 +73,17 @@ # Raise error when a before_action's only/except options reference missing actions config.action_controller.raise_on_missing_callback_actions = true + + config.login_gov_oidc = { + idp_host: "https://idp.int.identitysandbox.gov", + login_redirect_uri: "http://localhost:3000/auth/result", + logout_uri: "https://idp.int.identitysandbox.gov/openid_connect/logout", + logout_redirect_uri: "https://www.challenge.gov/", + acr_value: "http://idmanagement.gov/ns/assurance/loa/1", + client_id: "urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:challenge_gov_platform_dev", + private_key_password: nil, + private_key_path: "config/private.pem", + public_key_path: "config/public.crt", + token_endpoint: "https://idp.int.identitysandbox.gov/api/openid_connect/token" + } end diff --git a/config/routes.rb b/config/routes.rb index a125ef08..6d3916f7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,7 @@ Rails.application.routes.draw do # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + get 'auth/result', to: 'sessions#result' + resource 'session', only: [:new, :create, :destroy] # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb new file mode 100644 index 00000000..3e7ff661 --- /dev/null +++ b/test/controllers/sessions_controller_test.rb @@ -0,0 +1,23 @@ +require "test_helper" + +class SessionsControllerTest < ActionDispatch::IntegrationTest + test "should get new" do + get session_new_url + assert_response :success + end + + test "should get create" do + post session_create_url + assert_response :success + end + + test "should get delete" do + get session_delete_url + assert_response :success + end + + test "should get result" do + get session_result_url + assert_response :success + end +end