Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class ApplicationController < ActionController::Base
prepend_before_action :set_locale
before_action :disable_caching
before_action :cache_issuer_in_cookie
after_action :store_web_locale_in_session

def session_expires_at
return if @skip_session_expiration || @skip_session_load
Expand Down Expand Up @@ -400,6 +401,12 @@ def set_locale
I18n.locale = LocaleChooser.new(params[:locale], request).locale
end

def store_web_locale_in_session
return unless user_signed_in?

user_session[:web_locale] = I18n.locale.to_s
end

def pii_requested_but_locked?
if resolved_authn_context_result.identity_proofing? || resolved_authn_context_result.ialmax?
current_user.identity_verified? &&
Expand Down
5 changes: 5 additions & 0 deletions app/presenters/openid_connect_user_info_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def user_info
}

info[:all_emails] = all_emails_from_sp_identity(identity) if scoper.all_emails_requested?
info[:locale] = web_locale if scoper.locale_requested?
info.merge!(ial2_attributes) if identity_proofing_requested_for_verified_user?
info.merge!(x509_attributes) if scoper.x509_scopes_requested?
info[:verified_at] = verified_at if scoper.verified_at_requested?
Expand Down Expand Up @@ -57,6 +58,10 @@ def all_emails_from_sp_identity(identity)
identity.user.confirmed_email_addresses.map(&:email)
end

def web_locale
out_of_band_session_accessor.load_web_locale
end

