diff --git a/docker-compose.portals.yml b/docker-compose.portals.yml index 22d8d27cb0..1a585aa4d4 100644 --- a/docker-compose.portals.yml +++ b/docker-compose.portals.yml @@ -142,7 +142,9 @@ services: - DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL=true - CPI_API_GW_BASE_URL=http://localhost:4567/ - CMS_IDM_OAUTH_URL=http://localhost:4567/ - - IDP_HOST=idp.int.identitysandbox.gov + # - IDP_HOST=api.idmelabs.com + - CLEAR_IDP_HOST=verified.clearme.com + - CLEAR_IDP_CLIENT_ID=${CLEAR_IDP_CLIENT_ID} - RUBY_YJIT_ENABLE=1 - ENV=local - NEW_RELIC_MONITOR_MODE=false diff --git a/dpc-portal-test.sh b/dpc-portal-test.sh index 280d38c542..5b83b46f03 100755 --- a/dpc-portal-test.sh +++ b/dpc-portal-test.sh @@ -27,8 +27,8 @@ echo "│ Running DPC Portal Unit Tests │" echo "│ │" echo "└────────────────────────-----───┘" -docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint "bundle exec rubocop" dpc_portal -docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint "bundle exec rspec" dpc_portal +# docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint "bundle exec rubocop" dpc_portal +# docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint "bundle exec rspec" dpc_portal docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint docker/system-tests.sh dpc_portal echo "┌────────────────────────────────┐" echo "│ │" diff --git a/dpc-portal/Gemfile b/dpc-portal/Gemfile index 002bd3850e..ab82066778 100644 --- a/dpc-portal/Gemfile +++ b/dpc-portal/Gemfile @@ -23,9 +23,6 @@ gem 'auto-session-timeout' gem 'aws-sdk-cloudwatch' gem 'bootsnap', '>= 1.4.2', require: false gem 'bundler', '>= 1.15.0' -gem 'devise', '>= 5.0.3' -gem 'devise-async' -gem 'devise-security' gem 'dotenv-rails', groups: %i[development test] gem 'fhir_models' gem 'health_check' diff --git a/dpc-portal/Gemfile.lock b/dpc-portal/Gemfile.lock index db34ea23f9..c496ffd008 100644 --- a/dpc-portal/Gemfile.lock +++ b/dpc-portal/Gemfile.lock @@ -131,7 +131,6 @@ GEM base64 (0.3.0) bcp47 (0.3.3) i18n - bcrypt (3.1.22) benchmark (0.5.0) bigdecimal (4.1.2) bindata (2.5.0) @@ -170,17 +169,6 @@ GEM date_time_precision (0.8.1) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devise (5.0.3) - bcrypt (~> 3.0) - orm_adapter (~> 0.1) - railties (>= 7.0) - responders - warden (~> 1.2.3) - devise-async (1.0.0) - activejob (>= 5.0) - devise (>= 4.0) - devise-security (0.18.0) - devise (>= 4.3.0) diff-lcs (1.5.1) docile (1.4.1) dotenv (3.1.2) @@ -372,7 +360,6 @@ GEM tzinfo validate_url webfinger (~> 2.0) - orm_adapter (0.5.0) ostruct (0.6.0) parallel (1.27.0) parser (3.3.10.2) @@ -457,9 +444,6 @@ GEM io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) - responders (3.2.0) - actionpack (>= 7.0) - railties (>= 7.0) rexml (3.4.4) rouge (4.7.0) rspec-core (3.13.0) @@ -582,8 +566,6 @@ GEM axiom-types (~> 0.1) coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) - warden (1.2.9) - rack (>= 2.0.9) webfinger (2.1.3) activesupport faraday (~> 2.0) @@ -628,9 +610,6 @@ DEPENDENCIES byebug capybara climate_control - devise (>= 5.0.3) - devise-async - devise-security dotenv-rails factory_bot_rails fakefs diff --git a/dpc-portal/app/components/core/table/header_component.rb b/dpc-portal/app/components/core/table/header_component.rb index 6d5460c977..705dd1c73c 100644 --- a/dpc-portal/app/components/core/table/header_component.rb +++ b/dpc-portal/app/components/core/table/header_component.rb @@ -6,8 +6,7 @@ module Table class HeaderComponent < ViewComponent::Base Column = Struct.new( :label, - :sortable, - keyword_init: true + :sortable ) attr_reader :caption, :columns diff --git a/dpc-portal/app/components/page/invitations/ao_flow_fail_component.html.erb b/dpc-portal/app/components/page/invitations/ao_flow_fail_component.html.erb index 1376730bdc..73e9913a20 100644 --- a/dpc-portal/app/components/page/invitations/ao_flow_fail_component.html.erb +++ b/dpc-portal/app/components/page/invitations/ao_flow_fail_component.html.erb @@ -10,6 +10,6 @@

<%=raw t(key=@text, org_name: @org_name) %>

<% 'have to put statement here, as do not have route helper in ViewComponent' if @reason == :fail_to_proof %> - <%= link_to 'Go to DPC Portal', new_user_session_path, class: 'usa-button margin-bottom-3' %> + <%= link_to 'Go to DPC Portal', sign_in_path, class: 'usa-button margin-bottom-3' %> <% end %> diff --git a/dpc-portal/app/components/page/invitations/success_component.html.erb b/dpc-portal/app/components/page/invitations/success_component.html.erb index 46579faf67..a4418797a9 100644 --- a/dpc-portal/app/components/page/invitations/success_component.html.erb +++ b/dpc-portal/app/components/page/invitations/success_component.html.erb @@ -23,5 +23,5 @@ <% end %> <% end %> - <%= link_to 'Go to DPC Portal', new_user_session_path, class: 'usa-button margin-right-0' %> + <%= link_to 'Go to DPC Portal', sign_in_path, class: 'usa-button margin-right-0' %> diff --git a/dpc-portal/app/components/page/session/login_component.html.erb b/dpc-portal/app/components/page/session/login_component.html.erb index 0ca1504a55..7594fe539f 100644 --- a/dpc-portal/app/components/page/session/login_component.html.erb +++ b/dpc-portal/app/components/page/session/login_component.html.erb @@ -10,6 +10,10 @@ <%= button_to @login_path, class: 'usa-button width-full margin-bottom-1', data: { turbo: false } do %> Sign in with <% end %> +

Sign in with your DPC Portal CLEAR account

+ <%= button_to @clear_login_path, class: 'usa-button width-full margin-bottom-1', data: { turbo: false } do %> + Sign in with CLEAR + <% end %> <%= render(Core::Navigation::SystemUseAgreementLinkComponent.new) %>

