From e33a3b980c3e0215b50b5cf737061032f6e639ac Mon Sep 17 00:00:00 2001 From: Luke Short Date: Tue, 12 May 2026 11:16:39 -0700 Subject: [PATCH 01/23] PoC for using CLEAR integration --- docker-compose.portals.yml | 5 +- .../app/controllers/application_controller.rb | 3 +- dpc-portal/config/initializers/omniauth.rb | 18 ++++--- dpc-portal/config/routes.rb | 1 + ops/config/encrypted/local.env | 48 +++++++++++-------- 5 files changed, 45 insertions(+), 30 deletions(-) diff --git a/docker-compose.portals.yml b/docker-compose.portals.yml index 325628835..1a585aa4d 100644 --- a/docker-compose.portals.yml +++ b/docker-compose.portals.yml @@ -142,8 +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=api.idmelabs.com - - IDP_CLIENT_ID=925bb2985ccf623114359caa76228919 + # - 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/app/controllers/application_controller.rb b/dpc-portal/app/controllers/application_controller.rb index 9b8e2e1cc..46bc671ed 100644 --- a/dpc-portal/app/controllers/application_controller.rb +++ b/dpc-portal/app/controllers/application_controller.rb @@ -3,7 +3,8 @@ # Parent class of all controllers class ApplicationController < ActionController::Base IDP_HOST = ENV.fetch('IDP_HOST') - IDP_CLIENT_ID = ENV.fetch('IDP_CLIENT_ID') + # IDP_CLIENT_ID = ENV.fetch('IDP_CLIENT_ID') + IDP_CLIENT_ID = ENV.fetch('CLEAR_IDP_CLIENT_ID') before_action :check_session_length before_action :set_current_request_attributes diff --git a/dpc-portal/config/initializers/omniauth.rb b/dpc-portal/config/initializers/omniauth.rb index e65ce93b5..43cbf8285 100644 --- a/dpc-portal/config/initializers/omniauth.rb +++ b/dpc-portal/config/initializers/omniauth.rb @@ -8,9 +8,12 @@ Rails.application.config.middleware.use OmniAuth::Builder do OmniAuth.config.logger = Rails.logger - idp_host = ENV.fetch('IDP_HOST', 'api.idmelabs.com') - client_id = ENV.fetch('IDP_CLIENT_ID', '925bb2985ccf623114359caa76228919') - client_secret = ENV['IDP_CLIENT_SECRET'] + # idp_host = ENV.fetch('IDP_HOST', 'api.idmelabs.com') + idp_host = ENV.fetch('CLEAR_IDP_HOST') + # client_id = ENV.fetch('IDP_CLIENT_ID', '925bb2985ccf623114359caa76228919') + client_id = ENV.fetch('CLEAR_IDP_CLIENT_ID') + # client_secret = ENV['IDP_CLIENT_SECRET'] + client_secret = ENV['CLEAR_IDP_CLIENT_SECRET'] provider :openid_connect, { name: :id_me, issuer: "https://#{idp_host}/oidc", @@ -23,9 +26,12 @@ 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", + # redirect_uri: "#{my_protocol_host}/auth/id_me/callback", + redirect_uri: "#{my_protocol_host}/auth/clear/callback", + # authorization_endpoint: "https://#{idp_host}/oauth/authorize", + authorization_endpoint: "https://#{idp_host}/integrations/oauth2/auth", + # token_endpoint: "https://#{idp_host}/oauth/token", + token_endpoint: "https://#{idp_host}/integrations/oauth2/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" diff --git a/dpc-portal/config/routes.rb b/dpc-portal/config/routes.rb index 3f45ccfcd..e9c5c97e6 100644 --- a/dpc-portal/config/routes.rb +++ b/dpc-portal/config/routes.rb @@ -15,6 +15,7 @@ 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/ops/config/encrypted/local.env b/ops/config/encrypted/local.env index ef52ecaf7..1113a4ad5 100644 --- a/ops/config/encrypted/local.env +++ b/ops/config/encrypted/local.env @@ -1,22 +1,28 @@ $ANSIBLE_VAULT;1.1;AES256 -38383764616664386236663635363232383035373933363830626633303734313934353634343237 -3131316235376139303537396533656165636431316239320a616162313039613035613531343233 -37356164393738643837663462373034623639366165343838666433613863383234313033633137 -3035326662306533370a646536666437383234343765643732666636306662333237306133393631 -39656162316532323939366639306434323934646230356338326437396434643433323639356136 -36386238323333623137396637303332303835326266663937366532373339646530306136323034 -64343735343635626562323437333261636532396534323735313035643334343634653630393562 -31366436656437633736333336653531633138666366636265303466643132323538333662653565 -34663935323138653839663337333062346139376266373166353563393233336532343561643639 -66346438636161396532373465653734333866376362373638333161306638323838613463363637 -30343066326562396430346162386531356163323239393265643532653338313236393532663130 -65333736663338336535323838303866346261633737386161663031306266663732613230323634 -65393463353332626239303330616538336135326361373163393332366437653333316162613965 -62663266353235393261333831633662323364336430656330376566653562633033303633303731 -37633833343662343866623336353939613230633930313236346563626432306133633637666663 -31326132366161353935333535396238383664313333303063356339396661666334353966633631 -33346634373134653366333035336661336131316633376362383639623131343363363333653730 -66343332396432336437393735346632653961356364653966343061663331633732303935343936 -63643138623035336462303739376361653930326463383366393131363964613565623063396664 -34623863633561313066653566363430383837343961343639393432326666343337613661323062 -66643636316235653365353736666432643431643235613934356439323037306531 +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 From ac3bb9517299165c3a3d022eec81d61ba640bd48 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Tue, 12 May 2026 11:29:52 -0700 Subject: [PATCH 02/23] comment out audit check to test portal changes --- dpc-admin/Dockerfile | 4 ++-- dpc-portal/Dockerfile | 4 ++-- dpc-web/Dockerfile | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dpc-admin/Dockerfile b/dpc-admin/Dockerfile index 858602708..645391293 100644 --- a/dpc-admin/Dockerfile +++ b/dpc-admin/Dockerfile @@ -28,8 +28,8 @@ RUN gem install bundler --no-document && \ bundle install && \ npm install -# Run bundler audit -RUN bundle exec bundle audit update && bundle exec bundle audit check +# # Run bundler audit +# RUN bundle exec bundle audit update && bundle exec bundle audit check # Copy the code, test the app, and build the assets pipeline COPY /dpc-admin /dpc-admin diff --git a/dpc-portal/Dockerfile b/dpc-portal/Dockerfile index 830053311..817918fc8 100644 --- a/dpc-portal/Dockerfile +++ b/dpc-portal/Dockerfile @@ -33,8 +33,8 @@ RUN gem install bundler --no-document && \ # Install foreman RUN gem install foreman -# Run bundler audit -RUN bundle exec bundle audit update && bundle exec bundle audit check +# # Run bundler audit +# RUN bundle exec bundle audit update && bundle exec bundle audit check # Copy the code, test the app, and build the assets pipeline COPY /dpc-portal /dpc-portal diff --git a/dpc-web/Dockerfile b/dpc-web/Dockerfile index 04d540b6a..3017b716d 100644 --- a/dpc-web/Dockerfile +++ b/dpc-web/Dockerfile @@ -27,8 +27,8 @@ RUN gem install bundler --no-document && \ bundle install && \ npm install -# Run bundler audit -RUN bundle exec bundle audit update && bundle exec bundle audit check +# # Run bundler audit +# RUN bundle exec bundle audit update && bundle exec bundle audit check # Copy the code, test the app, and build the assets pipeline COPY /dpc-web /dpc-web From 343d7b85b7330dadac1329baad5329cea3454d87 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Tue, 12 May 2026 11:41:53 -0700 Subject: [PATCH 03/23] pulled in main, keep bundle audit check --- dpc-admin/Dockerfile | 4 ++-- dpc-portal/Dockerfile | 4 ++-- dpc-web/Dockerfile | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dpc-admin/Dockerfile b/dpc-admin/Dockerfile index 645391293..858602708 100644 --- a/dpc-admin/Dockerfile +++ b/dpc-admin/Dockerfile @@ -28,8 +28,8 @@ RUN gem install bundler --no-document && \ bundle install && \ npm install -# # Run bundler audit -# RUN bundle exec bundle audit update && bundle exec bundle audit check +# Run bundler audit +RUN bundle exec bundle audit update && bundle exec bundle audit check # Copy the code, test the app, and build the assets pipeline COPY /dpc-admin /dpc-admin diff --git a/dpc-portal/Dockerfile b/dpc-portal/Dockerfile index 817918fc8..830053311 100644 --- a/dpc-portal/Dockerfile +++ b/dpc-portal/Dockerfile @@ -33,8 +33,8 @@ RUN gem install bundler --no-document && \ # Install foreman RUN gem install foreman -# # Run bundler audit -# RUN bundle exec bundle audit update && bundle exec bundle audit check +# Run bundler audit +RUN bundle exec bundle audit update && bundle exec bundle audit check # Copy the code, test the app, and build the assets pipeline COPY /dpc-portal /dpc-portal diff --git a/dpc-web/Dockerfile b/dpc-web/Dockerfile index 3017b716d..04d540b6a 100644 --- a/dpc-web/Dockerfile +++ b/dpc-web/Dockerfile @@ -27,8 +27,8 @@ RUN gem install bundler --no-document && \ bundle install && \ npm install -# # Run bundler audit -# RUN bundle exec bundle audit update && bundle exec bundle audit check +# Run bundler audit +RUN bundle exec bundle audit update && bundle exec bundle audit check # Copy the code, test the app, and build the assets pipeline COPY /dpc-web /dpc-web From 07ffed742d43f64c6e1cab6f274e763e1c11f93c Mon Sep 17 00:00:00 2001 From: Luke Short Date: Tue, 12 May 2026 13:57:52 -0700 Subject: [PATCH 04/23] WIP userinfo updates --- .../app/controllers/application_controller.rb | 11 ++++++----- .../app/controllers/invitations_controller.rb | 12 +++++++----- .../app/controllers/login_dot_gov_controller.rb | 17 +++++++++++++++++ .../app/jobs/verify_resource_health_job.rb | 2 ++ dpc-portal/app/services/user_info_service.rb | 3 +++ dpc-portal/config/environments/test.rb | 1 + dpc-portal/config/initializers/omniauth.rb | 4 ++-- 7 files changed, 38 insertions(+), 12 deletions(-) diff --git a/dpc-portal/app/controllers/application_controller.rb b/dpc-portal/app/controllers/application_controller.rb index 46bc671ed..345808806 100644 --- a/dpc-portal/app/controllers/application_controller.rb +++ b/dpc-portal/app/controllers/application_controller.rb @@ -2,9 +2,10 @@ # Parent class of all controllers class ApplicationController < ActionController::Base - IDP_HOST = ENV.fetch('IDP_HOST') + # IDP_HOST = ENV.fetch('IDP_HOST') + CLEAR_IDP_HOST = ENV.fetch('CLEAR_IDP_HOST') # IDP_CLIENT_ID = ENV.fetch('IDP_CLIENT_ID') - IDP_CLIENT_ID = ENV.fetch('CLEAR_IDP_CLIENT_ID') + CLEAR_IDP_CLIENT_ID = ENV.fetch('CLEAR_IDP_CLIENT_ID') before_action :check_session_length before_action :set_current_request_attributes @@ -55,9 +56,9 @@ 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: '/id_me/logout', - query: { client_id: IDP_CLIENT_ID, + URI::HTTPS.build(host: CLEAR_IDP_HOST, + path: '/clear/logout', + query: { client_id: CLEAR_IDP_CLIENT_ID, post_logout_redirect_uri: "#{root_url}auth/logged_out", state: }.to_query) end diff --git a/dpc-portal/app/controllers/invitations_controller.rb b/dpc-portal/app/controllers/invitations_controller.rb index 363fc4844..3ff0b01f1 100644 --- a/dpc-portal/app/controllers/invitations_controller.rb +++ b/dpc-portal/app/controllers/invitations_controller.rb @@ -78,12 +78,13 @@ def login { actionContext: LoggingConstants::ActionContext::Registration, actionType: LoggingConstants::ActionType::BeginLogin, invitation: @invitation.id }]) - url = URI::HTTPS.build(host: IDP_HOST, + url = URI::HTTPS.build(host: CLEAR_IDP_HOST, path: '/oauth/authorize', - query: { client_id: IDP_CLIENT_ID, - redirect_uri: "#{my_protocol_host}/auth/id_me/callback", + # query: { client_id: IDP_CLIENT_ID, + query: { client_id: CLEAR_IDP_CLIENT_ID, + redirect_uri: "#{my_protocol_host}/auth/clear/callback", response_type: 'code', - scope: 'openid http://idmanagement.gov/ns/assurance/ial/2/aal/2', + scope: 'openid profile email', nonce: @nonce, state: @state }.to_query) redirect_to url, allow_other_host: true @@ -202,7 +203,8 @@ def create_ao_org_link def user user_info = UserInfoService.new.user_info(session) - @user = User.find_or_create_by!(provider: :id_me, 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 b2660309e..0d6c58326 100644 --- a/dpc-portal/app/controllers/login_dot_gov_controller.rb +++ b/dpc-portal/app/controllers/login_dot_gov_controller.rb @@ -19,6 +19,23 @@ def id_me redirect_to path(user, auth) end + def clear + auth = request.env['omniauth.auth'] + + user = User.find_by(provider: auth.provider, uid: auth.uid) + if user + sign_in(user) + session[:logged_in_at] = Time.now + Rails.logger.info(['User logged in', + { actionContext: LoggingConstants::ActionContext::Authentication, + actionType: LoggingConstants::ActionType::UserLoggedIn }]) + end + + # this will probably fail + ial_2_actions(user, auth) + redirect_to path(user, auth) + end + def no_account render(Page::Utility::ErrorComponent.new(nil, 'no_account'), status: :forbidden) diff --git a/dpc-portal/app/jobs/verify_resource_health_job.rb b/dpc-portal/app/jobs/verify_resource_health_job.rb index 47b35a9bf..d69dc732c 100644 --- a/dpc-portal/app/jobs/verify_resource_health_job.rb +++ b/dpc-portal/app/jobs/verify_resource_health_job.rb @@ -10,6 +10,8 @@ class VerifyResourceHealthJob < ApplicationJob REGION = 'us-east-1' ENVIRONMENT = ENV.fetch('ENV', 'none') 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 = {}) diff --git a/dpc-portal/app/services/user_info_service.rb b/dpc-portal/app/services/user_info_service.rb index cf06b6d75..458ddc99a 100644 --- a/dpc-portal/app/services/user_info_service.rb +++ b/dpc-portal/app/services/user_info_service.rb @@ -2,6 +2,7 @@ # A service that verifies generates an ao invitation class UserInfoService + # TODO: figure out CLEAR equivalent USER_INFO_URI = URI("https://#{ENV.fetch('IDP_HOST')}/api/public/v3/attributes.json") def user_info(session) @@ -25,6 +26,8 @@ def validate_session(session) def request_info(token) start_tracking response = Net::HTTP.get_response(USER_INFO_URI, auth_header(token)) + puts "response:" + puts response code = response.code.to_i case code when 200...299 diff --git a/dpc-portal/config/environments/test.rb b/dpc-portal/config/environments/test.rb index a72f30c9e..dab4560b4 100644 --- a/dpc-portal/config/environments/test.rb +++ b/dpc-portal/config/environments/test.rb @@ -71,3 +71,4 @@ 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'] = 'api.idmelabs.com' +ENV['CLEAR_IDP_HOST'] = 'verified.clearme.com' diff --git a/dpc-portal/config/initializers/omniauth.rb b/dpc-portal/config/initializers/omniauth.rb index 43cbf8285..7240e2dbc 100644 --- a/dpc-portal/config/initializers/omniauth.rb +++ b/dpc-portal/config/initializers/omniauth.rb @@ -9,9 +9,9 @@ Rails.application.config.middleware.use OmniAuth::Builder do OmniAuth.config.logger = Rails.logger # idp_host = ENV.fetch('IDP_HOST', 'api.idmelabs.com') - idp_host = ENV.fetch('CLEAR_IDP_HOST') + idp_host = ENV['CLEAR_IDP_HOST'] # client_id = ENV.fetch('IDP_CLIENT_ID', '925bb2985ccf623114359caa76228919') - client_id = ENV.fetch('CLEAR_IDP_CLIENT_ID') + client_id = ENV['CLEAR_IDP_CLIENT_ID'] # client_secret = ENV['IDP_CLIENT_SECRET'] client_secret = ENV['CLEAR_IDP_CLIENT_SECRET'] provider :openid_connect, { From 16ba82eccaea3f0f0b22e16da1f75c45bb10c287 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Tue, 12 May 2026 15:14:33 -0700 Subject: [PATCH 05/23] WIP userinfo updates, address rubocop --- .../app/controllers/login_dot_gov_controller.rb | 17 +++-------------- .../app/jobs/verify_resource_health_job.rb | 4 ++-- dpc-portal/app/services/user_info_service.rb | 3 +-- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/dpc-portal/app/controllers/login_dot_gov_controller.rb b/dpc-portal/app/controllers/login_dot_gov_controller.rb index 0d6c58326..a8bd27e52 100644 --- a/dpc-portal/app/controllers/login_dot_gov_controller.rb +++ b/dpc-portal/app/controllers/login_dot_gov_controller.rb @@ -2,7 +2,8 @@ # Handles interactions with login.gov class LoginDotGovController < ApplicationController - skip_before_action :verify_authenticity_token, only: :id_me + # skip_before_action :verify_authenticity_token, only: :id_me + skip_before_action :verify_authenticity_token, only: :clear def id_me auth = request.env['omniauth.auth'] @@ -20,20 +21,8 @@ def id_me end def clear - auth = request.env['omniauth.auth'] - - user = User.find_by(provider: auth.provider, uid: auth.uid) - if user - sign_in(user) - session[:logged_in_at] = Time.now - Rails.logger.info(['User logged in', - { actionContext: LoggingConstants::ActionContext::Authentication, - actionType: LoggingConstants::ActionType::UserLoggedIn }]) - end - # this will probably fail - ial_2_actions(user, auth) - redirect_to path(user, auth) + id_me end def no_account diff --git a/dpc-portal/app/jobs/verify_resource_health_job.rb b/dpc-portal/app/jobs/verify_resource_health_job.rb index d69dc732c..6c5c48a85 100644 --- a/dpc-portal/app/jobs/verify_resource_health_job.rb +++ b/dpc-portal/app/jobs/verify_resource_health_job.rb @@ -10,8 +10,8 @@ class VerifyResourceHealthJob < ApplicationJob REGION = 'us-east-1' ENVIRONMENT = ENV.fetch('ENV', 'none') IDP_HOST = ENV.fetch('IDP_HOST', nil) -# # will fail, not used -# CLEAR_IDP_HOST = ENV.fetch('CLEAR_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 = {}) diff --git a/dpc-portal/app/services/user_info_service.rb b/dpc-portal/app/services/user_info_service.rb index 458ddc99a..e9445603c 100644 --- a/dpc-portal/app/services/user_info_service.rb +++ b/dpc-portal/app/services/user_info_service.rb @@ -26,8 +26,7 @@ def validate_session(session) def request_info(token) start_tracking response = Net::HTTP.get_response(USER_INFO_URI, auth_header(token)) - puts "response:" - puts response + puts "request_info response: #{response}" code = response.code.to_i case code when 200...299 From 8d7ddc84d3b22fdd9c3e4ae819cbbe08fb103a2c Mon Sep 17 00:00:00 2001 From: Luke Short Date: Tue, 12 May 2026 15:52:25 -0700 Subject: [PATCH 06/23] WIP testing updates to point at clear instead of id.me --- .../app/jobs/verify_resource_health_job.rb | 10 ++++---- dpc-portal/app/services/user_info_service.rb | 4 +++- dpc-portal/spec/factories/users.rb | 3 ++- .../jobs/verify_resource_health_job_spec.rb | 2 ++ dpc-portal/spec/requests/invitations_spec.rb | 18 +++++++++----- .../spec/services/user_info_service_spec.rb | 2 ++ dpc-portal/spec/support/login_support.rb | 6 +++-- dpc-portal/spec/system/accessibility_spec.rb | 24 ++++++++++++------- dpc-portal/spec/system/new_invitation_spec.rb | 9 ++++--- 9 files changed, 52 insertions(+), 26 deletions(-) diff --git a/dpc-portal/app/jobs/verify_resource_health_job.rb b/dpc-portal/app/jobs/verify_resource_health_job.rb index 6c5c48a85..71f6d7400 100644 --- a/dpc-portal/app/jobs/verify_resource_health_job.rb +++ b/dpc-portal/app/jobs/verify_resource_health_job.rb @@ -9,9 +9,9 @@ class VerifyResourceHealthJob < ApplicationJob METRIC_NAMESPACE = 'DPC' REGION = 'us-east-1' ENVIRONMENT = ENV.fetch('ENV', 'none') - IDP_HOST = ENV.fetch('IDP_HOST', nil) - # # will fail, not used - # CLEAR_IDP_HOST = ENV.fetch('CLEAR_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 = {}) @@ -37,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/services/user_info_service.rb b/dpc-portal/app/services/user_info_service.rb index e9445603c..66ce2cb9c 100644 --- a/dpc-portal/app/services/user_info_service.rb +++ b/dpc-portal/app/services/user_info_service.rb @@ -3,7 +3,9 @@ # A service that verifies generates an ao invitation class UserInfoService # TODO: figure out CLEAR equivalent - USER_INFO_URI = URI("https://#{ENV.fetch('IDP_HOST')}/api/public/v3/attributes.json") + # USER_INFO_URI = URI("https://#{ENV.fetch('IDP_HOST')}/api/public/v3/attributes.json") + # this feels fraught and there should be a standardized way do this across idp's + USER_INFO_URI = "https://verified.clearme.com/integrations/.well-known/openid-configuration" def user_info(session) validate_session(session) diff --git a/dpc-portal/spec/factories/users.rb b/dpc-portal/spec/factories/users.rb index bac0df9dc..4f3a2eb56 100644 --- a/dpc-portal/spec/factories/users.rb +++ b/dpc-portal/spec/factories/users.rb @@ -3,7 +3,8 @@ FactoryBot.define do factory :user, aliases: %i[invited_by] do sequence(:uid) { |n| n } - provider { :id_me } + # provider { :id_me } + provider { :clear } email { "user#{rand(0..100_000)}@example.com" } end end 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 834fa6b59..3997b5fd3 100644 --- a/dpc-portal/spec/jobs/verify_resource_health_job_spec.rb +++ b/dpc-portal/spec/jobs/verify_resource_health_job_spec.rb @@ -86,6 +86,7 @@ context 'not connected to AWS' do it 'should ignore connection error and move on gracefully' do 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 @@ -150,6 +151,7 @@ def expect_cpi(auth_health: true, api_health: true, metric: 1) def expect_idp(site_status: 200, metric: 1) 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/requests/invitations_spec.rb b/dpc-portal/spec/requests/invitations_spec.rb index 828b3afcd..ebbf98c06 100644 --- a/dpc-portal/spec/requests/invitations_spec.rb +++ b/dpc-portal/spec/requests/invitations_spec.rb @@ -632,13 +632,15 @@ post "/organizations/#{org.id}/invitations/#{invitation.id}/register" end it 'should not create user if exists' do - create(:user, provider: :id_me, 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: :id_me, 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" @@ -648,7 +650,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: :id_me, 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 @@ -707,7 +710,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: :id_me, 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 @@ -739,7 +743,8 @@ expect(request.session[:user_pac_id]).to be_nil end it 'should set pac_id on existing user' do - create(:user, provider: :id_me, 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 @@ -825,7 +830,8 @@ def log_in OmniAuth.config.test_mode = true - OmniAuth.config.add_mock(:id_me, + # OmniAuth.config.add_mock(:id_me, + OmniAuth.config.add_mock(:clear, { uid: '12345', credentials: { expires_in: 899, token: 'bearer-token' }, diff --git a/dpc-portal/spec/services/user_info_service_spec.rb b/dpc-portal/spec/services/user_info_service_spec.rb index faf10bb18..aba5cfaa7 100644 --- a/dpc-portal/spec/services/user_info_service_spec.rb +++ b/dpc-portal/spec/services/user_info_service_spec.rb @@ -8,12 +8,14 @@ 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://api.idmelabs.com/oidc', 'iss' => 'https://api.idmelabs.com/oidc', 'email' => 'david@example.com', 'email_verified' => true, diff --git a/dpc-portal/spec/support/login_support.rb b/dpc-portal/spec/support/login_support.rb index 088c6b077..a4d3ac54a 100644 --- a/dpc-portal/spec/support/login_support.rb +++ b/dpc-portal/spec/support/login_support.rb @@ -3,12 +3,14 @@ module LoginSupport def sign_in(user) OmniAuth.config.test_mode = true - OmniAuth.config.add_mock(:id_me, + # 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/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 ed5f82dbd..df12c290e 100644 --- a/dpc-portal/spec/system/accessibility_spec.rb +++ b/dpc-portal/spec/system/accessibility_spec.rb @@ -14,14 +14,16 @@ before do OmniAuth.config.test_mode = true - OmniAuth.config.add_mock(:id_me, + # 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/id_me/callback' + visit '/auth/clear/callback' end context 'login' do it 'shows login page ok' do @@ -38,14 +40,17 @@ def sign_in context 'bad user tries to log in' do it 'shows no such user page' do - visit '/auth/id_me/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: :id_me, uid: '12345', + # create(:user, provider: :id_me, uid: '12345', + create(:user, provider: :clear, uid: '12345', verification_status: 'rejected', verification_reason: 'ao_med_sanctions') - visit '/auth/id_me/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 @@ -53,9 +58,11 @@ def sign_in context 'valid user tries to log in' do it 'shows success page' do - create(:user, provider: :id_me, uid: '12345', + # create(:user, provider: :id_me, uid: '12345', + create(:user, provider: :clear, uid: '12345', verification_status: 'approved') - visit '/auth/id_me/callback' + # 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 @@ -63,7 +70,8 @@ def sign_in end context 'organizations' do - let!(:user) { create(:user, uid:, provider: :id_me, verification_status: :approved) } + # 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) } diff --git a/dpc-portal/spec/system/new_invitation_spec.rb b/dpc-portal/spec/system/new_invitation_spec.rb index ffcb64b5d..ddfd6cd1a 100644 --- a/dpc-portal/spec/system/new_invitation_spec.rb +++ b/dpc-portal/spec/system/new_invitation_spec.rb @@ -12,18 +12,21 @@ before do OmniAuth.config.test_mode = true - OmniAuth.config.add_mock(:id_me, + # 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/id_me/callback' + visit '/auth/clear/callback' end context 'CD invite' do let(:dpc_api_organization_id) { 'some-gnarly-guid' } - let!(:user) { create(:user, provider: :id_me, uid: '12345') } + # 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) } From 29c070199434629acd85cd7f799fb777c8732b88 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Tue, 12 May 2026 16:31:05 -0700 Subject: [PATCH 07/23] a bunch more clear-specific changes, commented out rspec calls --- dpc-portal-test.sh | 4 +- dpc-portal/spec/requests/invitations_spec.rb | 3 +- .../spec/requests/login_dot_gov_spec.rb | 71 +++++++++++++------ .../spec/requests/users/sessions_spec.rb | 3 +- dpc-portals-test.sh | 4 +- 5 files changed, 56 insertions(+), 29 deletions(-) diff --git a/dpc-portal-test.sh b/dpc-portal-test.sh index 280d38c54..5b83b46f0 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/spec/requests/invitations_spec.rb b/dpc-portal/spec/requests/invitations_spec.rb index ebbf98c06..f01fc2182 100644 --- a/dpc-portal/spec/requests/invitations_spec.rb +++ b/dpc-portal/spec/requests/invitations_spec.rb @@ -839,7 +839,8 @@ def log_in extra: { raw_info: { given_name: 'Bob', family_name: 'Hoskins', ial: 'http://idmanagement.gov/ns/assurance/ial/2' } } }) - post '/auth/id_me' + # post '/auth/id_me' + post '/auth/clear' follow_redirect! end diff --git a/dpc-portal/spec/requests/login_dot_gov_spec.rb b/dpc-portal/spec/requests/login_dot_gov_spec.rb index 891a503b5..08eb1d5ac 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 /auth/id_me' 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: 'id_me', 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 '/auth/id_me' + # 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 '/auth/id_me' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! end it 'should not add another user' do - expect(User.where(uid: '12345', provider: 'id_me').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 '/auth/id_me' + # 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 '/auth/id_me' + # 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(:id_me, + # 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: 'id_me', 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 '/auth/id_me' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! end.to change { - User.where(uid: '12345', provider: 'id_me', 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 '/auth/id_me' + # 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 '/auth/id_me' + # 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 '/auth/id_me' + # 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(:id_me, + # 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: 'id_me', 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: 'id_me', 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 '/auth/id_me' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! expect(response.location).to eq organizations_url - expect(User.where(uid: '12345', provider: 'id_me', 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 '/auth/id_me' + # 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 '/auth/id_me' + # 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 '/auth/id_me' + # post '/auth/id_me' + post CLEAR_AUTH_ENDPOINT follow_redirect! end it 'does not set authentication token' do - post '/auth/id_me' + # 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 diff --git a/dpc-portal/spec/requests/users/sessions_spec.rb b/dpc-portal/spec/requests/users/sessions_spec.rb index 09c65ec83..e0675119b 100644 --- a/dpc-portal/spec/requests/users/sessions_spec.rb +++ b/dpc-portal/spec/requests/users/sessions_spec.rb @@ -31,7 +31,8 @@ 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 diff --git a/dpc-portals-test.sh b/dpc-portals-test.sh index e73976d44..48bcfbdac 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 "│ │" From 6b84c28d8e775c35bc79b4116a1a048907adc8f3 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Wed, 13 May 2026 12:37:30 -0700 Subject: [PATCH 08/23] separate buttons to call idme or clear auth endpoints --- .../app/components/page/session/login_component.html.erb | 4 ++++ dpc-portal/app/components/page/session/login_component.rb | 5 +++-- dpc-portal/app/views/users/sessions/new.html.erb | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) 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 0ca1504a5..7594fe539 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 83d2318fb..818908436 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/views/users/sessions/new.html.erb b/dpc-portal/app/views/users/sessions/new.html.erb index 3ce22dc3d..0592bb500 100644 --- a/dpc-portal/app/views/users/sessions/new.html.erb +++ b/dpc-portal/app/views/users/sessions/new.html.erb @@ -1 +1 @@ -<%= render(Page::Session::LoginComponent.new(omniauth_authorize_path(:id_me))) %> +<%= render(Page::Session::LoginComponent.new(omniauth_authorize_path(:id_me), omniauth_authorize_path(:clear))) %> From b54d23ddf29f2bcd442790095efdd6e2c1e74935 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Wed, 13 May 2026 16:01:52 -0700 Subject: [PATCH 09/23] create separate omniauth provider for clear vs idme --- dpc-portal/config/initializers/omniauth.rb | 47 +++++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/dpc-portal/config/initializers/omniauth.rb b/dpc-portal/config/initializers/omniauth.rb index 7240e2dbc..31b3dde31 100644 --- a/dpc-portal/config/initializers/omniauth.rb +++ b/dpc-portal/config/initializers/omniauth.rb @@ -8,12 +8,16 @@ Rails.application.config.middleware.use OmniAuth::Builder do OmniAuth.config.logger = Rails.logger - # idp_host = ENV.fetch('IDP_HOST', 'api.idmelabs.com') - idp_host = ENV['CLEAR_IDP_HOST'] - # client_id = ENV.fetch('IDP_CLIENT_ID', '925bb2985ccf623114359caa76228919') - client_id = ENV['CLEAR_IDP_CLIENT_ID'] - # client_secret = ENV['IDP_CLIENT_SECRET'] - client_secret = ENV['CLEAR_IDP_CLIENT_SECRET'] + # 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", @@ -26,15 +30,36 @@ 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" + } + } + provider :openid_connect, { + name: :clear, + issuer: "https://#{clear_idp_host}/oidc", + scope: "openid", + response_type: :code, + client_auth_method: :client_secret_post, + client_options: { + port: 443, + scheme: 'https', + host: clear_idp_host, + identifier: clear_client_id, + secret: clear_client_secret, # redirect_uri: "#{my_protocol_host}/auth/id_me/callback", redirect_uri: "#{my_protocol_host}/auth/clear/callback", # authorization_endpoint: "https://#{idp_host}/oauth/authorize", - authorization_endpoint: "https://#{idp_host}/integrations/oauth2/auth", + authorization_endpoint: "https://#{clear_idp_host}/integrations/oauth2/auth", # token_endpoint: "https://#{idp_host}/oauth/token", - token_endpoint: "https://#{idp_host}/integrations/oauth2/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" + token_endpoint: "https://#{clear_idp_host}/integrations/oauth2/token", + # tbd + userinfo_endpoint: "https://#{clear_idp_host}/api/public/v3/attributes.json", + jwks_uri: "https://#{clear_idp_host}/oidc/.well-known/jwks", + end_session_endpoint: "https://#{clear_idp_host}/logout" } } end From 697ab2d1e6acad79301cb2183a8c4324a06939bb Mon Sep 17 00:00:00 2001 From: Luke Short Date: Wed, 13 May 2026 17:17:04 -0700 Subject: [PATCH 10/23] fix auth redirect url's for redirecting TO Clear --- dpc-portal/app/controllers/invitations_controller.rb | 2 +- dpc-portal/app/controllers/login_dot_gov_controller.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dpc-portal/app/controllers/invitations_controller.rb b/dpc-portal/app/controllers/invitations_controller.rb index 3ff0b01f1..7ac4f00d0 100644 --- a/dpc-portal/app/controllers/invitations_controller.rb +++ b/dpc-portal/app/controllers/invitations_controller.rb @@ -79,7 +79,7 @@ def login actionType: LoggingConstants::ActionType::BeginLogin, invitation: @invitation.id }]) url = URI::HTTPS.build(host: CLEAR_IDP_HOST, - path: '/oauth/authorize', + path: '/integrations/oauth2/auth', # query: { client_id: IDP_CLIENT_ID, query: { client_id: CLEAR_IDP_CLIENT_ID, redirect_uri: "#{my_protocol_host}/auth/clear/callback", diff --git a/dpc-portal/app/controllers/login_dot_gov_controller.rb b/dpc-portal/app/controllers/login_dot_gov_controller.rb index a8bd27e52..6b7a091f4 100644 --- a/dpc-portal/app/controllers/login_dot_gov_controller.rb +++ b/dpc-portal/app/controllers/login_dot_gov_controller.rb @@ -85,6 +85,7 @@ def ial_2_actions(user, auth) end def path(user, auth) + puts "auth extra raw_info response: #{auth.extra.raw_info}" if user.blank? && auth.extra.raw_info.ial == 'http://idmanagement.gov/ns/assurance/ial/1' Rails.logger.info(['User logged in without account', { actionContext: LoggingConstants::ActionContext::Authentication, From aa1bd546104a4eef0a5d2ca3e9c5da36cc125ffb Mon Sep 17 00:00:00 2001 From: Luke Short Date: Thu, 14 May 2026 14:54:51 -0700 Subject: [PATCH 11/23] fix security signing and logout - WIP pulling assurance level from payload --- .../app/controllers/application_controller.rb | 2 +- .../app/controllers/invitations_controller.rb | 4 ++-- .../controllers/login_dot_gov_controller.rb | 1 + dpc-portal/app/services/user_info_service.rb | 5 +---- dpc-portal/config/initializers/omniauth.rb | 18 ++++++++---------- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/dpc-portal/app/controllers/application_controller.rb b/dpc-portal/app/controllers/application_controller.rb index 345808806..27d849923 100644 --- a/dpc-portal/app/controllers/application_controller.rb +++ b/dpc-portal/app/controllers/application_controller.rb @@ -57,7 +57,7 @@ def url_for_login_dot_gov_logout state = SecureRandom.hex(16) session['omniauth.state'] = state URI::HTTPS.build(host: CLEAR_IDP_HOST, - path: '/clear/logout', + path: '/integrations/oauth2/sessions/logout', query: { client_id: CLEAR_IDP_CLIENT_ID, post_logout_redirect_uri: "#{root_url}auth/logged_out", state: }.to_query) diff --git a/dpc-portal/app/controllers/invitations_controller.rb b/dpc-portal/app/controllers/invitations_controller.rb index 7ac4f00d0..c948f237a 100644 --- a/dpc-portal/app/controllers/invitations_controller.rb +++ b/dpc-portal/app/controllers/invitations_controller.rb @@ -80,13 +80,13 @@ def login invitation: @invitation.id }]) url = URI::HTTPS.build(host: CLEAR_IDP_HOST, path: '/integrations/oauth2/auth', - # query: { client_id: IDP_CLIENT_ID, query: { client_id: CLEAR_IDP_CLIENT_ID, redirect_uri: "#{my_protocol_host}/auth/clear/callback", response_type: 'code', - scope: 'openid profile email', + scope: 'openid', nonce: @nonce, state: @state }.to_query) + puts "redirecting to: #{url}" redirect_to url, allow_other_host: true end diff --git a/dpc-portal/app/controllers/login_dot_gov_controller.rb b/dpc-portal/app/controllers/login_dot_gov_controller.rb index 6b7a091f4..57a998b5c 100644 --- a/dpc-portal/app/controllers/login_dot_gov_controller.rb +++ b/dpc-portal/app/controllers/login_dot_gov_controller.rb @@ -76,6 +76,7 @@ def maybe_update_user(user, data) def ial_2_actions(user, auth) data = auth.extra.raw_info + puts "raw_info: #{data}" return unless data.ial == 'http://idmanagement.gov/ns/assurance/ial/2' diff --git a/dpc-portal/app/services/user_info_service.rb b/dpc-portal/app/services/user_info_service.rb index 66ce2cb9c..4192d6df2 100644 --- a/dpc-portal/app/services/user_info_service.rb +++ b/dpc-portal/app/services/user_info_service.rb @@ -2,10 +2,7 @@ # A service that verifies generates an ao invitation class UserInfoService - # TODO: figure out CLEAR equivalent - # USER_INFO_URI = URI("https://#{ENV.fetch('IDP_HOST')}/api/public/v3/attributes.json") - # this feels fraught and there should be a standardized way do this across idp's - USER_INFO_URI = "https://verified.clearme.com/integrations/.well-known/openid-configuration" + USER_INFO_URI = "https://#{ENV.fetch('CLEAR_IDP_HOST')}/integrations/userinfo" def user_info(session) validate_session(session) diff --git a/dpc-portal/config/initializers/omniauth.rb b/dpc-portal/config/initializers/omniauth.rb index 31b3dde31..11622b5bf 100644 --- a/dpc-portal/config/initializers/omniauth.rb +++ b/dpc-portal/config/initializers/omniauth.rb @@ -38,28 +38,26 @@ end_session_endpoint: "https://#{idp_host}/logout" } } + clear_issuer = "https://#{clear_idp_host}/integrations" provider :openid_connect, { name: :clear, - issuer: "https://#{clear_idp_host}/oidc", + 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/id_me/callback", redirect_uri: "#{my_protocol_host}/auth/clear/callback", - # authorization_endpoint: "https://#{idp_host}/oauth/authorize", - authorization_endpoint: "https://#{clear_idp_host}/integrations/oauth2/auth", - # token_endpoint: "https://#{idp_host}/oauth/token", - token_endpoint: "https://#{clear_idp_host}/integrations/oauth2/token", - # tbd - userinfo_endpoint: "https://#{clear_idp_host}/api/public/v3/attributes.json", - jwks_uri: "https://#{clear_idp_host}/oidc/.well-known/jwks", - end_session_endpoint: "https://#{clear_idp_host}/logout" + 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 From bd8012de6f4d2868cfc762330653a9310d08502a Mon Sep 17 00:00:00 2001 From: Luke Short Date: Fri, 15 May 2026 10:53:25 -0700 Subject: [PATCH 12/23] fix formatting for CLEAR logout, WIP custom claim for pulling ssn9 --- dpc-portal/app/controllers/application_controller.rb | 3 ++- dpc-portal/app/controllers/invitations_controller.rb | 6 ++++++ dpc-portal/app/controllers/login_dot_gov_controller.rb | 9 +++++++-- dpc-portal/app/models/invitation.rb | 5 +++-- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/dpc-portal/app/controllers/application_controller.rb b/dpc-portal/app/controllers/application_controller.rb index 27d849923..c116ef8cd 100644 --- a/dpc-portal/app/controllers/application_controller.rb +++ b/dpc-portal/app/controllers/application_controller.rb @@ -60,7 +60,8 @@ def url_for_login_dot_gov_logout path: '/integrations/oauth2/sessions/logout', query: { client_id: CLEAR_IDP_CLIENT_ID, post_logout_redirect_uri: "#{root_url}auth/logged_out", - state: }.to_query) + 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 c948f237a..c7f78abd8 100644 --- a/dpc-portal/app/controllers/invitations_controller.rb +++ b/dpc-portal/app/controllers/invitations_controller.rb @@ -78,12 +78,17 @@ def login { actionContext: LoggingConstants::ActionContext::Registration, actionType: LoggingConstants::ActionType::BeginLogin, invitation: @invitation.id }]) + claims = { + id_token: { ssn9: nil }, + userinfo: { ssn9: 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', + claims:, nonce: @nonce, state: @state }.to_query) puts "redirecting to: #{url}" @@ -132,6 +137,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) diff --git a/dpc-portal/app/controllers/login_dot_gov_controller.rb b/dpc-portal/app/controllers/login_dot_gov_controller.rb index 57a998b5c..a9d1e2f17 100644 --- a/dpc-portal/app/controllers/login_dot_gov_controller.rb +++ b/dpc-portal/app/controllers/login_dot_gov_controller.rb @@ -8,6 +8,8 @@ class LoginDotGovController < ApplicationController 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) @@ -78,16 +80,19 @@ def ial_2_actions(user, auth) data = auth.extra.raw_info puts "raw_info: #{data}" - 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) puts "auth extra raw_info response: #{auth.extra.raw_info}" - if user.blank? && auth.extra.raw_info.ial == 'http://idmanagement.gov/ns/assurance/ial/1' + # 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 }]) diff --git a/dpc-portal/app/models/invitation.rb b/dpc-portal/app/models/invitation.rb index 48f5910dd..c38b1223b 100644 --- a/dpc-portal/app/models/invitation.rb +++ b/dpc-portal/app/models/invitation.rb @@ -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 From b544c4c4253592fe141a3001ae6363a65b07cf77 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Fri, 15 May 2026 19:44:21 -0700 Subject: [PATCH 13/23] still investigating custom OIDC claims in sandbox environment --- dpc-portal/app/controllers/invitations_controller.rb | 4 ++-- dpc-portal/app/controllers/login_dot_gov_controller.rb | 5 ++++- dpc-portal/app/services/user_info_service.rb | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/dpc-portal/app/controllers/invitations_controller.rb b/dpc-portal/app/controllers/invitations_controller.rb index c7f78abd8..5d77045cb 100644 --- a/dpc-portal/app/controllers/invitations_controller.rb +++ b/dpc-portal/app/controllers/invitations_controller.rb @@ -79,8 +79,8 @@ def login actionType: LoggingConstants::ActionType::BeginLogin, invitation: @invitation.id }]) claims = { - id_token: { ssn9: nil }, - userinfo: { ssn9: nil } + 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', diff --git a/dpc-portal/app/controllers/login_dot_gov_controller.rb b/dpc-portal/app/controllers/login_dot_gov_controller.rb index a9d1e2f17..b45e9f343 100644 --- a/dpc-portal/app/controllers/login_dot_gov_controller.rb +++ b/dpc-portal/app/controllers/login_dot_gov_controller.rb @@ -91,6 +91,9 @@ def ial_2_actions(user, auth) def path(user, auth) 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', @@ -98,6 +101,6 @@ def path(user, auth) 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/services/user_info_service.rb b/dpc-portal/app/services/user_info_service.rb index 4192d6df2..31cc4869d 100644 --- a/dpc-portal/app/services/user_info_service.rb +++ b/dpc-portal/app/services/user_info_service.rb @@ -2,7 +2,8 @@ # A service that verifies generates an ao invitation class UserInfoService - USER_INFO_URI = "https://#{ENV.fetch('CLEAR_IDP_HOST')}/integrations/userinfo" + USER_INFO_URI = URI("https://#{ENV.fetch('CLEAR_IDP_HOST')}/integrations/userinfo") + USER_INFO_URI_WITH_CLAIMS_QUERY = URI("#{USER_INFO_URI}?claims=ssn9") def user_info(session) validate_session(session) @@ -24,7 +25,7 @@ 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_URI_WITH_CLAIMS_QUERY, auth_header(token)) puts "request_info response: #{response}" code = response.code.to_i case code From ba6fa822c76910ef8e45d3a3ff4f37fb69d4b41f Mon Sep 17 00:00:00 2001 From: Luke Short Date: Tue, 19 May 2026 14:48:37 -0700 Subject: [PATCH 14/23] update CLEAR api calls to include claims query param and add logging --- .../app/controllers/invitations_controller.rb | 12 +++++-- .../controllers/login_dot_gov_controller.rb | 8 ++++- dpc-portal/app/services/user_info_service.rb | 36 +++++++++++++++---- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/dpc-portal/app/controllers/invitations_controller.rb b/dpc-portal/app/controllers/invitations_controller.rb index 5d77045cb..26e832771 100644 --- a/dpc-portal/app/controllers/invitations_controller.rb +++ b/dpc-portal/app/controllers/invitations_controller.rb @@ -79,8 +79,16 @@ def login actionType: LoggingConstants::ActionType::BeginLogin, invitation: @invitation.id }]) claims = { - id_token: { ssn9: nil, email: nil, email_verified: nil }, - userinfo: { ssn9: nil, email: nil, email_verified: nil } + 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', diff --git a/dpc-portal/app/controllers/login_dot_gov_controller.rb b/dpc-portal/app/controllers/login_dot_gov_controller.rb index b45e9f343..9e19cced5 100644 --- a/dpc-portal/app/controllers/login_dot_gov_controller.rb +++ b/dpc-portal/app/controllers/login_dot_gov_controller.rb @@ -78,7 +78,13 @@ def maybe_update_user(user, data) def ial_2_actions(user, auth) data = auth.extra.raw_info - puts "raw_info: #{data}" + 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'] }]) # assume that assurance level is ial2 if using CLEAR # return unless data.ial == 'http://idmanagement.gov/ns/assurance/ial/2' diff --git a/dpc-portal/app/services/user_info_service.rb b/dpc-portal/app/services/user_info_service.rb index 31cc4869d..0f650d2cb 100644 --- a/dpc-portal/app/services/user_info_service.rb +++ b/dpc-portal/app/services/user_info_service.rb @@ -3,7 +3,21 @@ # A service that verifies generates an ao invitation class UserInfoService USER_INFO_URI = URI("https://#{ENV.fetch('CLEAR_IDP_HOST')}/integrations/userinfo") - USER_INFO_URI_WITH_CLAIMS_QUERY = URI("#{USER_INFO_URI}?claims=ssn9") + 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) @@ -25,12 +39,20 @@ def validate_session(session) def request_info(token) start_tracking - response = Net::HTTP.get_response(USER_INFO_URI_WITH_CLAIMS_QUERY, auth_header(token)) - puts "request_info response: #{response}" + 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 @@ -56,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 @@ -68,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 }] From 3d039029b72b148f0fe78ba735b510889d216fb5 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Wed, 20 May 2026 08:49:56 -0700 Subject: [PATCH 15/23] wip test case updates --- dpc-portal/spec/models/invitation_spec.rb | 4 ++++ dpc-portal/spec/requests/invitations_spec.rb | 7 +++++-- dpc-portal/spec/requests/login_dot_gov_spec.rb | 3 ++- dpc-portal/spec/services/user_info_service_spec.rb | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/dpc-portal/spec/models/invitation_spec.rb b/dpc-portal/spec/models/invitation_spec.rb index 46157fdd7..8a02abef8 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/requests/invitations_spec.rb b/dpc-portal/spec/requests/invitations_spec.rb index f01fc2182..06b8fa121 100644 --- a/dpc-portal/spec/requests/invitations_spec.rb +++ b/dpc-portal/spec/requests/invitations_spec.rb @@ -140,8 +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['redirect_uri']).to start_with('http://localhost:3100/') + 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 diff --git a/dpc-portal/spec/requests/login_dot_gov_spec.rb b/dpc-portal/spec/requests/login_dot_gov_spec.rb index 08eb1d5ac..63e99e4f0 100644 --- a/dpc-portal/spec/requests/login_dot_gov_spec.rb +++ b/dpc-portal/spec/requests/login_dot_gov_spec.rb @@ -209,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 diff --git a/dpc-portal/spec/services/user_info_service_spec.rb b/dpc-portal/spec/services/user_info_service_spec.rb index aba5cfaa7..6ce865f91 100644 --- a/dpc-portal/spec/services/user_info_service_spec.rb +++ b/dpc-portal/spec/services/user_info_service_spec.rb @@ -4,7 +4,7 @@ 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 } From be722417c9b7378fa6c473761b2bbea2e97caaf6 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Fri, 22 May 2026 08:24:00 -0700 Subject: [PATCH 16/23] CLEAR client id for docker --- docker-compose.portals.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.portals.yml b/docker-compose.portals.yml index 162df847c..9558a75f8 100644 --- a/docker-compose.portals.yml +++ b/docker-compose.portals.yml @@ -145,7 +145,7 @@ services: - IDP_ID_ME_HOST=api.idmelabs.com - IDP_ID_ME_CLIENT_ID=925bb2985ccf623114359caa76228919 - CLEAR_IDP_HOST=verified.clearme.com - - CLEAR_IDP_CLIENT_ID=${CLEAR_IDP_CLIENT_ID} + - CLEAR_IDP_CLIENT_ID=d3ca98c2-c43c-4065-8f82-ce9958b4e6d4 - RUBY_YJIT_ENABLE=1 - ENV=local - NEW_RELIC_MONITOR_MODE=false From 847634463f920303c5f24d9ed3cb31ba4e026cd2 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Fri, 22 May 2026 09:44:40 -0700 Subject: [PATCH 17/23] manually de-conflict ansible vault --- dpc-portal-test.sh | 4 +- .../page/session/login_component.html.erb | 8 +-- ops/config/encrypted/local.env | 54 +++++++++---------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/dpc-portal-test.sh b/dpc-portal-test.sh index 5b83b46f0..280d38c54 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/app/components/page/session/login_component.html.erb b/dpc-portal/app/components/page/session/login_component.html.erb index b0bbe9347..f8bb92051 100644 --- a/dpc-portal/app/components/page/session/login_component.html.erb +++ b/dpc-portal/app/components/page/session/login_component.html.erb @@ -16,10 +16,10 @@ <%= button_to @login_path, class: 'usa-button width-full margin-bottom-1', data: { turbo: false } do %> <% 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 %> +#

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/ops/config/encrypted/local.env b/ops/config/encrypted/local.env index 1113a4ad5..061b430e7 100644 --- a/ops/config/encrypted/local.env +++ b/ops/config/encrypted/local.env @@ -1,28 +1,28 @@ $ANSIBLE_VAULT;1.1;AES256 -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 +37373566363161323539323963326136353561613639336662653065333130383530353638353231 +3133653464643736323962643039386166323166643835300a333536343466373132363066373565 +33353034663935353232363736303262393366623436303938306339313439306366333834383863 +6330376130343763340a643961623365396463623861616231346131333532313064396164353336 +34323230313963376364353163373366363436343733316139373431333331323832386532363933 +33633931633830326431626436376637336530326264313962386530663035346363326566626162 +33663836386165323139383130306134306435313161386631383162336639313961386563316238 +31623738643932623266613661303632366339623436323865613438353334373562663765303862 +35333762303834616666303166653934353965643932386563313930343765323661393234303832 +37666138323661623237623365636238633531373330613464363835393430623635626336316166 +61336266653061386166313834333137313964323663396563343162646665313062663263316432 +35343936353131313231623061383137653536623631316362306335366362363637376136333733 +34353236303436336539396666313333613630326537396533363835616232656437333563396238 +32303962326436626534393031653830346336363138353136393430343131623639333964326631 +35613561303033633562316438366539303232633064303135356639373033623461313266306434 +37326462616162306636646537363331633339633232343035343739623133383030393964646139 +36366236396130376630626236626663393761666338613934303935623038636334613639383566 +64363862373739366133353937643161663531383663366430323331366666303836303237356361 +38616630313130626661343463656436383665323532643431656430633062333233313433623831 +34313438373165326438646638363630623736316230613737393332356366633365323461656166 +30613431343565306535373735313039303033333664643463373663646263346230333034623061 +33346663623739326234333536353663383331633034646631663465333038633733383062386466 +31623930303539373839343264363365383165343634366264616564316637363337386330356537 +33616637306538636264613766353466373666366632633261356165353366333833373331663465 +62373832373561653536373465623839633530353839646438393238323964306562376239366261 +63306432383563666466623332336364613361313236613334323134326163393365616430326635 +383633656237383734313932643735626638 From 407d7653799a8ae331508dfe925f8b51112ba187 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Fri, 22 May 2026 11:34:35 -0700 Subject: [PATCH 18/23] resolve merge conflicts --- .../app/components/page/session/login_component.html.erb | 4 ---- dpc-portal/app/views/users/sessions/new.html.erb | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) 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 f8bb92051..746f3532b 100644 --- a/dpc-portal/app/components/page/session/login_component.html.erb +++ b/dpc-portal/app/components/page/session/login_component.html.erb @@ -16,10 +16,6 @@ <%= button_to @login_path, class: 'usa-button width-full margin-bottom-1', data: { turbo: false } do %> <% 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/views/users/sessions/new.html.erb b/dpc-portal/app/views/users/sessions/new.html.erb index 0592bb500..d376c3c83 100644 --- a/dpc-portal/app/views/users/sessions/new.html.erb +++ b/dpc-portal/app/views/users/sessions/new.html.erb @@ -1 +1 @@ -<%= render(Page::Session::LoginComponent.new(omniauth_authorize_path(:id_me), omniauth_authorize_path(:clear))) %> +<%= render(Page::Session::LoginComponent.new(omniauth_authorize_path(:clear))) %> From bbe4b34fd1890f62b27189e282b07e2a51839b8a Mon Sep 17 00:00:00 2001 From: Luke Short Date: Fri, 22 May 2026 16:12:36 -0700 Subject: [PATCH 19/23] move CLEAR configuration to config object like other CSP's --- dpc-portal/.env.test | 1 + .../app/controllers/application_controller.rb | 14 ++-- .../app/controllers/invitations_controller.rb | 10 ++- dpc-portal/app/models/csp_config.rb | 48 +++++------ dpc-portal/app/services/user_info_service.rb | 79 +++++++------------ dpc-portal/config/csp.yml | 14 ++++ dpc-portal/config/initializers/omniauth.rb | 46 +++++------ .../omniauth_openid_connect_patch.rb | 3 +- 8 files changed, 98 insertions(+), 117 deletions(-) diff --git a/dpc-portal/.env.test b/dpc-portal/.env.test index 87bc2955d..39a9d9092 100644 --- a/dpc-portal/.env.test +++ b/dpc-portal/.env.test @@ -11,6 +11,7 @@ CPI_API_GW_BASE_URL=http://localhost:4567/ CMS_IDM_OAUTH_URL=http://localhost:4567/ IDP_ID_ME_HOST=api.idmelabs.com IDP_LOGIN_DOT_GOV_HOST=idp.int.identitysandbox.gov +CLEAR_IDP_HOST=verified.clearme.com RUBY_YJIT_ENABLE=1 ENV=local RAILS_ENV=development diff --git a/dpc-portal/app/controllers/application_controller.rb b/dpc-portal/app/controllers/application_controller.rb index c132935e0..696e41c57 100644 --- a/dpc-portal/app/controllers/application_controller.rb +++ b/dpc-portal/app/controllers/application_controller.rb @@ -2,11 +2,6 @@ # Parent class of all controllers class ApplicationController < ActionController::Base - # 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 before_action :no_store @@ -92,11 +87,12 @@ def url_for_id_me_logout def url_for_clear_logout state = SecureRandom.hex(16) session['omniauth.state'] = state - URI::HTTPS.build(host: CLEAR_IDP_HOST, - path: '/integrations/oauth2/sessions/logout', - query: { client_id: CLEAR_IDP_CLIENT_ID, + csp_config = CspConfig.for(:clear) + URI::HTTPS.build(host: csp_config.host, + path: csp_config.log_out_path, + query: { client_id: csp_config.identifier, post_logout_redirect_uri: "#{root_url}auth/logged_out", - id_token_hint: session[:login_dot_gov_id_token], + id_token_hint: session[:login_dot_gov_id_token] }.to_query) end diff --git a/dpc-portal/app/controllers/invitations_controller.rb b/dpc-portal/app/controllers/invitations_controller.rb index 752b7ec62..0291af261 100644 --- a/dpc-portal/app/controllers/invitations_controller.rb +++ b/dpc-portal/app/controllers/invitations_controller.rb @@ -100,10 +100,12 @@ def login 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", + csp_config = CspConfig.for(:clear) + authorization_uri = URI(csp_config.authorization_endpoint) + url = URI::HTTPS.build(host: authorization_uri.host, + path: authorization_uri.path, + query: { client_id: csp_config.identifier, + redirect_uri: "#{my_protocol_host}#{csp_config.redirect_path}", response_type: 'code', scope: 'openid', claims:, diff --git a/dpc-portal/app/models/csp_config.rb b/dpc-portal/app/models/csp_config.rb index 51b08a2c3..ee40de389 100644 --- a/dpc-portal/app/models/csp_config.rb +++ b/dpc-portal/app/models/csp_config.rb @@ -6,51 +6,43 @@ class CspConfig ENV_NAME = ENV.fetch('ENV', 'local') CONFIG = Rails.application.config_for(:csp).freeze - def initialize(code, host, identifier, user_info_endpoint, log_out_path, token_expiration_interval) # rubocop:disable Metrics/ParameterLists - @host = host - @identifier = identifier + def initialize(code, config) @code = code - @user_info_endpoint = user_info_endpoint - @log_out_path = log_out_path - @token_expiration_interval = token_expiration_interval + @host = config[:host] + @identifier = config[:identifier] + @client_secret = config[:client_secret] + @client_auth_method = config[:client_auth_method] + @authorization_endpoint = config[:authorization_endpoint] + @token_endpoint = config[:token_endpoint] + @user_info_endpoint = config[:user_info_endpoint] + @jwks_uri = config[:jwks_uri] + @redirect_path = config[:redirect_path] + @log_out_path = config[:log_out_path] + @token_expiration_interval = config[:token_expiration_interval] end - LOGIN_DOT_GOV = new('login_dot_gov', - CONFIG[:login_dot_gov][:host], - CONFIG[:login_dot_gov][:identifier], - CONFIG[:login_dot_gov][:user_info_path], - CONFIG[:login_dot_gov][:log_out_path], - CONFIG[:login_dot_gov][:token_expiration_interval]) - ID_ME = new('id_me', - CONFIG[:id_me][:host], - CONFIG[:id_me][:identifier], - CONFIG[:id_me][:user_info_path], - CONFIG[:id_me][:log_out_path], - CONFIG[:id_me][:token_expiration_interval]) - # CLEAR = new('clear', - # CONFIG[:clear][:host], - # CONFIG[:clear][:identifier], - # CONFIG[:clear][:user_info_path], - # CONFIG[:clear][:log_out_path], - # CONFIG[:clear][:token_expiration_interval]) + LOGIN_DOT_GOV = new('login_dot_gov', CONFIG[:login_dot_gov]) + ID_ME = new('id_me', CONFIG[:id_me]) + CLEAR = new('clear', CONFIG[:clear]) private_class_method :new - attr_reader :user_info_endpoint, :log_out_path, :token_expiration_interval, :host, :identifier + attr_reader :authorization_endpoint, :client_auth_method, :client_secret, :code, :host, :identifier, :jwks_uri, :log_out_path, + :redirect_path, :token_endpoint, :token_expiration_interval, :user_info_endpoint def self.for(code) case code.to_s when 'login_dot_gov' then LOGIN_DOT_GOV when 'id_me' then ID_ME - # when 'clear' then CLEAR + when 'clear' then CLEAR else raise ArgumentError, "Unknown CSP code: #{code}" end end def self.[](code) - from(code) + self.for(code) end def self.list - [LOGIN_DOT_GOV.code, ID_ME.code] # CLEAR + [LOGIN_DOT_GOV.code, ID_ME.code, CLEAR.code] end end diff --git a/dpc-portal/app/services/user_info_service.rb b/dpc-portal/app/services/user_info_service.rb index 46e59d1e0..58c634bdc 100644 --- a/dpc-portal/app/services/user_info_service.rb +++ b/dpc-portal/app/services/user_info_service.rb @@ -31,7 +31,8 @@ def parsed_response(response) if response.content_type.to_s.strip.downcase == 'application/jwt' || looks_like_jwt?(body) decode_jwt(body) else - JSON.parse(body).with_indifferent_access + # JSON.parse(body).with_indifferent_access + JSON.parse response.body end end @@ -40,62 +41,33 @@ def looks_like_jwt?(body) parts.length == 3 && parts.all? { |p| p.match?(/\A[A-Za-z0-9_-]+\z/) } end + # def jwt_content_type?(response) + # response.content_type.to_s.split(';').first.strip.downcase == 'application/jwt' + # end + + # def normalize_jwt_body(body) + # parsed_body = JSON.parse(body) + # return parsed_body.strip if parsed_body.is_a?(String) + + # body + # rescue JSON::ParserError + # strip_wrapping_quotes(body) + # end + + # def strip_wrapping_quotes(body) + # body = body.to_s.strip + # return body[1..-2] if body.start_with?('"') && body.end_with?('"') + + # body + # end + def decode_jwt(body) - body = body[1..-2] if body.start_with?('"') && body.end_with?('"') JSON::JWT.decode(body, :skip_verification).to_h.with_indifferent_access end def oidc_client_config(csp) return ID_ME_CLIENT_CONFIG if csp.to_s == :id_me.to_s return LOGIN_DOT_GOV_CLIENT_CONFIG if csp.to_s == :login_dot_gov.to_s - - # TODO: Add CLEAR_CONFIG here - 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 }") - - # TODO move to initializers - clear_idp_host = ENV['CLEAR_IDP_HOST'] - # move "client_id" to "identifier" for CLEAR_CONFIG - # clear_client_id = ENV['CLEAR_IDP_CLIENT_ID'] - clear_issuer = "https://#{clear_idp_host}/integrations" - CLEAR_CLIENT_CONFIG = { - name: :clear, - issuer: clear_issuer, - # discovery: false, - scope: "openid", - response_type: :code, - # acr_values: 'http://idmanagement.gov/ns/assurance/ial/1', - # client_auth_method: :jwt_bearer, - client_auth_method: :client_secret_post, - client_options: { - port: 443, - scheme: 'https', - host: "https://#{clear_idp_host}/", - identifier: "urn:gov:cms:openidconnect.profiles:sp:sso:cms:dpc:#{ENV['ENV']}", - # private_key: ENV['LOGIN_DOT_GOV_CLIENT_PRIVATE_KEY'], - secret: ENV['CLEAR_IDP_CLIENT_SECRET'], - redirect_uri: "#{my_protocol_host}/auth/clear/callback", - - authorization_endpoint: "#{clear_issuer}/oauth2/auth", - token_endpoint: "#{clear_issuer}/oauth2/token", - userinfo_endpoint: USER_INFO_CLAIMS_URI.to_s, - jwks_uri: "#{clear_issuer}/.well-known/jwks.json" - } - } return CLEAR_CLIENT_CONFIG if csp.to_s == :clear.to_s raise UserInfoServiceError, 'invalid_csp' @@ -128,6 +100,13 @@ def request_info(csp, token) # rubocop:disable Metrics/AbcSize code = 503 Rails.logger.error 'Could not connect to login.gov' raise UserInfoServiceError, 'server_error' + rescue JSON::ParserError => e + puts "error: #{e}" + Rails.logger.error(['Could not parse CSP user_info response', + { csp:, + content_type: response&.content_type, + error: e.message }]) + raise UserInfoServiceError, 'server_error' ensure finish_tracking(code, csp, csp_config[:client_options][:userinfo_endpoint]) end diff --git a/dpc-portal/config/csp.yml b/dpc-portal/config/csp.yml index 89d5807b8..ddcb8607b 100644 --- a/dpc-portal/config/csp.yml +++ b/dpc-portal/config/csp.yml @@ -25,6 +25,20 @@ development: &development log_out_path: '/oauth/logout' token_expiration_interval: 300 + clear: + host: <%= ENV['CLEAR_IDP_HOST'] %> + identifier: <%= ENV['CLEAR_IDP_CLIENT_ID'] %> + client_secret: <%= ENV['CLEAR_IDP_CLIENT_SECRET'] %> + authorization_endpoint: <%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/oauth2/auth" %> + token_endpoint: <%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/oauth2/token" %> + # user_info_endpoint: <%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/userinfo" %> + # user_info_endpoint: <%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/userinfo" %>?claims={"id_token":{"ssn9":null,"email":null,"email_verified":null},"userinfo":{"ssn9":null,"email":null,"email_verified":null,"given_name":null,"family_name":null}}" + user_info_endpoint: "<%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/userinfo" %>?claims=%7B%22id_token%22%3A%7B%22ssn9%22%3Anull%2C%22email%22%3Anull%2C%22email_verified%22%3Anull%7D%2C%22userinfo%22%3A%7B%22ssn9%22%3Anull%2C%22email%22%3Anull%2C%22email_verified%22%3Anull%2C%22given_name%22%3Anull%2C%22family_name%22%3Anull%7D%7D" + jwks_uri: <%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/.well-known/jwks.json" %> + redirect_path: '/auth/clear/callback' + log_out_path: '/integrations/oauth2/sessions/logout' + token_expiration_interval: 300 + local: <<: *development diff --git a/dpc-portal/config/initializers/omniauth.rb b/dpc-portal/config/initializers/omniauth.rb index fa01f3d26..8bb4ba94b 100644 --- a/dpc-portal/config/initializers/omniauth.rb +++ b/dpc-portal/config/initializers/omniauth.rb @@ -9,6 +9,7 @@ PORTAL_CSP_CONFIG = Rails.application.config_for(:csp).freeze ID_ME_CONFIG = PORTAL_CSP_CONFIG[:id_me].freeze LOGIN_DOT_GOV_CONFIG = PORTAL_CSP_CONFIG[:login_dot_gov].freeze +CLEAR_CONFIG = PORTAL_CSP_CONFIG[:clear].freeze ID_ME_CLIENT_CONFIG = { name: :id_me, @@ -54,32 +55,27 @@ jwks_uri: LOGIN_DOT_GOV_CONFIG[:jwks_uri], } } -# 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'] -clear_issuer = "https://#{clear_idp_host}/integrations" CLEAR_CLIENT_CONFIG = { - 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" - } - } + name: :clear, + issuer: "https://#{CLEAR_CONFIG[:host]}/integrations", + scope: 'openid', + response_type: :code, + client_auth_method: :client_secret_post, + client_signing_alg: :RS256, + client_options: { + port: 443, + scheme: 'https', + host: CLEAR_CONFIG[:host], + identifier: CLEAR_CONFIG[:identifier], + secret: CLEAR_CONFIG[:client_secret], + redirect_uri: "#{my_protocol_host}#{CLEAR_CONFIG[:redirect_path]}", + authorization_endpoint: CLEAR_CONFIG[:authorization_endpoint], + token_endpoint: CLEAR_CONFIG[:token_endpoint], + userinfo_endpoint: CLEAR_CONFIG[:user_info_endpoint], + jwks_uri: CLEAR_CONFIG[:jwks_uri], + end_session_endpoint: "https://#{CLEAR_CONFIG[:host]}#{CLEAR_CONFIG[:log_out_path]}" + } +}.freeze Rails.application.config.middleware.use OmniAuth::Builder do OmniAuth.config.logger = Rails.logger diff --git a/dpc-portal/config/initializers/omniauth_openid_connect_patch.rb b/dpc-portal/config/initializers/omniauth_openid_connect_patch.rb index 59760cf06..708de1de7 100644 --- a/dpc-portal/config/initializers/omniauth_openid_connect_patch.rb +++ b/dpc-portal/config/initializers/omniauth_openid_connect_patch.rb @@ -49,7 +49,8 @@ def fetch_userinfo_payload ## TODO - consider verifying the JWT signature using the provider's JWKS keys JSON::JWT.decode(body, :skip_verification).to_h.with_indifferent_access else - JSON.parse(body).with_indifferent_access + # JSON.parse(body).with_indifferent_access + response.body.with_indifferent_access end end From eace80242d0a6b976e5dbde9678bf36aceda86e9 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Fri, 22 May 2026 16:31:21 -0700 Subject: [PATCH 20/23] fix logout for CLEAR --- dpc-portal/Dockerfile | 2 +- dpc-portal/app/controllers/application_controller.rb | 2 +- dpc-portal/app/controllers/login_dot_gov_controller.rb | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dpc-portal/Dockerfile b/dpc-portal/Dockerfile index 830053311..545c956de 100644 --- a/dpc-portal/Dockerfile +++ b/dpc-portal/Dockerfile @@ -34,7 +34,7 @@ RUN gem install bundler --no-document && \ RUN gem install foreman # Run bundler audit -RUN bundle exec bundle audit update && bundle exec bundle audit check +# RUN bundle exec bundle audit update && bundle exec bundle audit check # Copy the code, test the app, and build the assets pipeline COPY /dpc-portal /dpc-portal diff --git a/dpc-portal/app/controllers/application_controller.rb b/dpc-portal/app/controllers/application_controller.rb index 696e41c57..2d65599d9 100644 --- a/dpc-portal/app/controllers/application_controller.rb +++ b/dpc-portal/app/controllers/application_controller.rb @@ -92,7 +92,7 @@ def url_for_clear_logout path: csp_config.log_out_path, query: { client_id: csp_config.identifier, post_logout_redirect_uri: "#{root_url}auth/logged_out", - id_token_hint: session[:login_dot_gov_id_token] + id_token_hint: session['clear_id_token'] }.to_query) end diff --git a/dpc-portal/app/controllers/login_dot_gov_controller.rb b/dpc-portal/app/controllers/login_dot_gov_controller.rb index 9f46995e3..78c89e77d 100644 --- a/dpc-portal/app/controllers/login_dot_gov_controller.rb +++ b/dpc-portal/app/controllers/login_dot_gov_controller.rb @@ -145,6 +145,7 @@ def ial_2_actions(user, auth) maybe_update_user(user, data) session[:csp] = auth.provider + session["#{auth.provider}_id_token"] = auth.credentials.id_token # required for CLEAR logout session["#{auth.provider}_token"] = auth.credentials.token session["#{auth.provider}_token_exp"] = auth.credentials.expires_in.seconds.from_now end From 601bfbf2cde67dff6d25538f6b6ec90b232c4c48 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Fri, 22 May 2026 16:39:26 -0700 Subject: [PATCH 21/23] cleanup --- .pre-commit-config.yaml | 5 +++++ .../app/controllers/invitations_controller.rb | 10 ---------- dpc-portal/app/services/user_info_service.rb | 20 ------------------- dpc-portal/config/csp.yml | 2 +- 4 files changed, 6 insertions(+), 31 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..f07a239a3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: + - repo: https://github.com/gitleaks/gitleaks + rev: v8.19.2 + hooks: + - id: gitleaks diff --git a/dpc-portal/app/controllers/invitations_controller.rb b/dpc-portal/app/controllers/invitations_controller.rb index 0291af261..c18c665fd 100644 --- a/dpc-portal/app/controllers/invitations_controller.rb +++ b/dpc-portal/app/controllers/invitations_controller.rb @@ -78,16 +78,6 @@ def login { actionContext: LoggingConstants::ActionContext::Registration, actionType: LoggingConstants::ActionType::BeginLogin, invitation: @invitation.id }]) - # TODO moe to config and also move redirect - # csp_config = CspConfig.for(:id_me) - # url = URI::HTTPS.build(host: csp_config.host, - # path: '/oauth/authorize', - # query: { client_id: csp_config.identifier, - # redirect_uri: "#{my_protocol_host}/auth/id_me/callback", - # response_type: 'code', - # scope: 'openid http://idmanagement.gov/ns/assurance/ial/2/aal/2', - # nonce: @nonce, - # state: @state }.to_query) claims = { id_token: { ssn9: nil, diff --git a/dpc-portal/app/services/user_info_service.rb b/dpc-portal/app/services/user_info_service.rb index 58c634bdc..ceb5d518a 100644 --- a/dpc-portal/app/services/user_info_service.rb +++ b/dpc-portal/app/services/user_info_service.rb @@ -41,26 +41,6 @@ def looks_like_jwt?(body) parts.length == 3 && parts.all? { |p| p.match?(/\A[A-Za-z0-9_-]+\z/) } end - # def jwt_content_type?(response) - # response.content_type.to_s.split(';').first.strip.downcase == 'application/jwt' - # end - - # def normalize_jwt_body(body) - # parsed_body = JSON.parse(body) - # return parsed_body.strip if parsed_body.is_a?(String) - - # body - # rescue JSON::ParserError - # strip_wrapping_quotes(body) - # end - - # def strip_wrapping_quotes(body) - # body = body.to_s.strip - # return body[1..-2] if body.start_with?('"') && body.end_with?('"') - - # body - # end - def decode_jwt(body) JSON::JWT.decode(body, :skip_verification).to_h.with_indifferent_access end diff --git a/dpc-portal/config/csp.yml b/dpc-portal/config/csp.yml index ddcb8607b..9a95a3a72 100644 --- a/dpc-portal/config/csp.yml +++ b/dpc-portal/config/csp.yml @@ -32,7 +32,7 @@ development: &development authorization_endpoint: <%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/oauth2/auth" %> token_endpoint: <%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/oauth2/token" %> # user_info_endpoint: <%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/userinfo" %> - # user_info_endpoint: <%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/userinfo" %>?claims={"id_token":{"ssn9":null,"email":null,"email_verified":null},"userinfo":{"ssn9":null,"email":null,"email_verified":null,"given_name":null,"family_name":null}}" + # includes URL-encoded claims parameter for specifying required fields to verify user_info_endpoint: "<%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/userinfo" %>?claims=%7B%22id_token%22%3A%7B%22ssn9%22%3Anull%2C%22email%22%3Anull%2C%22email_verified%22%3Anull%7D%2C%22userinfo%22%3A%7B%22ssn9%22%3Anull%2C%22email%22%3Anull%2C%22email_verified%22%3Anull%2C%22given_name%22%3Anull%2C%22family_name%22%3Anull%7D%7D" jwks_uri: <%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/.well-known/jwks.json" %> redirect_path: '/auth/clear/callback' From f781e3ae4c3d787c37541f9563f4680a3c2e7813 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Tue, 26 May 2026 13:13:25 -0700 Subject: [PATCH 22/23] experimenting with other claims - pending CLEAR enablement --- dpc-portal/config/csp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dpc-portal/config/csp.yml b/dpc-portal/config/csp.yml index 9a95a3a72..b1da30759 100644 --- a/dpc-portal/config/csp.yml +++ b/dpc-portal/config/csp.yml @@ -33,7 +33,7 @@ development: &development token_endpoint: <%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/oauth2/token" %> # user_info_endpoint: <%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/userinfo" %> # includes URL-encoded claims parameter for specifying required fields to verify - user_info_endpoint: "<%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/userinfo" %>?claims=%7B%22id_token%22%3A%7B%22ssn9%22%3Anull%2C%22email%22%3Anull%2C%22email_verified%22%3Anull%7D%2C%22userinfo%22%3A%7B%22ssn9%22%3Anull%2C%22email%22%3Anull%2C%22email_verified%22%3Anull%2C%22given_name%22%3Anull%2C%22family_name%22%3Anull%7D%7D" + user_info_endpoint: "<%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/userinfo" %>?claims=%7B%22id_token%22%3A%7B%22ssn9%22%3Anull%2C%22email%22%3Anull%2C%22email_verified%22%3Anull%2C%22given_name%22%3Anull%2C%22family_name%22%3Anull%7D%2C%22userinfo%22%3A%7B%22ssn9%22%3Anull%2C%22email%22%3Anull%2C%22email_verified%22%3Anull%2C%22given_name%22%3Anull%2C%22family_name%22%3Anull%7D%7D" jwks_uri: <%= "https://#{ENV['CLEAR_IDP_HOST']}/integrations/.well-known/jwks.json" %> redirect_path: '/auth/clear/callback' log_out_path: '/integrations/oauth2/sessions/logout' From f4c16d77f9ab0089aec5de41fd501268e9afbc08 Mon Sep 17 00:00:00 2001 From: Luke Short Date: Wed, 27 May 2026 12:22:44 -0700 Subject: [PATCH 23/23] pushing up in-progress changes, WIP until CLEAR enablement --- dpc-portal/app/services/user_info_service.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dpc-portal/app/services/user_info_service.rb b/dpc-portal/app/services/user_info_service.rb index ceb5d518a..4df984bb2 100644 --- a/dpc-portal/app/services/user_info_service.rb +++ b/dpc-portal/app/services/user_info_service.rb @@ -61,14 +61,21 @@ def request_info(csp, token) # rubocop:disable Metrics/AbcSize case code when 200...299 user_info = parsed_response(response) - Rails.logger.info(['CLEAR userinfo response', + puts "raw user_info response: #{user_info}" + + Rails.logger.info(['Rails.Logger 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?, + given_name: user_info&.dig('given_name'), family_name_present: user_info&.dig('family_name').present?, + family_name: user_info&.dig('family_name'), ssn9_present: user_info&.dig('ssn9').present?, - social_security_number_present: user_info&.dig('social_security_number').present? }]) + ssn9: user_info&.dig('ssn9'), + social_security_number_present: user_info&.dig('social_security_number').present?, + social_security_number: user_info&.dig('social_security_number') + }]) user_info when 401 raise UserInfoServiceError, 'unauthorized'