def ial2_attributes
{
given_name: stringify_attr(ial2_data.first_name),
Expand Down
2 changes: 2 additions & 0 deletions app/presenters/saml_requested_attributes_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class SamlRequestedAttributesPresenter
ATTRIBUTE_TO_FRIENDLY_NAME_MAP = {
email: :email,
all_emails: :all_emails,
locale: :locale,
first_name: :given_name,
last_name: :family_name,
dob: :birthdate,
Expand All @@ -30,6 +31,7 @@ def requested_attributes
else
attrs = [:email]
attrs << :all_emails if bundle.include?(:all_emails)
attrs << :locale if bundle.include?(:locale)
attrs << :verified_at if bundle.include?(:verified_at)
attrs
end
Expand Down
5 changes: 5 additions & 0 deletions app/services/attribute_asserter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def build
attrs = default_attrs
add_email(attrs) if bundle.include? :email
add_all_emails(attrs) if bundle.include? :all_emails
add_locale(attrs) if bundle.include? :locale
add_bundle(attrs) if should_add_proofed_attributes?
add_verified_at(attrs) if bundle.include?(:verified_at) && ial2_service_provider?
if authn_request.requested_vtr_authn_contexts.present?
Expand Down Expand Up @@ -208,6 +209,10 @@ def add_email(attrs)
}
end

def add_locale(attrs)
attrs[:locale] = { getter: ->(_principal) { user_session[:web_locale] } }
end

def add_all_emails(attrs)
attrs[:all_emails] = {
getter: ->(principal) { principal.confirmed_email_addresses.map(&:email) },
Expand Down
7 changes: 7 additions & 0 deletions app/services/openid_connect_attribute_scoper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ class OpenidConnectAttributeScoper
VALID_SCOPES = (%w[
email
all_emails
locale
openid
profile:verified_at
] + X509_SCOPES + IAL2_SCOPES).freeze

VALID_IAL1_SCOPES = (%w[
email
all_emails
locale
openid
profile:verified_at
] + X509_SCOPES).freeze
Expand All @@ -35,6 +37,7 @@ class OpenidConnectAttributeScoper
email: %w[email],
email_verified: %w[email],
all_emails: %w[all_emails],
locale: %w[locale],
address: %w[address],
phone: %w[phone],
phone_verified: %w[phone],
Expand Down Expand Up @@ -82,6 +85,10 @@ def all_emails_requested?
scopes.include?('all_emails')
end

def locale_requested?
scopes.include?('locale')
end

def filter(user_info)
user_info.select do |key, _v|
!ATTRIBUTE_SCOPES_MAP.key?(key) || (scopes & ATTRIBUTE_SCOPES_MAP[key]).present?
Expand Down
19 changes: 16 additions & 3 deletions app/services/out_of_band_session_accessor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ def load_pii(profile_id)
Pii::Cacher.new(nil, session).fetch(profile_id) if session
end

# @return [String, nil]
def load_web_locale
session_data.dig('warden.user.user.session', :web_locale)
end

# @return [X509::Attributes]
def load_x509
X509::Attributes.new_from_json(session_data.dig('warden.user.user.session', :decrypted_x509))
Expand Down Expand Up @@ -83,6 +88,13 @@ def put_x509(piv_cert_info, expiration = 5.minutes)
put(data, expiration)
end

# @api private
# Only used for convenience in tests
# @param [String] locale
def put_locale(locale, expiration = 5.minutes)
put({ web_locale: locale }, expiration)
end

# @api private
# Only used for convenience in tests
def exists?
Expand All @@ -92,15 +104,16 @@ def exists?
private

def put(data, expiration = 5.minutes)
session_data = {
'warden.user.user.session' => data.to_h,
existing_user_session = session_data.dig('warden.user.user.session')
new_session_data = {
'warden.user.user.session' => data.to_h.merge(existing_user_session.to_h),
}

session_store.send(
:write_session,
PLACEHOLDER_REQUEST,
Rack::Session::SessionId.new(session_uuid),
session_data,
new_session_data,
expire_after: expiration.to_i,
)
end
Expand Down
1 change: 1 addition & 0 deletions spec/controllers/sign_up/passwords_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
'last_request_at' => kind_of(Numeric),
new_device: false,
in_account_creation_flow: true,
web_locale: 'en',
)
end
end
Expand Down
40 changes: 39 additions & 1 deletion spec/features/openid_connect/openid_connect_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,39 @@
expect(userinfo_response[:verified_at]).to be_nil
end

it 'returns the locale if requested', driver: :mobile_rack_test do
user = user_with_2fa

token_response = sign_in_get_token_response(
user: user,
scope: 'openid email locale',
sign_in_steps: proc do
visit root_path(locale: 'es')

fill_in_credentials_and_submit(
user.confirmed_email_addresses.first.email,
user.password,
)
fill_in_code_with_last_phone_otp
click_submit_default
end,
handoff_page_steps: proc do
click_agree_and_continue
end,
)

access_token = token_response[:access_token]
expect(access_token).to be_present

page.driver.get api_openid_connect_userinfo_path,
{},
'HTTP_AUTHORIZATION' => "Bearer #{access_token}"

userinfo_response = JSON.parse(page.body).with_indifferent_access
expect(userinfo_response[:email]).to eq(user.email)
expect(userinfo_response[:locale]).to eq('es')
end

it 'errors if verified_within param is too recent', driver: :mobile_rack_test do
client_id = 'urn:gov:gsa:openidconnect:test'
allow(IdentityConfig.store).to receive(:allowed_verified_within_providers)
Expand Down Expand Up @@ -1087,6 +1120,7 @@ def sign_in_get_token_response(
acr_values: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF,
scope: 'openid email',
redirect_uri: 'gov.gsa.openidconnect.test://result',
sign_in_steps: nil,
handoff_page_steps: nil,
proofing_steps: nil,
verified_within: nil,
Expand Down Expand Up @@ -1114,7 +1148,11 @@ def sign_in_get_token_response(
verified_within: verified_within,
)

_user = sign_in_live_with_2fa(user)
if sign_in_steps.present?
sign_in_steps.call
else
sign_in_live_with_2fa(user)
end

proofing_steps&.call
handoff_page_steps&.call
Expand Down
31 changes: 31 additions & 0 deletions spec/features/saml/ial1_sso_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,35 @@
expect(xmldoc.attribute_value_for('verified_at')).to be_blank
end
end

context 'requesting locale' do
it 'includes locale in the response' do
user = create(:user, :fully_registered)
saml_authn_request = saml_authn_request_url(
overrides: {
issuer: sp1_issuer,
authn_context: [
Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF,
"#{Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF}email,locale",
],
},
)

visit saml_authn_request
visit root_url(locale: 'es')
fill_in_credentials_and_submit(user.email, user.password)
fill_in_code_with_last_phone_otp
click_submit_default

expect(current_url).to match new_user_session_path
click_submit_default
click_agree_and_continue
click_submit_default

xmldoc = SamlResponseDoc.new('feature', 'response_assertion')

expect(xmldoc.attribute_node_for('locale')).to be_present
expect(xmldoc.attribute_value_for('locale')).to eq('es')
end
end
end
30 changes: 23 additions & 7 deletions spec/presenters/openid_connect_user_info_presenter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@

let(:rails_session_id) { SecureRandom.uuid }
let(:scope) do
'openid email all_emails address phone profile social_security_number x509'
'openid email all_emails locale address phone profile social_security_number x509'
end
let(:service_provider_ial) { 2 }
let(:service_provider) { create(:service_provider, ial: service_provider_ial) }
let(:profile) { create(:profile, :active, :verified) }
let(:vtr) { ['C1.C2.P1'] }
let(:acr_values) { nil }
let(:requested_aal_value) { nil }
let(:locale) { 'en' }
let(:identity) do
build(
:service_provider_identity,
Expand All @@ -26,6 +27,10 @@
)
end

before do
OutOfBandSessionAccessor.new(rails_session_id).put_locale(locale, 5.minutes.in_seconds)
end

subject(:presenter) { OpenidConnectUserInfoPresenter.new(identity) }

describe '#user_info' do
Expand Down Expand Up @@ -61,7 +66,7 @@

context 'no identity proofing' do
let(:vtr) { ['C1.C2'] }
let(:scope) { 'openid email all_emails' }
let(:scope) { 'openid email all_emails locale' }

it 'includes the correct attributes' do
aggregate_failures do
Expand All @@ -70,6 +75,7 @@
expect(user_info[:email]).to eq(identity.user.email_addresses.first.email)
expect(user_info[:email_verified]).to eq(true)
expect(user_info[:all_emails]).to eq([identity.user.email_addresses.first.email])
expect(user_info[:locale]).to eq(locale)
expect(user_info).to_not have_key(:ial)
expect(user_info).to_not have_key(:aal)
expect(user_info[:vot]).to eq('C1.C2')
Expand All @@ -79,7 +85,9 @@

context 'identity proofing' do
let(:vtr) { ['C1.C2.P1'] }
let(:scope) { 'openid email all_emails address phone profile social_security_number' }
let(:scope) do
'openid email all_emails locale address phone profile social_security_number'
end

it 'includes the correct non-proofed attributes' do
aggregate_failures do
Expand All @@ -88,6 +96,7 @@
expect(user_info[:email]).to eq(identity.user.email_addresses.first.email)
expect(user_info[:email_verified]).to eq(true)
expect(user_info[:all_emails]).to eq([identity.user.email_addresses.first.email])
expect(user_info[:locale]).to eq(locale)
expect(user_info).to_not have_key(:ial)
expect(user_info).to_not have_key(:aal)
expect(user_info[:vot]).to eq('C1.C2.P1')
Expand Down Expand Up @@ -126,7 +135,7 @@
].join(' ')
end
let(:requested_aal_value) { Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF }
let(:scope) { 'openid email all_emails' }
let(:scope) { 'openid email all_emails locale' }

it 'includes the correct attributes' do
aggregate_failures do
Expand All @@ -135,6 +144,7 @@
expect(user_info[:email]).to eq(identity.user.email_addresses.first.email)
expect(user_info[:email_verified]).to eq(true)
expect(user_info[:all_emails]).to eq([identity.user.email_addresses.first.email])
expect(user_info[:locale]).to eq(locale)
expect(user_info[:ial]).to eq(Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF)
expect(user_info[:aal]).to eq(Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF)
expect(user_info).to_not have_key(:vot)
Expand All @@ -150,7 +160,9 @@
].join(' ')
end
let(:requested_aal_value) { Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF }
let(:scope) { 'openid email all_emails address phone profile social_security_number' }
let(:scope) do
'openid email all_emails locale address phone profile social_security_number'
end

it 'includes the correct non-proofed attributes' do
aggregate_failures do
Expand All @@ -159,6 +171,7 @@
expect(user_info[:email]).to eq(identity.user.email_addresses.first.email)
expect(user_info[:email_verified]).to eq(true)
expect(user_info[:all_emails]).to eq([identity.user.email_addresses.first.email])
expect(user_info[:locale]).to eq(locale)
expect(user_info[:ial]).to eq(Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF)
expect(user_info[:aal]).to eq(Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF)
expect(user_info).to_not have_key(:vot)
Expand Down Expand Up @@ -199,6 +212,7 @@
expect(user_info[:email]).to eq(identity.user.email_addresses.first.email)
expect(user_info[:email_verified]).to eq(true)
expect(user_info[:all_emails]).to eq([identity.user.email_addresses.first.email])
expect(user_info[:locale]).to eq(locale)
expect(user_info[:ial]).to eq(
Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF,
)
Expand Down Expand Up @@ -236,7 +250,9 @@
].join(' ')
end
let(:requested_aal_value) { Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF }
let(:scope) { 'openid email all_emails address phone profile social_security_number' }
let(:scope) do
'openid email all_emails locale address phone profile social_security_number'
end

context 'the user has verified their identity' do
it 'includes the proofed attributes' do
Expand Down Expand Up @@ -286,7 +302,7 @@

context 'when minimal scopes are requested for proofed attributes' do
let(:scope) do
'openid email all_emails profile'
'openid email all_emails locale profile'
end

it 'only returns the requested attributes' do
Expand Down
Loading