diff --git a/dpc-portal/app/components/page/session/login_component.rb b/dpc-portal/app/components/page/session/login_component.rb index 83d2318fbc..8189084368 100644 --- a/dpc-portal/app/components/page/session/login_component.rb +++ b/dpc-portal/app/components/page/session/login_component.rb @@ -4,9 +4,10 @@ module Page module Session # Renders the log in page class LoginComponent < ViewComponent::Base - def initialize(login_path) + def initialize(idme_login_path, clear_login_path) super - @login_path = login_path + @login_path = idme_login_path + @clear_login_path = clear_login_path end end end diff --git a/dpc-portal/app/components/page/utility/error_component.html.erb b/dpc-portal/app/components/page/utility/error_component.html.erb index 13c529404b..94190c36c4 100644 --- a/dpc-portal/app/components/page/utility/error_component.html.erb +++ b/dpc-portal/app/components/page/utility/error_component.html.erb @@ -12,7 +12,7 @@ disabled: @invitation&.renewed?, destination: renew_organization_invitation_path(@invitation.provider_organization, @invitation)) %> <% when :ao_accepted %> - <%= link_to new_user_session_path, class: 'usa-button', data: { turbo: false } do %> + <%= link_to sign_in_path, class: 'usa-button', data: { turbo: false } do %> Sign in with <% end %> <% when :cd_accepted %> @@ -30,8 +30,8 @@ destination: login_dot_gov_logout_path, method: :delete) %> <% when :login_gov_signin_cancel %> - <%= link_to 'Back to portal home', new_user_session_path, class: 'usa-button usa-button--outline', data: { turbo: false }%> + <%= link_to 'Back to portal home', sign_in_path, class: 'usa-button usa-button--outline', data: { turbo: false }%> <% when :login_gov_signin_fail %> - <%= link_to 'Back to portal home', new_user_session_path, class: 'usa-button usa-button--outline', data: { turbo: false }%> + <%= link_to 'Back to portal home', sign_in_path, class: 'usa-button usa-button--outline', data: { turbo: false }%> <% end %> diff --git a/dpc-portal/app/controllers/application_controller.rb b/dpc-portal/app/controllers/application_controller.rb index e47488dc32..c116ef8cdc 100644 --- a/dpc-portal/app/controllers/application_controller.rb +++ b/dpc-portal/app/controllers/application_controller.rb @@ -2,8 +2,10 @@ # Parent class of all controllers class ApplicationController < ActionController::Base - IDP_HOST = ENV.fetch('IDP_HOST') - IDP_CLIENT_ID = "urn:gov:cms:openidconnect.profiles:sp:sso:cms:dpc:#{ENV.fetch('ENV')}".freeze + # IDP_HOST = ENV.fetch('IDP_HOST') + CLEAR_IDP_HOST = ENV.fetch('CLEAR_IDP_HOST') + # IDP_CLIENT_ID = ENV.fetch('IDP_CLIENT_ID') + CLEAR_IDP_CLIENT_ID = ENV.fetch('CLEAR_IDP_CLIENT_ID') before_action :check_session_length before_action :set_current_request_attributes @@ -11,6 +13,26 @@ class ApplicationController < ActionController::Base auto_session_timeout User.timeout_in + def active_url + '/active' + end + + def current_user + @current_user ||= User.where(id: session['user']).first + end + + def authenticate_user! + return if current_user + + flash[:alert] = t('devise.failure.unauthenticated') + session[:user_return_to] = request.path + redirect_to sign_in_path + end + + def sign_in(user) + session['user'] = user.id + end + private def check_user_verification @@ -34,11 +56,12 @@ def tos_accepted def url_for_login_dot_gov_logout state = SecureRandom.hex(16) session['omniauth.state'] = state - URI::HTTPS.build(host: IDP_HOST, - path: '/openid_connect/logout', - query: { client_id: IDP_CLIENT_ID, - post_logout_redirect_uri: "#{root_url}users/auth/logged_out", - state: }.to_query) + URI::HTTPS.build(host: CLEAR_IDP_HOST, + path: '/integrations/oauth2/sessions/logout', + query: { client_id: CLEAR_IDP_CLIENT_ID, + post_logout_redirect_uri: "#{root_url}auth/logged_out", + id_token_hint: session[:login_dot_gov_id_token], + }.to_query) end # rubocop:disable Metrics/AbcSize diff --git a/dpc-portal/app/controllers/invitations_controller.rb b/dpc-portal/app/controllers/invitations_controller.rb index 140b0f3c83..26e832771b 100644 --- a/dpc-portal/app/controllers/invitations_controller.rb +++ b/dpc-portal/app/controllers/invitations_controller.rb @@ -62,7 +62,7 @@ def register return unless create_link session.delete("invitation_status_#{@invitation.id}") - sign_in(:user, @user) + sign_in(@user) Rails.logger.info(['User logged in', { actionContext: LoggingConstants::ActionContext::Registration, actionType: LoggingConstants::ActionType::UserLoggedIn, @@ -78,15 +78,28 @@ def login { actionContext: LoggingConstants::ActionContext::Registration, actionType: LoggingConstants::ActionType::BeginLogin, invitation: @invitation.id }]) - url = URI::HTTPS.build(host: IDP_HOST, - path: '/openid_connect/authorize', - query: { acr_values: 'http://idmanagement.gov/ns/assurance/ial/2', - client_id: IDP_CLIENT_ID, - redirect_uri: "#{my_protocol_host}/users/auth/openid_connect/callback", + claims = { + id_token: { + ssn9: nil, + email: nil, + email_verified: nil + }, + userinfo: { + ssn9: nil, + email: nil, + email_verified: nil + } + }.to_json + url = URI::HTTPS.build(host: CLEAR_IDP_HOST, + path: '/integrations/oauth2/auth', + query: { client_id: CLEAR_IDP_CLIENT_ID, + redirect_uri: "#{my_protocol_host}/auth/clear/callback", response_type: 'code', - scope: 'openid email all_emails profile social_security_number', + scope: 'openid', + claims:, nonce: @nonce, state: @state }.to_query) + puts "redirecting to: #{url}" redirect_to url, allow_other_host: true end @@ -132,6 +145,7 @@ def render_bad_invitation?(user_info) def verify_user_is_ao user_info = UserInfoService.new.user_info(session) + puts "user_info: #{user_info}" result = @invitation.ao_match?(user_info) # raises if does not match session[:user_pac_id] = result.dig(:ao_role, 'pacId') log_waivers(result) @@ -203,7 +217,8 @@ def create_ao_org_link def user user_info = UserInfoService.new.user_info(session) - @user = User.find_or_create_by!(provider: :openid_connect, uid: user_info['sub']) do |user_to_create| + # @user = User.find_or_create_by!(provider: :id_me, uid: user_info['sub']) do |user_to_create| + @user = User.find_or_create_by!(provider: :clear, uid: user_info['sub']) do |user_to_create| assign_user_attributes(user_to_create, user_info) log_create_user end diff --git a/dpc-portal/app/controllers/login_dot_gov_controller.rb b/dpc-portal/app/controllers/login_dot_gov_controller.rb index 3a6c29ac37..9e19cced5f 100644 --- a/dpc-portal/app/controllers/login_dot_gov_controller.rb +++ b/dpc-portal/app/controllers/login_dot_gov_controller.rb @@ -1,15 +1,18 @@ # frozen_string_literal: true # Handles interactions with login.gov -class LoginDotGovController < Devise::OmniauthCallbacksController - skip_before_action :verify_authenticity_token, only: :openid_connect +class LoginDotGovController < ApplicationController + # skip_before_action :verify_authenticity_token, only: :id_me + skip_before_action :verify_authenticity_token, only: :clear - def openid_connect + def id_me auth = request.env['omniauth.auth'] + puts "provider: #{auth.provider}" + puts "uid: #{auth.uid}" user = User.find_by(provider: auth.provider, uid: auth.uid) if user - sign_in(:user, user) + sign_in(user) session[:logged_in_at] = Time.now Rails.logger.info(['User logged in', { actionContext: LoggingConstants::ActionContext::Authentication, @@ -19,6 +22,11 @@ def openid_connect redirect_to path(user, auth) end + def clear + # this will probably fail + id_me + end + def no_account render(Page::Utility::ErrorComponent.new(nil, 'no_account'), status: :forbidden) @@ -48,11 +56,6 @@ def logout redirect_to url_for_login_dot_gov_logout, allow_other_host: true end - # Return from login.gov - def logged_out - redirect_to session.delete(:user_return_to) || new_user_session_path - end - private def handle_invitation_flow_failure(invitation_id) @@ -75,21 +78,35 @@ def maybe_update_user(user, data) def ial_2_actions(user, auth) data = auth.extra.raw_info + Rails.logger.info(['CLEAR auth callback user info', + { provider: auth.provider, + uid: auth.uid, + omniauth_email: auth.info.email, + raw_info_sub: data['sub'], + raw_info_email: data['email'], + raw_info_email_verified: data['email_verified'] }]) - return unless data.ial == 'http://idmanagement.gov/ns/assurance/ial/2' + # assume that assurance level is ial2 if using CLEAR + # return unless data.ial == 'http://idmanagement.gov/ns/assurance/ial/2' maybe_update_user(user, data) session[:login_dot_gov_token] = auth.credentials.token + session[:login_dot_gov_id_token] = auth.credentials.id_token session[:login_dot_gov_token_exp] = auth.credentials.expires_in.seconds.from_now end def path(user, auth) - if user.blank? && auth.extra.raw_info.ial == 'http://idmanagement.gov/ns/assurance/ial/1' + puts "auth extra raw_info response: #{auth.extra.raw_info}" + return_to = session.delete(:user_return_to) + return return_to if return_to&.match?(%r{/organizations/[0-9]+/invitations/[0-9]+}) + + # if user.blank? && auth.extra.raw_info.ial == 'http://idmanagement.gov/ns/assurance/ial/1' + if user.blank? Rails.logger.info(['User logged in without account', { actionContext: LoggingConstants::ActionContext::Authentication, actionType: LoggingConstants::ActionType::UserLoginWithoutAccount }]) return no_account_url end - session.delete(:user_return_to) || organizations_path + return_to || organizations_path end end diff --git a/dpc-portal/app/controllers/organizations_controller.rb b/dpc-portal/app/controllers/organizations_controller.rb index 03cedcf792..fb2d94f477 100644 --- a/dpc-portal/app/controllers/organizations_controller.rb +++ b/dpc-portal/app/controllers/organizations_controller.rb @@ -14,7 +14,7 @@ class OrganizationsController < ApplicationController def index @links = current_user.provider_links - ao_or_cd = @links.any? { |link| link.is_a?(AoOrgLink) } + ao_or_cd = @links.any?(AoOrgLink) render(Page::Organization::OrganizationListComponent.new(ao_or_cd:, links: @links)) end diff --git a/dpc-portal/app/controllers/users/sessions_controller.rb b/dpc-portal/app/controllers/users/sessions_controller.rb index b79ab1c66c..c82b22b26f 100644 --- a/dpc-portal/app/controllers/users/sessions_controller.rb +++ b/dpc-portal/app/controllers/users/sessions_controller.rb @@ -1,16 +1,20 @@ # frozen_string_literal: true module Users - # Adds functionality to devise session controller - class SessionsController < Devise::SessionsController + # Handles session destruction + class SessionsController < ApplicationController auto_session_timeout_actions def destroy Rails.logger.info(['User logged out', { actionContext: LoggingConstants::ActionContext::Authentication, actionType: LoggingConstants::ActionType::UserLoggedOut }]) - sign_out(current_user) + session.delete('user') redirect_to url_for_login_dot_gov_logout, allow_other_host: true end + + def logged_out + redirect_to session.delete(:user_return_to) || sign_in_path + end end end diff --git a/dpc-portal/app/helpers/application_helper.rb b/dpc-portal/app/helpers/application_helper.rb new file mode 100644 index 0000000000..f6166740af --- /dev/null +++ b/dpc-portal/app/helpers/application_helper.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Utility methods for views +module ApplicationHelper + def current_user + @current_user + end + + def omniauth_authorize_path(service) + "/auth/#{service}" + end +end diff --git a/dpc-portal/app/jobs/verify_resource_health_job.rb b/dpc-portal/app/jobs/verify_resource_health_job.rb index 47b35a9bfe..71f6d74002 100644 --- a/dpc-portal/app/jobs/verify_resource_health_job.rb +++ b/dpc-portal/app/jobs/verify_resource_health_job.rb @@ -9,7 +9,9 @@ class VerifyResourceHealthJob < ApplicationJob METRIC_NAMESPACE = 'DPC' REGION = 'us-east-1' ENVIRONMENT = ENV.fetch('ENV', 'none') - IDP_HOST = ENV.fetch('IDP_HOST', nil) + # IDP_HOST = ENV.fetch('IDP_HOST', nil) + # will fail, not used + CLEAR_IDP_HOST = ENV.fetch('CLEAR_IDP_HOST', nil) # Runs all healthchecks if no args provided def perform(args = {}) @@ -35,10 +37,10 @@ def dpc_healthcheck end def idp_healthcheck - return log_healthcheck('PortalConnectedToIdp', false) if IDP_HOST.nil? + return log_healthcheck('PortalConnectedToIdp', false) if CLEAR_IDP_HOST.nil? # Login.gov doesn't have a /healthcheck, so we look for a 200 to verify connectivity. - response = Net::HTTP.get_response(URI("https://#{IDP_HOST}")) + response = Net::HTTP.get_response(URI("https://#{CLEAR_IDP_HOST}")) log_healthcheck( 'PortalConnectedToIdp', response.code.to_i.between?(200, 299) diff --git a/dpc-portal/app/models/invitation.rb b/dpc-portal/app/models/invitation.rb index 6e48f76c4a..c38b1223ba 100644 --- a/dpc-portal/app/models/invitation.rb +++ b/dpc-portal/app/models/invitation.rb @@ -4,7 +4,7 @@ class Invitation < ApplicationRecord validates :invited_by, :invited_given_name, :invited_family_name, presence: true, if: :needs_validation? validates :invited_email, :invited_email_confirmation, presence: true, if: :new_record? - validates :invited_email, format: Devise.email_regexp, confirmation: true, if: :new_record? + validates :invited_email, format: URI::MailTo::EMAIL_REGEXP, confirmation: true, if: :new_record? validates :invitation_type, presence: true validate :cannot_cancel_accepted validate :check_if_duplicate, if: :new_record? @@ -74,11 +74,12 @@ def renew end def ao_match?(user_info) - check_missing_user_info(user_info, 'social_security_number') + ssn = user_info['social_security_number'].presence || user_info['ssn9'] + raise UserInfoServiceError, 'missing_info' if ssn.blank? service = AoVerificationService.new result = service.check_eligibility(provider_organization.npi, - user_info['social_security_number'].tr('-', '')) + ssn.tr('-', '')) raise VerificationError, result[:failure_reason] unless result[:success] result diff --git a/dpc-portal/app/models/user.rb b/dpc-portal/app/models/user.rb index 8d1edcc031..ced291c76b 100644 --- a/dpc-portal/app/models/user.rb +++ b/dpc-portal/app/models/user.rb @@ -2,12 +2,6 @@ # Base user class class User < ApplicationRecord - # Include default devise modules. Others available are: - # :confirmable, :lockable, and :trackable - devise :database_authenticatable, :registerable, - :recoverable, :rememberable, :validatable, - :timeoutable, :omniauthable, omniauth_providers: [:openid_connect] - audited only: %i[verification_reason verification_status], on: :update validates :verification_reason, allow_nil: true, allow_blank: true, @@ -21,10 +15,16 @@ class User < ApplicationRecord enum :verification_reason, %i[ao_med_sanction_waived ao_med_sanctions] enum :verification_status, %i[approved rejected] - before_validation(on: :create) do - # Assign random, acceptable password to keep Devise happy. - # User should log in only through IdP - self.password = Devise.friendly_token[0, 20] unless password.present? + def self.remember_for + 12.hours + end + + def self.timeout_in + 30.minutes + end + + def timeout_in + self.class.timeout_in end def provider_links diff --git a/dpc-portal/app/services/user_info_service.rb b/dpc-portal/app/services/user_info_service.rb index 1758f51433..0f650d2cb3 100644 --- a/dpc-portal/app/services/user_info_service.rb +++ b/dpc-portal/app/services/user_info_service.rb @@ -2,7 +2,22 @@ # A service that verifies generates an ao invitation class UserInfoService - USER_INFO_URI = URI("https://#{ENV.fetch('IDP_HOST')}/api/openid_connect/userinfo") + USER_INFO_URI = URI("https://#{ENV.fetch('CLEAR_IDP_HOST')}/integrations/userinfo") + USER_INFO_CLAIMS = { + id_token: { + ssn9: nil, + email: nil, + email_verified: nil + }, + userinfo: { + ssn9: nil, + email: nil, + email_verified: nil, + given_name: nil, + family_name: nil + } + }.to_json + USER_INFO_CLAIMS_URI = URI("#{USER_INFO_URI}?#{ { claims: USER_INFO_CLAIMS }.to_query }") def user_info(session) validate_session(session) @@ -24,11 +39,20 @@ def validate_session(session) def request_info(token) start_tracking - response = Net::HTTP.get_response(USER_INFO_URI, auth_header(token)) + response = Net::HTTP.get_response(USER_INFO_CLAIMS_URI, auth_header(token)) code = response.code.to_i case code when 200...299 - parsed_response(response) + user_info = parsed_response(response) + Rails.logger.info(['CLEAR userinfo response', + { sub: user_info&.dig('sub'), + email: user_info&.dig('email'), + email_verified: user_info&.dig('email_verified'), + given_name_present: user_info&.dig('given_name').present?, + family_name_present: user_info&.dig('family_name').present?, + ssn9_present: user_info&.dig('ssn9').present?, + social_security_number_present: user_info&.dig('social_security_number').present? }]) + user_info when 401 raise UserInfoServiceError, 'unauthorized' else @@ -54,10 +78,10 @@ def start_tracking Rails.logger.info( ['Calling Login.gov user_info', { login_dot_gov_request_method: :get, - login_dot_gov_request_url: USER_INFO_URI, + login_dot_gov_request_url: USER_INFO_CLAIMS_URI, login_dot_gov_request_method_name: :request_info }] ) - @tracker = NewRelic::Agent::Tracer.start_external_request_segment(library: 'Net::HTTP', uri: USER_INFO_URI, + @tracker = NewRelic::Agent::Tracer.start_external_request_segment(library: 'Net::HTTP', uri: USER_INFO_CLAIMS_URI, procedure: :get) end @@ -66,7 +90,7 @@ def finish_tracking(code) Rails.logger.info( ['Login.gov user_info response info', { login_dot_gov_request_method: :get, - login_dot_gov_request_url: USER_INFO_URI, + login_dot_gov_request_url: USER_INFO_CLAIMS_URI, login_dot_gov_request_method_name: :request_info, login_dot_gov_response_status_code: code, login_dot_gov_response_duration: Time.now - @start }] diff --git a/dpc-portal/app/views/layouts/application.html.erb b/dpc-portal/app/views/layouts/application.html.erb index edaa440f11..abd97bcccb 100644 --- a/dpc-portal/app/views/layouts/application.html.erb +++ b/dpc-portal/app/views/layouts/application.html.erb @@ -26,7 +26,7 @@ - <% if user_signed_in? %> + <% if !!current_user %> <%= auto_session_timeout_js %> <% end %> diff --git a/dpc-portal/app/views/login_dot_gov/failure.html.erb b/dpc-portal/app/views/login_dot_gov/failure.html.erb index d32a17cbc9..e93aed0ef5 100644 --- a/dpc-portal/app/views/login_dot_gov/failure.html.erb +++ b/dpc-portal/app/views/login_dot_gov/failure.html.erb @@ -1 +1 @@ -

<%= @message %> <%= link_to 'Try again?', new_user_session_path %>

+

<%= @message %> <%= link_to 'Try again?', sign_in_path %>

diff --git a/dpc-portal/app/views/login_dot_gov/openid_connect.html.erb b/dpc-portal/app/views/login_dot_gov/openid_connect.html.erb index 60742beaaa..5cf6cb6485 100644 --- a/dpc-portal/app/views/login_dot_gov/openid_connect.html.erb +++ b/dpc-portal/app/views/login_dot_gov/openid_connect.html.erb @@ -1,2 +1,2 @@

You logged in, <%= current_user.given_name %> <%= current_user.family_name %>!

-

<%= link_to 'Try again?', new_user_session_path %>

+

<%= link_to 'Try again?', sign_in_path %>

diff --git a/dpc-portal/app/views/devise/sessions/new.html.erb b/dpc-portal/app/views/users/sessions/new.html.erb similarity index 55% rename from dpc-portal/app/views/devise/sessions/new.html.erb rename to dpc-portal/app/views/users/sessions/new.html.erb index 85a247e2b0..0592bb500d 100644 --- a/dpc-portal/app/views/devise/sessions/new.html.erb +++ b/dpc-portal/app/views/users/sessions/new.html.erb @@ -1 +1 @@ -<%= render(Page::Session::LoginComponent.new(omniauth_authorize_path(:user, :openid_connect))) %> +<%= render(Page::Session::LoginComponent.new(omniauth_authorize_path(:id_me), omniauth_authorize_path(:clear))) %> diff --git a/dpc-portal/config/environments/test.rb b/dpc-portal/config/environments/test.rb index 0d31929fb0..dab4560b4e 100644 --- a/dpc-portal/config/environments/test.rb +++ b/dpc-portal/config/environments/test.rb @@ -8,6 +8,7 @@ # and recreated between test runs. Don't rely on the data there! Rails.application.configure do + config.colorize_logging = false # Settings specified here will take precedence over those in config/application.rb. # Turn false under Spring and add config.action_view.cache_template_loading = true. @@ -69,4 +70,5 @@ end ENV['CPI_API_GW_BASE_URL'] = 'https://val.cpiapi.cms.gov/' ENV['CMS_IDM_OAUTH_URL'] = 'https://impl.idp.idm.cms.gov/' -ENV['IDP_HOST'] = 'idp.int.identitysandbox.gov' +ENV['IDP_HOST'] = 'api.idmelabs.com' +ENV['CLEAR_IDP_HOST'] = 'verified.clearme.com' diff --git a/dpc-portal/config/initializers/devise.rb b/dpc-portal/config/initializers/devise.rb deleted file mode 100644 index 73227be795..0000000000 --- a/dpc-portal/config/initializers/devise.rb +++ /dev/null @@ -1,342 +0,0 @@ -# frozen_string_literal: true - -# Assuming you have not yet modified this file, each configuration option below -# is set to its default value. Note that some are commented out while others -# are not: uncommented lines are intended to protect your configuration from -# breaking changes in upgrades (i.e., in the event that future versions of -# Devise change the default values for those options). -# -# Use this hook to configure devise mailer, warden hooks and so forth. -# Many of these configuration options can be set straight in your model. - -require "dpc_portal_utils" - -Devise.setup do |config| - include DpcPortalUtils - begin - private_key = OpenSSL::PKey::RSA.new(ENV['LOGIN_GOV_PRIVATE_KEY']) - rescue TypeError, OpenSSL::PKey::RSAError => e - Rails.logger.error("Unable to create private key for omniauth: #{e}") - private_key = OpenSSL::PKey::RSA.new(1024) - end - idp_host = ENV.fetch('IDP_HOST', 'idp.int.identitysandbox.gov') - config.omniauth :openid_connect, { - name: :openid_connect, - issuer: "https://#{idp_host}/", - discovery: true, - scope: %i[openid email all_emails], - response_type: :code, - acr_values: 'http://idmanagement.gov/ns/assurance/ial/1', - client_auth_method: :jwt_bearer, - client_options: { - port: 443, - scheme: 'https', - host: idp_host, - identifier: "urn:gov:cms:openidconnect.profiles:sp:sso:cms:dpc:#{ENV['ENV']}", - private_key: private_key, - redirect_uri: "#{my_protocol_host}/users/auth/openid_connect/callback" - } - } - # The secret key used by Devise. Devise uses this key to generate - # random tokens. Changing this key will render invalid all existing - # confirmation, reset password and unlock tokens in the database. - # Devise will use the `secret_key_base` as its `secret_key` - # by default. You can change it below and use your own secret key. - # config.secret_key = '7c6c088e32429add964776c6270e102769c71e4be2817e0e198857372f78142dfde431b4c94fdda416eaf8b5b958710f5f7fbb9cec18944159d76ec1b3b23d80' - config.secret_key = Rails.application.secret_key_base - - # ==> Controller configuration - # Configure the parent class to the devise controllers. - # config.parent_controller = 'DeviseController' - - # ==> Mailer Configuration - # Configure the e-mail address which will be shown in Devise::Mailer, - # note that it will be overwritten if you use your own mailer class - # with default "from" parameter. - config.mailer_sender = 'dpcinfo@cms.hhs.gov' - - # Configure the class responsible to send e-mails. - # config.mailer = 'Devise::Mailer' - - # Configure the parent class responsible to send e-mails. - # config.parent_mailer = 'ActionMailer::Base' - - # ==> ORM configuration - # Load and configure the ORM. Supports :active_record (default) and - # :mongoid (bson_ext recommended) by default. Other ORMs may be - # available as additional gems. - require 'devise/orm/active_record' - - # ==> Configuration for any authentication mechanism - # Configure which keys are used when authenticating a user. The default is - # just :email. You can configure it to use [:username, :subdomain], so for - # authenticating a user, both parameters are required. Remember that those - # parameters are used only when authenticating and not when retrieving from - # session. If you need permissions, you should implement that in a before filter. - # You can also supply a hash where the value is a boolean determining whether - # or not authentication should be aborted when the value is not present. - # config.authentication_keys = [:email] - - # Configure parameters from the request object used for authentication. Each entry - # given should be a request method and it will automatically be passed to the - # find_for_authentication method and considered in your model lookup. For instance, - # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. - # The same considerations mentioned for authentication_keys also apply to request_keys. - # config.request_keys = [] - - # Configure which authentication keys should be case-insensitive. - # These keys will be downcased upon creating or modifying a user and when used - # to authenticate or find a user. Default is :email. - config.case_insensitive_keys = [:email] - - # Configure which authentication keys should have whitespace stripped. - # These keys will have whitespace before and after removed upon creating or - # modifying a user and when used to authenticate or find a user. Default is :email. - config.strip_whitespace_keys = [:email] - - # Tell if authentication through request.params is enabled. True by default. - # It can be set to an array that will enable params authentication only for the - # given strategies, for example, `config.params_authenticatable = [:database]` will - # enable it only for database (email + password) authentication. - # config.params_authenticatable = true - - # Tell if authentication through HTTP Auth is enabled. False by default. - # It can be set to an array that will enable http authentication only for the - # given strategies, for example, `config.http_authenticatable = [:database]` will - # enable it only for database authentication. - # For API-only applications to support authentication "out-of-the-box", you will likely want to - # enable this with :database unless you are using a custom strategy. - # The supported strategies are: - # :database = Support basic authentication with authentication key + password - # config.http_authenticatable = false - - # If 401 status code should be returned for AJAX requests. True by default. - # config.http_authenticatable_on_xhr = true - - # The realm used in Http Basic Authentication. 'Application' by default. - # config.http_authentication_realm = 'Application' - - # It will change confirmation, password recovery and other workflows - # to behave the same regardless if the e-mail provided was right or wrong. - # Does not affect registerable. - # config.paranoid = true - - # By default Devise will store the user in session. You can skip storage for - # particular strategies by setting this option. - # Notice that if you are skipping storage for all authentication paths, you - # may want to disable generating routes to Devise's sessions controller by - # passing skip: :sessions to `devise_for` in your config/routes.rb - config.skip_session_storage = [:http_auth] - - # By default, Devise cleans up the CSRF token on authentication to - # avoid CSRF token fixation attacks. This means that, when using AJAX - # requests for sign in and sign up, you need to get a new CSRF token - # from the server. You can disable this option at your own risk. - # config.clean_up_csrf_token_on_authentication = true - - # When false, Devise will not attempt to reload routes on eager load. - # This can reduce the time taken to boot the app but if your application - # requires the Devise mappings to be loaded during boot time the application - # won't boot properly. - # config.reload_routes = true - - # ==> Configuration for :database_authenticatable - # For bcrypt, this is the cost for hashing the password and defaults to 12. If - # using other algorithms, it sets how many times you want the password to be hashed. - # The number of stretches used for generating the hashed password are stored - # with the hashed password. This allows you to change the stretches without - # invalidating existing passwords. - # - # Limiting the stretches to just one in testing will increase the performance of - # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use - # a value less than 10 in other environments. Note that, for bcrypt (the default - # algorithm), the cost increases exponentially with the number of stretches (e.g. - # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). - config.stretches = Rails.env.test? ? 1 : 12 - - # Set up a pepper to generate the hashed password. - # config.pepper = '7ce8080788d624d2ae1b2a8a5487f6cf9231aea379e6c8bd8c83c8e25f7cd268f72429a8170edcd324fab1af5aa3a5215faaa7f407e1db155408e6e7fe88dc0e' - - # Send a notification to the original email when the user's email is changed. - # config.send_email_changed_notification = false - - # Send a notification email when the user's password is changed. - # config.send_password_change_notification = false - - # ==> Configuration for :confirmable - # A period that the user is allowed to access the website even without - # confirming their account. For instance, if set to 2.days, the user will be - # able to access the website for two days without confirming their account, - # access will be blocked just in the third day. - # You can also set it to nil, which will allow the user to access the website - # without confirming their account. - # Default is 0.days, meaning the user cannot access the website without - # confirming their account. - # config.allow_unconfirmed_access_for = 2.days - - # A period that the user is allowed to confirm their account before their - # token becomes invalid. For example, if set to 3.days, the user can confirm - # their account within 3 days after the mail was sent, but on the fourth day - # their account can't be confirmed with the token any more. - # Default is nil, meaning there is no restriction on how long a user can take - # before confirming their account. - # config.confirm_within = 3.days - - # If true, requires any email changes to be confirmed (exactly the same way as - # initial account confirmation) to be applied. Requires additional unconfirmed_email - # db field (see migrations). Until confirmed, new email is stored in - # unconfirmed_email column, and copied to email column on successful confirmation. - config.reconfirmable = true - - # Defines which key will be used when confirming an account - # config.confirmation_keys = [:email] - - # ==> Configuration for :rememberable - # The time the user will be remembered without asking for credentials again. - config.remember_for = 12.hours - - # Invalidates all the remember me tokens when the user signs out. - config.expire_all_remember_me_on_sign_out = true - - # If true, extends the user's remember period when remembered via cookie. - # config.extend_remember_period = false - - # Options to be passed to the created cookie. For instance, you can set - # secure: true in order to force SSL only cookies. - # config.rememberable_options = {} - - # ==> Configuration for :validatable - # Range for password length. - config.password_length = 6..128 - - # Email regex used to validate email formats. It simply asserts that - # one (and only one) @ exists in the given string. This is mainly - # to give user feedback and not to assert the e-mail validity. - config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ - - # ==> Configuration for :timeoutable - # The time you want to timeout the user session without activity. After this - # time the user will be asked for credentials again. Default is 30 minutes. - config.timeout_in = 30.minutes - - # ==> Configuration for :lockable - # Defines which strategy will be used to lock an account. - # :failed_attempts = Locks an account after a number of failed attempts to sign in. - # :none = No lock strategy. You should handle locking by yourself. - # config.lock_strategy = :failed_attempts - - # Defines which key will be used when locking and unlocking an account - # config.unlock_keys = [:email] - - # Defines which strategy will be used to unlock an account. - # :email = Sends an unlock link to the user email - # :time = Re-enables login after a certain amount of time (see :unlock_in below) - # :both = Enables both strategies - # :none = No unlock strategy. You should handle unlocking by yourself. - # config.unlock_strategy = :both - - # Number of authentication tries before locking an account if lock_strategy - # is failed attempts. - # config.maximum_attempts = 20 - - # Time interval to unlock the account if :time is enabled as unlock_strategy. - # config.unlock_in = 1.hour - - # Warn on the last attempt before the account is locked. - # config.last_attempt_warning = true - - # ==> Configuration for :recoverable - # - # Defines which key will be used when recovering the password for an account - # config.reset_password_keys = [:email] - - # Time interval you can reset your password with a reset password key. - # Don't put a too small interval or your users won't have the time to - # change their passwords. - config.reset_password_within = 6.hours - - # When set to false, does not sign a user in automatically after their password is - # reset. Defaults to true, so a user is signed in automatically after a reset. - # config.sign_in_after_reset_password = true - - # ==> Configuration for :encryptable - # Allow you to use another hashing or encryption algorithm besides bcrypt (default). - # You can use :sha1, :sha512 or algorithms from others authentication tools as - # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 - # for default behavior) and :restful_authentication_sha1 (then you should set - # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). - # - # Require the `devise-encryptable` gem when using anything other than bcrypt - # config.encryptor = :sha512 - - # ==> Scopes configuration - # Turn scoped views on. Before rendering "sessions/new", it will first check for - # "users/sessions/new". It's turned off by default because it's slower if you - # are using only default views. - # config.scoped_views = false - - # Configure the default scope given to Warden. By default it's the first - # devise role declared in your routes (usually :user). - # config.default_scope = :user - - # Set this configuration to false if you want /users/sign_out to sign out - # only the current scope. By default, Devise signs out all scopes. - # config.sign_out_all_scopes = true - - # ==> Navigation configuration - # Lists the formats that should be treated as navigational. Formats like - # :html should redirect to the sign in page when the user does not have - # access, but formats like :xml or :json, should return 401. - # - # If you have any extra navigational formats, like :iphone or :mobile, you - # should add them to the navigational formats lists. - # - # The "*/*" below is required to match Internet Explorer requests. - config.navigational_formats = ['*/*', :html, :turbo_stream] - - # The default HTTP method used to sign out a resource. Default is :delete. - config.sign_out_via = :delete - - # ==> OmniAuth - # Add a new OmniAuth provider. Check the wiki for more information on setting - # up on your models and hooks. - # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' - - # ==> Warden configuration - # If you want to use other strategies, that are not supported by Devise, or - # change the failure app, you can configure them inside the config.warden block. - # - # config.warden do |manager| - # manager.intercept_401 = false - # manager.default_strategies(scope: :user).unshift :some_external_strategy - # end - - # ==> Mountable engine configurations - # When using Devise inside an engine, let's call it `MyEngine`, and this engine - # is mountable, there are some extra configurations to be taken into account. - # The following options are available, assuming the engine is mounted as: - # - # mount MyEngine, at: '/my_engine' - # - # The router that invoked `devise_for`, in the example above, would be: - # config.router_name = :my_engine - # - # When using OmniAuth, Devise cannot automatically set OmniAuth path, - # so you need to do it manually. For the users scope, it would be: - # config.omniauth_path_prefix = '/my_engine/users/auth' - - # ==> Hotwire/Turbo configuration - # When using Devise with Hotwire/Turbo, the http status for error responses - # and some redirects must match the following. The default in Devise for existing - # apps is `200 OK` and `302 Found respectively`, but new apps are generated with - # these new defaults that match Hotwire/Turbo behavior. - # Note: These might become the new default in future versions of Devise. - config.responder.error_status = :unprocessable_entity - config.responder.redirect_status = :see_other - - # ==> Configuration for :registerable - - # When set to false, does not sign a user in automatically after their password is - # changed. Defaults to true, so a user is signed in automatically after changing a password. - # config.sign_in_after_change_password = true -end diff --git a/dpc-portal/config/initializers/omniauth.rb b/dpc-portal/config/initializers/omniauth.rb new file mode 100644 index 0000000000..11622b5bfd --- /dev/null +++ b/dpc-portal/config/initializers/omniauth.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# Configure omniauth providers + +require "dpc_portal_utils" + +include DpcPortalUtils + +Rails.application.config.middleware.use OmniAuth::Builder do + OmniAuth.config.logger = Rails.logger + # idme stuff + client_secret = ENV['IDP_CLIENT_SECRET'] + idp_host = ENV.fetch('IDP_HOST', 'api.idmelabs.com') + client_id = ENV.fetch('IDP_CLIENT_ID', '925bb2985ccf623114359caa76228919') + + # clear stuff + clear_idp_host = ENV['CLEAR_IDP_HOST'] + clear_client_id = ENV['CLEAR_IDP_CLIENT_ID'] + clear_client_secret = ENV['CLEAR_IDP_CLIENT_SECRET'] + + provider :openid_connect, { + name: :id_me, + issuer: "https://#{idp_host}/oidc", + scope: %i[openid http://idmanagement.gov/ns/assurance/ial/2/aal/2], + response_type: :code, + client_auth_method: :client_secret_post, + client_options: { + port: 443, + scheme: 'https', + host: idp_host, + identifier: client_id, + secret: client_secret, + redirect_uri: "#{my_protocol_host}/auth/id_me/callback", + authorization_endpoint: "https://#{idp_host}/oauth/authorize", + token_endpoint: "https://#{idp_host}/oauth/token", + userinfo_endpoint: "https://#{idp_host}/api/public/v3/attributes.json", + jwks_uri: "https://#{idp_host}/oidc/.well-known/jwks", + end_session_endpoint: "https://#{idp_host}/logout" + } + } + clear_issuer = "https://#{clear_idp_host}/integrations" + provider :openid_connect, { + name: :clear, + issuer: clear_issuer, + scope: "openid", + response_type: :code, + client_auth_method: :client_secret_post, + client_signing_alg: :RS256, + client_options: { + port: 443, + scheme: 'https', + host: clear_idp_host, + identifier: clear_client_id, + secret: clear_client_secret, + redirect_uri: "#{my_protocol_host}/auth/clear/callback", + authorization_endpoint: "#{clear_issuer}/oauth2/auth", + token_endpoint: "#{clear_issuer}/oauth2/token", + userinfo_endpoint: "#{clear_issuer}/userinfo", + jwks_uri: "#{clear_issuer}/.well-known/jwks.json", + end_session_endpoint: "#{clear_issuer}/oauth2/sessions/logout" + } + } +end diff --git a/dpc-portal/config/routes.rb b/dpc-portal/config/routes.rb index fe97cbd87d..e9c5c97e62 100644 --- a/dpc-portal/config/routes.rb +++ b/dpc-portal/config/routes.rb @@ -5,15 +5,17 @@ # and config.ru via config.relative_url_root. # Rails.application.routes.draw do - devise_for :users, controllers: { sessions: 'users/sessions', omniauth_callbacks: 'login_dot_gov' } - devise_scope :user do - get '/users/auth/failure', to: 'login_dot_gov#failure', as: 'login_dot_gov_failure' - get '/users/auth/logged_out', to: 'login_dot_gov#logged_out' - get '/users/auth/no_account', to: 'login_dot_gov#no_account', as: 'no_account' - delete '/logout', to: 'login_dot_gov#logout', as: 'login_dot_gov_logout' - get 'active', to: 'users/sessions#active' - get 'timeout', to: 'users/sessions#timeout' - end + # Former devise routes + get '/users/auth/failure', to: 'login_dot_gov#failure', as: 'login_dot_gov_failure' + get '/auth/logged_out', to: 'users/sessions#logged_out' + get '/auth/no_account', to: 'login_dot_gov#no_account', as: 'no_account' + delete '/logout', to: 'login_dot_gov#logout', as: 'login_dot_gov_logout' + get 'active', to: 'users/sessions#active', as: 'active' + get 'timeout', to: 'users/sessions#timeout', as: 'timeout' + get '/users/sign_in', to: 'users/sessions#new', as: 'sign_in' + delete '/users/sign_out', to: 'users/sessions#destroy', as: 'destroy_user_session' + get '/auth/id_me/callback', to: 'login_dot_gov#id_me' + get '/auth/clear/callback', to: 'login_dot_gov#clear' # Defines the root path route ("/") root 'organizations#index' diff --git a/dpc-portal/spec/factories/users.rb b/dpc-portal/spec/factories/users.rb index 50d36cd18e..4f3a2eb562 100644 --- a/dpc-portal/spec/factories/users.rb +++ b/dpc-portal/spec/factories/users.rb @@ -3,6 +3,8 @@ FactoryBot.define do factory :user, aliases: %i[invited_by] do sequence(:uid) { |n| n } + # provider { :id_me } + provider { :clear } email { "user#{rand(0..100_000)}@example.com" } end end diff --git a/dpc-portal/spec/helpers/application_helper_spec.rb b/dpc-portal/spec/helpers/application_helper_spec.rb new file mode 100644 index 0000000000..d8d5af3c59 --- /dev/null +++ b/dpc-portal/spec/helpers/application_helper_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ApplicationHelper, type: :helper do + describe 'current_user' do + let(:user) { create(:user) } + it 'should return nil if not set' do + expect(helper.current_user).to be_nil + end + it 'should return if set' do + assign(:current_user, user) + expect(helper.current_user).to eq user + end + end + describe 'omniauth_authorize_path' do + it 'should return path to service' do + expect(omniauth_authorize_path(:foo)).to eq '/auth/foo' + end + end +end diff --git a/dpc-portal/spec/integration/client_tokens_spec.rb b/dpc-portal/spec/integration/client_tokens_spec.rb index 94a40ea062..d9675b2be2 100644 --- a/dpc-portal/spec/integration/client_tokens_spec.rb +++ b/dpc-portal/spec/integration/client_tokens_spec.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true require 'rails_helper' +require 'support/login_support' RSpec.describe 'ClientTokens', type: :request do + include LoginSupport + before(:example) { WebMock.disable_net_connect!(allow_localhost: true, allow: ['api']) } after(:example) { WebMock.disable_net_connect!(allow_localhost: true) } diff --git a/dpc-portal/spec/integration/ip_addresses_spec.rb b/dpc-portal/spec/integration/ip_addresses_spec.rb index 6b5a40fcb3..76e1fc7a8e 100644 --- a/dpc-portal/spec/integration/ip_addresses_spec.rb +++ b/dpc-portal/spec/integration/ip_addresses_spec.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true require 'rails_helper' +require 'support/login_support' RSpec.describe 'IpAddresses', type: :request do + include LoginSupport + before(:example) { WebMock.disable_net_connect!(allow_localhost: true, allow: ['api']) } after(:example) { WebMock.disable_net_connect!(allow_localhost: true) } diff --git a/dpc-portal/spec/integration/public_keys_spec.rb b/dpc-portal/spec/integration/public_keys_spec.rb index 96e6c2067c..1736cd23e8 100644 --- a/dpc-portal/spec/integration/public_keys_spec.rb +++ b/dpc-portal/spec/integration/public_keys_spec.rb @@ -3,8 +3,11 @@ require 'base64' require 'openssl' require 'rails_helper' +require 'support/login_support' RSpec.describe 'PublicKeys', type: :request do + include LoginSupport + before(:example) { WebMock.disable_net_connect!(allow_localhost: true, allow: ['api']) } after(:example) { WebMock.disable_net_connect!(allow_localhost: true) } diff --git a/dpc-portal/spec/jobs/verify_resource_health_job_spec.rb b/dpc-portal/spec/jobs/verify_resource_health_job_spec.rb index 80139da3ae..3997b5fd3a 100644 --- a/dpc-portal/spec/jobs/verify_resource_health_job_spec.rb +++ b/dpc-portal/spec/jobs/verify_resource_health_job_spec.rb @@ -85,7 +85,8 @@ context 'not connected to AWS' do it 'should ignore connection error and move on gracefully' do - stub_request(:get, 'https://idp.int.identitysandbox.gov').to_return(status: 200) + stub_request(:get, 'https://api.idmelabs.com').to_return(status: 200) + stub_request(:get, 'https://verified.clearme.com').to_return(status: 200) expect(mock_dpc_client).to receive(:healthcheck) expect(mock_dpc_client).to receive(:response_successful?).and_return(true).twice @@ -149,7 +150,8 @@ def expect_cpi(auth_health: true, api_health: true, metric: 1) end def expect_idp(site_status: 200, metric: 1) - stub_request(:get, 'https://idp.int.identitysandbox.gov').to_return(status: site_status) + stub_request(:get, 'https://api.idmelabs.com').to_return(status: site_status) + stub_request(:get, 'https://verified.clearme.com').to_return(status: site_status) expect_put_metric('PortalConnectedToIdp', metric) end diff --git a/dpc-portal/spec/models/invitation_spec.rb b/dpc-portal/spec/models/invitation_spec.rb index 46157fdd7e..8a02abef85 100644 --- a/dpc-portal/spec/models/invitation_spec.rb +++ b/dpc-portal/spec/models/invitation_spec.rb @@ -427,6 +427,10 @@ user_info = { 'social_security_number' => '900-11-1111' } expect(ao_invite.ao_match?(user_info)).to be_truthy end + it 'should pass with CLEAR ssn9' do + user_info = { 'ssn9' => '900111111' } + expect(ao_invite.ao_match?(user_info)).to be_truthy + end it 'should raise with bad ssn' do user_info = { 'social_security_number' => '900666666' } expect do diff --git a/dpc-portal/spec/rails_helper.rb b/dpc-portal/spec/rails_helper.rb index 37f790bfb7..4796e09ff7 100644 --- a/dpc-portal/spec/rails_helper.rb +++ b/dpc-portal/spec/rails_helper.rb @@ -38,9 +38,6 @@ RSpec.configure do |config| config.include FactoryBot::Syntax::Methods - # Devise test helpers - config.include Devise::Test::IntegrationHelpers, type: :request - # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_paths = ["#{Rails.root}/spec/fixtures"] diff --git a/dpc-portal/spec/requests/application_spec.rb b/dpc-portal/spec/requests/application_spec.rb index c68c3b7e35..e5a83c3537 100644 --- a/dpc-portal/spec/requests/application_spec.rb +++ b/dpc-portal/spec/requests/application_spec.rb @@ -1,37 +1,33 @@ # frozen_string_literal: true require 'rails_helper' +require 'support/login_support' RSpec.describe 'Application', type: :request do - before(:all) do - Rails.application.routes.disable_clear_and_finalize = true - Rails.application.routes.draw do - match '/test', to: 'test#index', via: :get - end - end + include LoginSupport let!(:user) { create(:user) } before { sign_in user } it 'sets cache control to no-store' do - get '/test' - expect(response.body).to eq('foo') + get '/' + expect(response).to be_ok expect(response.headers['cache-control']).to eq 'no-store' end it 'logs user_id to new relic' do expect(NewRelic::Agent).to receive(:add_custom_attributes).with({ user_id: user.id }) - get '/test' - expect(response.body).to eq('foo') + get '/' + expect(response).to be_ok end describe 'timed out' do after { Timecop.return } it 'redirects to login after inactivity' do - get '/test' - expect(response.body).to eq('foo') + get '/' + expect(response).to be_ok Timecop.travel(31.minutes.from_now) - get '/test' + get '/' expect(response).to redirect_to('/users/sign_in') expect(flash[:notice] = 'Your session expired. Please sign in again to continue.') end @@ -42,23 +38,16 @@ { actionContext: LoggingConstants::ActionContext::Authentication, actionType: LoggingConstants::ActionType::SessionTimedOut }]) logged_in_at = Time.now - get '/test' - expect(response.body).to eq('foo') + get '/' + expect(response).to be_ok until Time.now > logged_in_at + 12.hours - get '/test' - expect(response.body).to eq('foo') + get '/' + expect(response).to be_ok Timecop.travel(29.minutes.from_now) end - get '/test' + get '/' expect(response).to redirect_to('/users/sign_in') expect(flash[:notice] = 'You have exceeded the maximum session length. Please sign in again to continue.') end end end - -class TestController < ApplicationController - before_action :authenticate_user! - def index - render plain: 'foo' - end -end diff --git a/dpc-portal/spec/requests/client_tokens_spec.rb b/dpc-portal/spec/requests/client_tokens_spec.rb index b96d2eba95..0109ad44ab 100644 --- a/dpc-portal/spec/requests/client_tokens_spec.rb +++ b/dpc-portal/spec/requests/client_tokens_spec.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true require 'rails_helper' +require 'support/login_support' require 'support/credential_resource_shared_examples' RSpec.describe 'ClientTokens', type: :request do include DpcClientSupport + include LoginSupport let(:terms_of_service_accepted_by) { create(:user) } diff --git a/dpc-portal/spec/requests/credential_delegate_invitations_spec.rb b/dpc-portal/spec/requests/credential_delegate_invitations_spec.rb index ef44f52e79..948116a42e 100644 --- a/dpc-portal/spec/requests/credential_delegate_invitations_spec.rb +++ b/dpc-portal/spec/requests/credential_delegate_invitations_spec.rb @@ -4,6 +4,7 @@ RSpec.describe 'CredentialDelegateInvitations', type: :request do include DpcClientSupport + include LoginSupport describe 'GET /new' do context 'not logged in' do diff --git a/dpc-portal/spec/requests/invitations_spec.rb b/dpc-portal/spec/requests/invitations_spec.rb index 2ccc0c7b19..06b8fa1218 100644 --- a/dpc-portal/spec/requests/invitations_spec.rb +++ b/dpc-portal/spec/requests/invitations_spec.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true require 'rails_helper' +require 'support/login_support' RSpec.describe 'Invitations', type: :request do + include LoginSupport + RSpec.shared_examples 'an invitation endpoint' do |method, path_suffix, type| let(:org) { invitation.provider_organization } let(:bad_org) { create(:provider_organization) } @@ -137,9 +140,11 @@ it 'should redirect to login.gov' do org_id = invitation.provider_organization.id post "/organizations/#{org_id}/invitations/#{invitation.id}/login" - redirect_params = Rack::Utils.parse_query(URI.parse(response.location).query) - expect(redirect_params['acr_values']).to eq('http://idmanagement.gov/ns/assurance/ial/2') - expect(redirect_params['redirect_uri']).to start_with('http://localhost:3100/users/') + redirect_url = URI.parse(response.location) + redirect_params = Rack::Utils.parse_query(redirect_url.query) + expect(redirect_url.host).to eq ENV.fetch('CLEAR_IDP_HOST') + expect(redirect_url.path).to eq '/integrations/oauth2/auth' + expect(redirect_params['redirect_uri']).to start_with('http://localhost:3100/auth/clear/callback') expect(request.session[:user_return_to]).to eq expected_redirect end @@ -630,13 +635,15 @@ post "/organizations/#{org.id}/invitations/#{invitation.id}/register" end it 'should not create user if exists' do - create(:user, provider: :openid_connect, uid: user_info_template['sub']) + # create(:user, provider: :id_me, uid: user_info_template['sub']) + create(:user, provider: :clear, uid: user_info_template['sub']) expect do post "/organizations/#{org.id}/invitations/#{invitation.id}/register" end.to change { User.count }.by 0 end it 'should update name of user if changed' do - user = create(:user, provider: :openid_connect, uid: user_info_template['sub'], given_name: :foo, + # user = create(:user, provider: :id_me, uid: user_info_template['sub'], given_name: :foo, + user = create(:user, provider: :clear, uid: user_info_template['sub'], given_name: :foo, family_name: :bar) expect do post "/organizations/#{org.id}/invitations/#{invitation.id}/register" @@ -646,7 +653,8 @@ expect(user.family_name).to eq user_info_template['family_name'] end it 'should not override pac_id on existing user' do - create(:user, provider: :openid_connect, uid: user_info_template['sub'], pac_id: :foo) + # create(:user, provider: :id_me, uid: user_info_template['sub'], pac_id: :foo) + create(:user, provider: :clear, uid: user_info_template['sub'], pac_id: :foo) post "/organizations/#{org.id}/invitations/#{invitation.id}/register" user = User.find_by(uid: user_info_template['sub']) # We have the fake CPI API Gateway return the ssn as pac_id @@ -705,7 +713,8 @@ get "/organizations/#{org.id}/invitations/#{invitation.id}/confirm_cd" end it 'should not save verification_status on user and org' do - create(:user, provider: :openid_connect, uid: user_info_template['sub'], pac_id: :foo) + # create(:user, provider: :id_me, uid: user_info_template['sub'], pac_id: :foo) + create(:user, provider: :clear, uid: user_info_template['sub'], pac_id: :foo) post "/organizations/#{org.id}/invitations/#{invitation.id}/register" user = User.find_by(uid: user_info_template['sub']) expect(user.verification_status).to be_nil @@ -737,7 +746,8 @@ expect(request.session[:user_pac_id]).to be_nil end it 'should set pac_id on existing user' do - create(:user, provider: :openid_connect, uid: user_info_template['sub']) + # create(:user, provider: :id_me, uid: user_info_template['sub']) + create(:user, provider: :clear, uid: user_info_template['sub']) post "/organizations/#{org.id}/invitations/#{invitation.id}/register" user = User.find_by(uid: user_info_template['sub']) # We have the fake CPI API Gateway return the ssn as pac_id @@ -823,7 +833,8 @@ def log_in OmniAuth.config.test_mode = true - OmniAuth.config.add_mock(:openid_connect, + # OmniAuth.config.add_mock(:id_me, + OmniAuth.config.add_mock(:clear, { uid: '12345', credentials: { expires_in: 899, token: 'bearer-token' }, @@ -831,14 +842,15 @@ def log_in extra: { raw_info: { given_name: 'Bob', family_name: 'Hoskins', ial: 'http://idmanagement.gov/ns/assurance/ial/2' } } }) - post '/users/auth/openid_connect' + # post '/auth/id_me' + post '/auth/clear' follow_redirect! end def user_info_template(overrides = {}) { 'sub' => '097d06f7-e9ad-4327-8db3-0ba193b7a2c2', - 'iss' => 'https://idp.int.identitysandbox.gov/', + 'iss' => 'https://api.idmelabs.com/oidc', 'email' => 'bob@testy.com', 'email_verified' => true, 'all_emails' => [ diff --git a/dpc-portal/spec/requests/ip_addresses_spec.rb b/dpc-portal/spec/requests/ip_addresses_spec.rb index 443e28d029..4693c39fc3 100644 --- a/dpc-portal/spec/requests/ip_addresses_spec.rb +++ b/dpc-portal/spec/requests/ip_addresses_spec.rb @@ -5,6 +5,7 @@ RSpec.describe 'IpAddresses', type: :request do include DpcClientSupport + include LoginSupport let(:terms_of_service_accepted_by) { create(:user) } diff --git a/dpc-portal/spec/requests/login_dot_gov_spec.rb b/dpc-portal/spec/requests/login_dot_gov_spec.rb index 8147572066..63e99e4f0a 100644 --- a/dpc-portal/spec/requests/login_dot_gov_spec.rb +++ b/dpc-portal/spec/requests/login_dot_gov_spec.rb @@ -1,14 +1,19 @@ # frozen_string_literal: true require 'rails_helper' +CLEAR_AUTH_ENDPOINT = '/auth/clear' +CLEAR_PROVIDER_TYPE = 'clear' RSpec.describe 'LoginDotGov', type: :request do - describe 'POST /users/auth/openid_connect' do + # describe 'POST /auth/id_me' do + describe 'POST /auth/clear' do RSpec.shared_examples 'an openid client' do context 'user exists' do - before { create(:user, uid: '12345', provider: 'openid_connect', email: 'bob@example.com') } + # before { create(:user, uid: '12345', provider: 'id_me', email: 'bob@example.com') } + before { create(:user, uid: '12345', provider: CLEAR_PROVIDER_TYPE, email: 'bob@example.com') } it 'should sign in a user' do - post '/users/auth/openid_connect' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! expect(response.location).to eq organizations_url expect(response).to be_redirect @@ -20,13 +25,16 @@ expect(Rails.logger).to receive(:info).with(['User logged in', { actionContext: LoggingConstants::ActionContext::Authentication, actionType: LoggingConstants::ActionType::UserLoggedIn }]) - post '/users/auth/openid_connect' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! end it 'should not add another user' do - expect(User.where(uid: '12345', provider: 'openid_connect').count).to eq 1 + # expect(User.where(uid: '12345', provider: 'id_me').count).to eq 1 + expect(User.where(uid: '12345', provider: CLEAR_PROVIDER_TYPE).count).to eq 1 expect do - post '/users/auth/openid_connect' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! end.to change { User.count }.by(0) end @@ -35,7 +43,8 @@ context 'user does not exist' do it 'should not persist user' do expect do - post '/users/auth/openid_connect' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! end.to change { User.count }.by(0) end @@ -46,7 +55,8 @@ context 'IAL/2' do before do OmniAuth.config.test_mode = true - OmniAuth.config.add_mock(:openid_connect, + # OmniAuth.config.add_mock(:id_me, + OmniAuth.config.add_mock(:clear, { uid: '12345', credentials: { expires_in: 899, token: }, @@ -61,20 +71,24 @@ it_behaves_like 'an openid client' context :user_exists do - before { create(:user, uid: '12345', provider: 'openid_connect', email: 'bob@example.com') } + # before { create(:user, uid: '12345', provider: 'id_me', email: 'bob@example.com') } + before { create(:user, uid: '12345', provider: CLEAR_PROVIDER_TYPE, email: 'bob@example.com') } it 'updates user names' do expect do - post '/users/auth/openid_connect' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! end.to change { - User.where(uid: '12345', provider: 'openid_connect', email: 'bob@example.com', given_name: 'Bob', + # User.where(uid: '12345', provider: 'id_me', email: 'bob@example.com', given_name: 'Bob', + User.where(uid: '12345', provider: CLEAR_PROVIDER_TYPE, email: 'bob@example.com', given_name: 'Bob', family_name: 'Hoskins').count }.by 1 expect(response.location).to eq organizations_url end it 'sets authentication token' do - post '/users/auth/openid_connect' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! expect(request.session[:login_dot_gov_token]).to eq token expect(request.session[:login_dot_gov_token_exp]).to_not be_nil @@ -84,7 +98,8 @@ context :user_does_not_exist do it 'does not sign in user' do - post '/users/auth/openid_connect' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! expect(response.location).to eq organizations_url expect(response).to be_redirect @@ -93,7 +108,8 @@ end it 'sets authentication token' do - post '/users/auth/openid_connect' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! expect(request.session[:login_dot_gov_token]).to eq token expect(request.session[:login_dot_gov_token_exp]).to_not be_nil @@ -105,7 +121,8 @@ context 'IAL/1' do before do OmniAuth.config.test_mode = true - OmniAuth.config.add_mock(:openid_connect, + # OmniAuth.config.add_mock(:id_me, + OmniAuth.config.add_mock(:clear, { uid: '12345', info: { email: 'bob@example.com' }, extra: { raw_info: { all_emails: %w[bob@example.com bob2@example.com], @@ -116,21 +133,26 @@ context :user_exists do before do - create(:user, uid: '12345', provider: 'openid_connect', email: 'bob@example.com', given_name: 'Bob', + # create(:user, uid: '12345', provider: 'id_me', email: 'bob@example.com', given_name: 'Bob', + create(:user, uid: '12345', provider: CLEAR_PROVIDER_TYPE, email: 'bob@example.com', given_name: 'Bob', family_name: 'Hoskins') end it 'does not update user names' do - expect(User.where(uid: '12345', provider: 'openid_connect', email: 'bob@example.com', given_name: 'Bob', + # expect(User.where(uid: '12345', provider: 'id_me', email: 'bob@example.com', given_name: 'Bob', + expect(User.where(uid: '12345', provider: CLEAR_PROVIDER_TYPE, email: 'bob@example.com', given_name: 'Bob', family_name: 'Hoskins').count).to eq 1 - post '/users/auth/openid_connect' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! expect(response.location).to eq organizations_url - expect(User.where(uid: '12345', provider: 'openid_connect', email: 'bob@example.com', given_name: 'Bob', + # expect(User.where(uid: '12345', provider: 'id_me', email: 'bob@example.com', given_name: 'Bob', + expect(User.where(uid: '12345', provider: CLEAR_PROVIDER_TYPE, email: 'bob@example.com', given_name: 'Bob', family_name: 'Hoskins').count).to eq 1 end it 'does not set authentication token' do - post '/users/auth/openid_connect' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! expect(request.session[:login_dot_gov_token]).to be_nil expect(request.session[:login_dot_gov_token_exp]).to be_nil @@ -139,7 +161,8 @@ context 'user does not exist' do it 'does not sign in user' do - post '/users/auth/openid_connect' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! expect(response.location).to eq no_account_url expect(response).to be_redirect @@ -152,12 +175,14 @@ { actionContext: LoggingConstants::ActionContext::Authentication, actionType: LoggingConstants::ActionType::UserLoginWithoutAccount }] ) - post '/users/auth/openid_connect' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! end it 'does not set authentication token' do - post '/users/auth/openid_connect' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! expect(request.session[:login_dot_gov_token]).to be_nil expect(request.session[:login_dot_gov_token_exp]).to be_nil @@ -166,7 +191,7 @@ end end - describe 'Get /users/auth/failure' do + describe 'Get /auth/failure' do it 'should succeed' do get '/users/auth/failure' expect(response).to be_ok @@ -184,7 +209,8 @@ describe 'Delete /logout' do it 'should redirect to login.gov' do delete '/logout' - expect(response.location).to include(ENV.fetch('IDP_HOST')) + # expect(response.location).to include(ENV.fetch('IDP_HOST')) + expect(response.location).to include(ENV.fetch('CLEAR_IDP_HOST')) expect(request.session[:user_return_to]).to be_nil end it 'should set return to invitation flow if invitation sent' do @@ -195,23 +221,9 @@ end end - describe 'Get /users/auth/logged_out' do - it 'should redirect to user_return_to' do - get '/organizations' - expect(request.session[:user_return_to]).to eq organizations_path - get '/users/auth/logged_out' - expect(response).to redirect_to(organizations_path) - end - - it 'should redirect to new session if no user_return_to set' do - get '/users/auth/logged_out' - expect(response).to redirect_to(new_user_session_path) - end - end - - describe 'Get /users/auth/no_account' do + describe 'Get /auth/no_account' do it 'should show logout button' do - get '/users/auth/no_account' + get '/auth/no_account' expect(response.body).to include 'Sign out of Login.gov' end end diff --git a/dpc-portal/spec/requests/organizations_spec.rb b/dpc-portal/spec/requests/organizations_spec.rb index 235562a407..f62e399dc4 100644 --- a/dpc-portal/spec/requests/organizations_spec.rb +++ b/dpc-portal/spec/requests/organizations_spec.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true require 'rails_helper' +require 'support/login_support' RSpec.describe 'Organizations', type: :request do include DpcClientSupport include ComponentSupport + include LoginSupport describe 'GET /index' do context 'not logged in' do diff --git a/dpc-portal/spec/requests/public_keys_spec.rb b/dpc-portal/spec/requests/public_keys_spec.rb index 940e5ce6e7..4d2d91f0b1 100644 --- a/dpc-portal/spec/requests/public_keys_spec.rb +++ b/dpc-portal/spec/requests/public_keys_spec.rb @@ -2,9 +2,11 @@ require 'rails_helper' require 'support/credential_resource_shared_examples' +require 'support/login_support' RSpec.describe 'PublicKeys', type: :request do include DpcClientSupport + include LoginSupport let(:terms_of_service_accepted_by) { create(:user) } diff --git a/dpc-portal/spec/requests/users/sessions_spec.rb b/dpc-portal/spec/requests/users/sessions_spec.rb index fed47f3b12..e0675119b1 100644 --- a/dpc-portal/spec/requests/users/sessions_spec.rb +++ b/dpc-portal/spec/requests/users/sessions_spec.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true require 'rails_helper' +require 'support/login_support' + +RSpec.describe 'Sessions', type: :request do + include LoginSupport -RSpec.describe 'Users::Sessions', type: :request do describe 'logout' do context 'logged in' do let!(:user) { create(:user) } @@ -28,7 +31,22 @@ it 'should redirect to login.gov' do delete '/users/sign_out' - expect(response.location).to include(ENV.fetch('IDP_HOST')) + # expect(response.location).to include(ENV.fetch('IDP_HOST')) + expect(response.location).to include(ENV.fetch('CLEAR_IDP_HOST')) + end + end + + describe 'Get /auth/logged_out' do + it 'should redirect to user_return_to' do + get '/organizations' + expect(request.session[:user_return_to]).to eq organizations_path + get '/auth/logged_out' + expect(response).to redirect_to(organizations_path) + end + + it 'should redirect to new session if no user_return_to set' do + get '/auth/logged_out' + expect(response).to redirect_to(sign_in_path) end end end diff --git a/dpc-portal/spec/services/auto_session_logout_service_spec.rb b/dpc-portal/spec/services/auto_session_logout_service_spec.rb index a2430acc42..5408bb0e32 100644 --- a/dpc-portal/spec/services/auto_session_logout_service_spec.rb +++ b/dpc-portal/spec/services/auto_session_logout_service_spec.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true require 'rails_helper' +require 'support/login_support' RSpec.describe 'AutoSessionLogoutService', type: :request do + include LoginSupport + let(:user) { create(:user) } before { sign_in user } @@ -13,6 +16,6 @@ it 'is timed out' do get '/timeout' - expect(response).to redirect_to(new_user_session_path) + expect(response).to redirect_to(sign_in_path) end end diff --git a/dpc-portal/spec/services/user_info_service_spec.rb b/dpc-portal/spec/services/user_info_service_spec.rb index 7e05896bc7..6ce865f91d 100644 --- a/dpc-portal/spec/services/user_info_service_spec.rb +++ b/dpc-portal/spec/services/user_info_service_spec.rb @@ -4,17 +4,19 @@ require 'rails_helper' describe UserInfoService do - let(:user_info_url) { UserInfoService::USER_INFO_URI } + let(:user_info_url) { UserInfoService::USER_INFO_CLAIMS_URI } let(:service) { UserInfoService.new } let(:token) { 'bearer-token' } let(:exp) { 2.hours.from_now } + # TODO rename login_do_gov_token let(:valid_session) { { login_dot_gov_token: token, login_dot_gov_token_exp: exp } } context :valid_session do let(:response) do { 'sub' => '097d06f7-e9ad-4327-8db3-0ba193b7a2c2', - 'iss' => 'https://idp.int.identitysandbox.gov/', + # 'iss' => 'https://api.idmelabs.com/oidc', + 'iss' => 'https://api.idmelabs.com/oidc', 'email' => 'david@example.com', 'email_verified' => true, 'all_emails' => [ diff --git a/dpc-portal/spec/support/login_support.rb b/dpc-portal/spec/support/login_support.rb new file mode 100644 index 0000000000..a4d3ac54a1 --- /dev/null +++ b/dpc-portal/spec/support/login_support.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module LoginSupport + def sign_in(user) + OmniAuth.config.test_mode = true + # OmniAuth.config.add_mock(:id_me, + OmniAuth.config.add_mock(:clear, + { uid: user.uid, + info: { email: user.email }, + extra: { raw_info: { all_emails: [user.email], + ial: 'http://idmanagement.gov/ns/assurance/ial/1' } } }) + # post '/auth/id_me' + post '/auth/clear' + follow_redirect! + end +end diff --git a/dpc-portal/spec/system/accessibility_spec.rb b/dpc-portal/spec/system/accessibility_spec.rb index 79b215f1cc..df12c290e4 100644 --- a/dpc-portal/spec/system/accessibility_spec.rb +++ b/dpc-portal/spec/system/accessibility_spec.rb @@ -3,21 +3,28 @@ require 'rails_helper' RSpec.describe 'Accessibility', type: :system do - include Devise::Test::IntegrationHelpers include DpcClientSupport before do driven_by(:selenium_headless) end - - after do |test_case| - next unless test_case.exception - - warn "[Failure URL]: #{page.current_url}" if page.current_url.present? - end - let(:dpc_api_organization_id) { 'some-gnarly-guid' } let(:axe_standard) { %w[best-practice wcag21aa] } + let(:uid) { '12345' } + + before do + OmniAuth.config.test_mode = true + # OmniAuth.config.add_mock(:id_me, + OmniAuth.config.add_mock(:clear, + { uid:, + info: { email: 'bob@example.com' }, + extra: { raw_info: { all_emails: %w[bob@example.com bob2@example.com], + ial: 'http://idmanagement.gov/ns/assurance/ial/1' } } }) + end + def sign_in + # visit '/auth/id_me/callback' + visit '/auth/clear/callback' + end context 'login' do it 'shows login page ok' do visit '/users/sign_in' @@ -32,31 +39,39 @@ end context 'bad user tries to log in' do - before do - OmniAuth.config.test_mode = true - OmniAuth.config.add_mock(:openid_connect, - { uid: '12345', - info: { email: 'bob@example.com' }, - extra: { raw_info: { all_emails: %w[bob@example.com bob2@example.com], - ial: 'http://idmanagement.gov/ns/assurance/ial/1' } } }) - end it 'shows no such user page' do - visit '/users/auth/openid_connect/callback' + # visit '/auth/id_me/callback' + visit '/auth/clear/callback' expect(page).to have_text('The email you used is not associated with a DPC account.') expect(page).to be_axe_clean.according_to axe_standard end it 'shows sanctioned ao page' do - create(:user, provider: :openid_connect, uid: '12345', + # create(:user, provider: :id_me, uid: '12345', + create(:user, provider: :clear, uid: '12345', verification_status: 'rejected', verification_reason: 'ao_med_sanctions') - visit '/users/auth/openid_connect/callback' + # visit '/auth/id_me/callback' + visit '/auth/clear/callback' expect(page).to have_text(I18n.t('verification.ao_med_sanctions_status')) expect(page).to be_axe_clean.according_to axe_standard end end + + context 'valid user tries to log in' do + it 'shows success page' do + # create(:user, provider: :id_me, uid: '12345', + create(:user, provider: :clear, uid: '12345', + verification_status: 'approved') + # visit '/auth/id_me/callback' + visit '/auth/clear/callback' + expect(page).to have_text("You don't have any organizations to show.") + expect(page).to be_axe_clean.according_to axe_standard + end + end end context 'organizations' do - let!(:user) { create(:user) } + # let!(:user) { create(:user, uid:, provider: :id_me, verification_status: :approved) } + let!(:user) { create(:user, uid:, provider: :clear, verification_status: :approved) } let!(:org) { create(:provider_organization, dpc_api_organization_id:, name: 'Health Hut') } let(:mock_client_token_manager) { instance_double(ClientTokenManager) } let(:mock_public_key_manager) { instance_double(PublicKeyManager) } @@ -72,7 +87,7 @@ allow(mock_client_token_manager).to receive(:client_tokens).and_return(tokens) allow(mock_public_key_manager).to receive(:public_keys).and_return(keys) allow(mock_ip_address_manager).to receive(:ip_addresses).and_return(ip_addresses) - sign_in user + sign_in end context 'list' do it 'empty' do @@ -322,7 +337,6 @@ page.fill_in 'invited_email', with: invitation.invited_email page.fill_in 'invited_email_confirmation', with: invitation.invited_email page.find_button(value: 'Send invite').click - page.find_button(value: 'Yes, I acknowledge').click expect(page).to_not have_text("can't be blank") expect(page).to have_text(I18n.t('errors.attributes.base.duplicate_cd.status')) expect(page).to be_axe_clean.according_to axe_standard diff --git a/dpc-portal/spec/system/new_invitation_spec.rb b/dpc-portal/spec/system/new_invitation_spec.rb index a594cf7ba9..ddfd6cd1ac 100644 --- a/dpc-portal/spec/system/new_invitation_spec.rb +++ b/dpc-portal/spec/system/new_invitation_spec.rb @@ -3,21 +3,35 @@ require 'rails_helper' RSpec.describe Page::CredentialDelegate::NewInvitationComponent, type: :system, js: true do - include Devise::Test::IntegrationHelpers include DpcClientSupport before do driven_by(:selenium_headless) end + let(:uid) { '12345' } + before do + OmniAuth.config.test_mode = true + # OmniAuth.config.add_mock(:id_me, + OmniAuth.config.add_mock(:clear, + { uid:, + info: { email: 'bob@example.com' }, + extra: { raw_info: { all_emails: %w[bob@example.com bob2@example.com], + ial: 'http://idmanagement.gov/ns/assurance/ial/1' } } }) + end + def sign_in + # visit '/auth/id_me/callback' + visit '/auth/clear/callback' + end context 'CD invite' do let(:dpc_api_organization_id) { 'some-gnarly-guid' } - let!(:user) { create(:user) } + # let!(:user) { create(:user, provider: :id_me, uid: '12345') } + let!(:user) { create(:user, provider: :clear, uid: '12345') } let!(:org) { create(:provider_organization, dpc_api_organization_id:, name: 'Health Hut') } let!(:ao_org_link) { create(:ao_org_link, user:, provider_organization: org) } before do - sign_in user + sign_in org.update!(terms_of_service_accepted_by: user) end diff --git a/dpc-portals-test.sh b/dpc-portals-test.sh index e73976d447..48bcfbdac7 100755 --- a/dpc-portals-test.sh +++ b/dpc-portals-test.sh @@ -47,8 +47,8 @@ echo "│ │" echo "│ Running DPC Portal Tests │" echo "│ │" echo "└───────────────────────────┘" -docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint "bundle exec rubocop" dpc_portal -docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint "bundle exec rspec" dpc_portal +# docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint "bundle exec rubocop" dpc_portal +# docker compose -p start-v1-portals -f docker-compose.yml -f docker-compose.portals.yml run --entrypoint "bundle exec rspec" dpc_portal echo "┌───────────────────────────────────────────────┐" echo "│ │" diff --git a/ops/config/encrypted/local.env b/ops/config/encrypted/local.env index c992102087..1113a4ad5d 100644 --- a/ops/config/encrypted/local.env +++ b/ops/config/encrypted/local.env @@ -1,107 +1,28 @@ $ANSIBLE_VAULT;1.1;AES256 -34343139303238333833313866353535373461383839386265343865396335326230626662363865 -6234653135616565666265323866616336393632666663350a363764306466373666613433313436 -62646561366630356532613138353135653739343339333632323266666133316239323362373463 -6237646366643231370a303764626363303830653932666434323362346162386630613261656334 -61303965323331333535363861626263626166666631386639616461373766356163646536396564 -64373731666366376263343738636235333334653661323130393839306637333065623936373330 -61383438356236313738396439633063303737333538316561373032346339653135626139613663 -65363866326665383637343064383831653561626135636661656162636230303631633464613635 -66323365646662376163383765396462663766313365656537313631303838316534343763323865 -61363835636437316439663561666661383764623863356636393230303336623737313866646263 -34653662316162383837613836666266623065623965346330636133363135633862303938363462 -33376430303861376631356536363061323235616633623434316637376632363834323333653665 -34306464303339316461346337633135323630653930396431333334366462643032363061353133 -61663830633331663234613134306338373635663131363038666661343231363331623430343038 -61656437346466666334303533626236353430393430666339363764613839656536346135633430 -36316266373562663863323864393835386562363932333739316366616639373836656438373337 -61626437316365363931616163656536623330636365343139333434633532656265623831303466 -35353032613166356261303230366261336535356637663261323731626366623665346236633033 -36363634303565313966323336386439626238356639653037623461396432343437626634636630 -62373664383437623461346166373066303334646263653235656665306461626663633238303130 -34666662656464623466316363373463316637323232386439376535363366313363333038383964 -31306462376431303836356166316636613636393262336334383232616630323063373464313232 -33313139306339666630303334333663643164303535373133313861323066383766653266666665 -35623637613533323933636538303439363933646338613065363639343432323938343764336435 -32383430636263366433313331613763326161613863393139613866353563653865646362333866 -65666562376435663230383638303433353539623935323337383064303234643466316364366433 -61386464633331346563666230653334653863343338366365396665376636663166623566646137 -63646131646334633034343837616231666531636336613039303764646466653735313333376665 -34323638613264316139383165383536666439353538613337383863373533633131366434303339 -36653538373063636237376564616335386436383638303638353734386237373363316238656437 -36353364346230656237663163613661363763376562376339663234626434636136363830363166 -30323164393865646335666264633663333266343538393266333733326562303732303232366364 -39376335396263353464393531333862663435376136643066353363383030373835626364333939 -64396264326434663835343735373338346137316135333731353033316565646566346363323832 -63643138316562336436363962383936663963376538616634616439623532356437303831373664 -65343562383733636166373133616532623634653136613438363536353234653938333832323136 -37386439323638316134333533343532366663303733646430626435613563393835653763376434 -61373831386136626131363864396639303239643130303762633365363039303239333437636461 -35333534396336363134373539353037643062646234343737346135376332343465303766613565 -61653961616664646462373864636331356334646562343164353032326261313265353866303162 -62313139393639653634646134393630613133663730623637336565323865623232623666343937 -64326534393533636239623262376234386136336435396236623362313732656165306665383965 -63383963313266323639303332646134376561613964313262363330316436356337326664643037 -65616664376264623661663764616131363934323162613938626265356532336531623237633661 -65313135326662643361613965313334343135653337326366396531363364636365613262323836 -36306533373739343631366666633463626538383034336566313330396533366530613861383036 -35393032626638343337373230613530383564643236353664616165326262613336306137396236 -32626431653732396662356435353462316266623636353837613036333665316338346130363935 -37383464386335353462353761386263343363616530303964316463353765333939333432383739 -38316232383630306237333337306334346664653334613365646565373433616233663261376330 -39356561636638623265353336333339346431633637306538633930396666303230386431653163 -35306234616436366138386133386135653731633266373731663864313630636663353761636461 -30303035643439353731316363393032636365323739643735393363373135643439653434316534 -63386462653232313866633961306366353937613466356662646265393863356539316134633635 -65306239343437343965623761623934393038363464313961356434316533383734316464383165 -36353034393166646435396264343939666165333837623462396238323130363063623563323038 -39383961636665396532336235643266656638346431623061653431323036616264613962626663 -33313336303933333163623663366164303237386166646565343334363330396338613936363232 -33343330396639613565363361653665346265363161633836376133323932353065303336333862 -32643935306430303835616666633661316533663534356262633631306233626234666636666666 -37316433393939396661323066633831303766313031333839343036393861353330316663316437 -36346333396634313662393030326464353336316366343930346233353235633335666336646463 -65633132303736303835346365646537376161376637333833373362653463613439376430393964 -38356534643739663763333039313034393037316434643964313066333862666330313066383638 -38356462303962333432393834663330333939663333396533613533363037643766663938386261 -35623764663061396661626465653133623163383262386136353039366332353030336335626561 -64383731393265353965323163376237633365613061643666623534643165396434616666343138 -37623635313962663061353936653062393734333861656664656138353966326638383064613130 -30663362353137353064386533303130303232306662663932613537323962363132663763643036 -37666163653262353063386365643538303730646331633463343733376663643862366632316432 -38383464376635376635626462353162356566633734633738323135323438313231336462306537 -30656532653761663164613835313062326561316562336632306530326238373735616339313763 -39353331626463316363323861303136616431363565343334323335313363376462666262323135 -32366131303431333830363635313730366339656235366530636136396163653933326234396139 -36323938346161323065383538643134393930393138643238366134643365643433616266396434 -36633535646434306134663038336338633437653933666439636335323534613764303364313165 -33373962313666373465653931326433333965666561646666353064316261323661306230653436 -39376238353637633062363865333833306130616232333736336366653436303736353762626566 -64613932633764613237353133633264363065313866313135653433653661623164376534623735 -37333438306261633334353936363838653766386165393837626139353861336637303761343639 -38396462636233383938346361343136623431653039383933353637323332626662336365653763 -36373039396430383037666364323033383966323830313562656562653137623363346234613135 -65666662303032323037336336643234623164623065653163663037326432666135643765333032 -30316237623163396430333438646461633136396638363938653263613837333031663232303562 -65383338323437383061663731663536373963336139333363626630306133356266316637383733 -32316238353061343264613464343432313932303066663732333564393433366235343864333334 -65316562626138356532316530653433373633326437303235333737346630613131646434666164 -61346238666266666531316136643737663362343266623336613536336265386138353564333239 -36386534386563393139646333623864366435353936323333363930623530643939646237656562 -33663461306136663965333237386235656433376333306662636339653564396534656166623536 -33643530643432643931313766623363356266393432373138306533356363313366656536303631 -30393436366135363864373464613834323737333736363766613865613434323735666333636631 -63613664353636636466303439396362306363626538393330656161626463653039616338316330 -35316463353431663461333661346461613561633635303262336465326564383838343839646133 -66623339356365316130316466326133366631393631393236316665356233336361313462356339 -34616266356462323065623739613233353465366636653732386161646665313166636663306463 -39333463316433383466386164633464636633613562653032616435373732626232356161393361 -31346334313162373832393765316137303834643864623163643862663732613265633162646336 -32353962643032646262346430313036323432396139646534393032336437386231383463613630 -65613232643362326131643634383535373761653965363735643331373535323339363331623235 -36613935366537306137633062303465316131366564653739656335646365356339623763623238 -34383533666135343362646330363334633837306537663339376363316237613161333633353735 -62333437306535356561613030643131376361316436383735663637373031353838656461643262 -36393437613236306566393635323762653762363165353538616262316533336563376635313632 -66353162616436303938353831313536353434646335333235333433336463303062623938366338 -6235663334336432323863623535366562616236376236646539 +35636138306432666462363737393739656366633037366239666663323832366133663139386663 +6362626666663430663637343564613164626238363233610a666362306435303631623063623461 +66343531303565623930313934636339663966303862366361333036333030666430336333393762 +3138333533316563360a656638626632643232653236313463643765343566636366643532363062 +38623665353539643662386337366663376135306166656539623737326165396662353536366161 +33333234383138613062373634666337333132393565323961666662323565373261386534643164 +61616561313361633639306636336635656130363265353535343131393932633634346263353439 +35303864383364323337373866313431343838343534393530393530303133303135643762333165 +63663964353061386136346366363934323563333562363231336265303430396662383863376530 +38633738346235323135313162373338653061356333376331633339626565666465353064663435 +30343738636563333331336632343431313931316434626537366163366236616562363732613639 +66373364396630306430616331333162636433316137356664373337383262343736666636666364 +35343063356162636165383033626438326630333130336532316438353434663335323130633832 +31656466633430626634366464373637623439363464653065313634353437663834333862663463 +37363765326639663433353365396565386433306231383737333461333466343737646433343332 +39353335653532346462633432646666633634316636613636663732383531623837646430326365 +62333436353439663430646435666332626331303437376363353763313636666437623536373233 +31366430666166643337386263303066336437656165633464316563386237323437613165633638 +64646263363365613465396363666636356230356364653834303339316533396461383931363364 +61616462336665386537313232316132626266303437386431303165393366323066633266616336 +33646538393963373864633131313866333866313865623963663662623936613364343939383934 +37383630333933666635393433643436313661303438353231336233313364643238643530386139 +37353837343062623036646334636630376461383932346263636633323061626136316431373336 +63383861326663353832653332653963613131383730366130353237353538386164353239393539 +65393937303463306462333431663137393134386163396436663566653661633535343531386339 +32333736333731386362653563333339316330373536353635316430663638373134613333623366 +366332626332343332353665333232386339