Skip to content

DO NOT MERGE example sign in service sso #21150

New issue

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

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

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
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
52 changes: 52 additions & 0 deletions app/controllers/v0/sign_in_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class SignInController < SignIn::ApplicationController
only: %i[authorize callback token refresh revoke revoke_all_sessions logout
logingov_logout_proxy]
before_action :access_token_authenticate, only: :revoke_all_sessions
before_action -> { access_token_authenticate(skip_error_handling: true) }, only: :authorize_sso

def authorize # rubocop:disable Metrics/MethodLength
type = params[:type].presence
Expand Down Expand Up @@ -49,6 +50,51 @@ def authorize # rubocop:disable Metrics/MethodLength
handle_pre_login_error(e, client_id)
end

def authorize_sso # rubocop:disable Metrics/MethodLength
client_state = params[:state].presence
code_challenge = params[:code_challenge].presence
code_challenge_method = params[:code_challenge_method].presence
client_id = params[:client_id].presence

validate_authorize_sso_params(client_id)

user_attributes = SignIn::AccessTokenSSOValidator.new(access_token: @access_token,
client_config: client_config(client_id)).perform
state = SignIn::StatePayloadJwtEncoder.new(code_challenge:,
code_challenge_method:,
acr: user_attributes[:acr],
client_config: client_config(client_id),
type: user_attributes[:type],
client_state:,
scope: nil).perform
state_payload = SignIn::StatePayloadJwtDecoder.new(state_payload_jwt: state).perform
user_code_map = SignIn::UserCodeMapCreator.new(user_attributes:,
state_payload:,
verified_icn: user_attributes[:icn],
request_ip: request.remote_ip).perform

params_hash = { code: user_code_map.login_code, type: user_code_map.type }
params_hash.merge!(state: user_code_map.client_state) if user_code_map.client_state.present?

sign_in_logger.info('authorize sso', { client_id: })
StatsD.increment(SignIn::Constants::Statsd::STATSD_SIS_AUTHORIZE_SSO_SUCCESS, tags: ["client_id:#{client_id}"])

render body: SignIn::RedirectUrlGenerator.new(redirect_uri: user_code_map.client_config.redirect_uri,
terms_code: user_code_map.terms_code,
terms_redirect_uri: user_code_map.client_config.terms_of_use_url,
params_hash:).perform,
content_type: 'text/html'
rescue SignIn::Errors::StandardError => e
sign_in_logger.info('authorize sso redirect', { errors: e.message, client_id: })
StatsD.increment(SignIn::Constants::Statsd::STATSD_SIS_AUTHORIZE_SSO_REDIRECT)
render body: { redirect_url: 'some-redirect-url' },
content_type: 'text/html'
rescue => e
log_message_to_sentry(e.message, :error)
StatsD.increment(SignIn::Constants::Statsd::STATSD_SIS_AUTHORIZE_SSO_FAILURE)
handle_pre_login_error(e, client_id)
end

def callback # rubocop:disable Metrics/MethodLength
code = params[:code].presence
state = params[:state].presence
Expand Down Expand Up @@ -241,6 +287,12 @@ def validate_authorize_params(type, client_id, acr, operation)
end
end

def validate_authorize_sso_params(client_id)
if client_config(client_id).blank?
raise SignIn::Errors::MalformedParamsError.new message: 'Client id is not valid'
end
end

def validate_callback_params(code, state, error)
raise SignIn::Errors::MalformedParamsError.new message: 'Code is not defined' unless code || error
raise SignIn::Errors::MalformedParamsError.new message: 'State is not defined' unless state
Expand Down
6 changes: 5 additions & 1 deletion app/models/sign_in/client_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,14 @@ def valid_service_level?(acr)
service_levels.include?(acr)
end

def device_sso_enabled?
def api_sso_enabled?
api_auth? && shared_sessions
end

def web_sso_server_enabled?
cookie_auth? && shared_sessions
end

private

def appropriate_mock_environment?
Expand Down
1 change: 1 addition & 0 deletions app/models/sign_in/code_container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class CodeContainer < Common::RedisStore
attribute :credential_email, String
attribute :user_attributes, Hash
attribute :device_sso, Boolean
attribute :web_sso_session_id, Integer

validates(:code, :user_verification_id, presence: true)

Expand Down
7 changes: 5 additions & 2 deletions app/models/sign_in/session_container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ class SessionContainer
:access_token,
:anti_csrf_token,
:client_config,
:device_secret
:device_secret,
:web_sso_client
)

validates(
Expand All @@ -27,13 +28,15 @@ def initialize(session:, # rubocop:disable Metrics/ParameterLists
access_token:,
anti_csrf_token:,
client_config:,
device_secret: nil)
device_secret: nil,
web_sso_client: false)
@session = session
@refresh_token = refresh_token
@access_token = access_token
@anti_csrf_token = anti_csrf_token
@client_config = client_config
@device_secret = device_secret
@web_sso_client = web_sso_client

validate!
end
Expand Down
9 changes: 6 additions & 3 deletions app/models/sign_in/validated_credential.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ class ValidatedCredential
:credential_email,
:client_config,
:user_attributes,
:device_sso
:device_sso,
:web_sso_session_id
)

validates(
Expand All @@ -18,16 +19,18 @@ class ValidatedCredential
presence: true
)

def initialize(user_verification:,
def initialize(user_verification:, # rubocop:disable Metrics/ParameterLists
client_config:,
credential_email:,
user_attributes:,
device_sso:)
device_sso:,
web_sso_session_id:)
@user_verification = user_verification
@client_config = client_config
@credential_email = credential_email
@user_attributes = user_attributes
@device_sso = device_sso
@web_sso_session_id = web_sso_session_id

validate!
end
Expand Down
82 changes: 82 additions & 0 deletions app/services/sign_in/access_token_sso_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# frozen_string_literal: true

module SignIn
class AccessTokenSSOValidator
attr_reader :access_token, :client_config

def initialize(access_token:, client_config:)
@access_token = access_token
@client_config = client_config
end

def perform
validate_session!
validate_shared_sessions!
validate_credential_level_and_type!

sso_validator_attributes
end

private

def validate_session!
raise Errors::AccessTokenUnauthenticatedError.new message: 'Access token invalid' unless access_token
raise Errors::SessionNotFoundError.new message: 'Session not found' unless session
end

def validate_shared_sessions!
unless client_config.api_sso_enabled? && current_session_client_config.web_sso_server_enabled?
raise Errors::InvalidClientConfigError.new message: 'SSO requested for client without shared sessions'
end
end

def validate_credential_level_and_type!
unless client_config.valid_service_level?(session_assurance_level)
raise Errors::InvalidClientConfigError.new message: 'SSO requested for session with excluded assurance level'
end
unless client_config.valid_credential_service_provider?(user_verification.credential_type)
raise Errors::InvalidClientConfigError.new message: 'SSO requested for session with excluded service provider'
end
end

def session_assurance_level
if user_verification.credential_type == Constants::Auth::LOGINGOV
user_verification.verified? ? Constants::Auth::IAL2 : Constants::Auth::IAL1
else
user_verification.verified? ? Constants::Auth::LOA3 : Constants::Auth::LOA1
end
end

def sso_validator_attributes
{
idme_uuid: user_verification.idme_uuid || user_verification.backing_idme_uuid,
logingov_uuid: user_verification.logingov_uuid,
credential_email: session.credential_email,
edipi: user_verification.dslogon_uuid,
mhv_credential_uuid: user_verification.mhv_uuid,
first_name: session_user_attributes[:first_name],
last_name: session_user_attributes[:last_name],
acr: session_assurance_level,
type: user_verification.credential_type,
icn: user_verification.user_account.icn,
session_id: session.id
}
end

def user_verification
@user_verification ||= session.user_verification
end

def session_user_attributes
@session_user_attributes ||= session.user_attributes_hash
end

def current_session_client_config
@current_session_client_config ||= ClientConfig.find_by(client_id: session.client_id)
end

def session
@session ||= SignIn::OAuthSession.find_by(handle: access_token.session_handle)
end
end
end
3 changes: 2 additions & 1 deletion app/services/sign_in/code_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ def validated_credential
credential_email: code_container.credential_email,
client_config:,
user_attributes: code_container.user_attributes,
device_sso: code_container.device_sso)
device_sso: code_container.device_sso,
web_sso_session_id: code_container.web_sso_session_id)
end

def client_config
Expand Down
3 changes: 3 additions & 0 deletions app/services/sign_in/constants/statsd.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ module Constants
module Statsd
STATSD_SIS_AUTHORIZE_SUCCESS = 'api.sis.auth.success'
STATSD_SIS_AUTHORIZE_FAILURE = 'api.sis.auth.failure'
STATSD_SIS_AUTHORIZE_SSO_SUCCESS = 'api.sis.auth_sso.success'
STATSD_SIS_AUTHORIZE_SSO_FAILURE = 'api.sis.auth_sso.failure'
STATSD_SIS_AUTHORIZE_SSO_REDIRECT = 'api.sis.auth_sso.redirect'
STATSD_SIS_CALLBACK_SUCCESS = 'api.sis.callback.success'
STATSD_SIS_CALLBACK_FAILURE = 'api.sis.callback.failure'
STATSD_SIS_TOKEN_SUCCESS = 'api.sis.token.success'
Expand Down
1 change: 1 addition & 0 deletions app/services/sign_in/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class RefreshTokenDecryptionError < StandardError; end
class AccessTokenSignatureMismatchError < StandardError; end
class AccessTokenMalformedJWTError < StandardError; end
class AccessTokenExpiredError < StandardError; end
class AccessTokenUnauthenticatedError < StandardError; end
class AntiCSRFMismatchError < StandardError; end
class SessionNotAuthorizedError < StandardError; end
class TokenTheftDetectedError < StandardError; end
Expand Down
9 changes: 7 additions & 2 deletions app/services/sign_in/session_creator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ def perform
access_token: create_new_access_token,
anti_csrf_token:,
client_config:,
device_secret:)
device_secret:,
web_sso_client: validated_credential.web_sso_session_id.present?)
end

private
Expand Down Expand Up @@ -95,7 +96,11 @@ def create_new_session
end

def refresh_created_time
@refresh_created_time ||= Time.zone.now
@refresh_created_time ||= web_sso_session_creation || Time.zone.now
end

def web_sso_session_creation
OAuthSession.find_by(id: validated_credential.web_sso_session_id)&.refresh_creation
end

def refresh_expiration_time
Expand Down
2 changes: 1 addition & 1 deletion app/services/sign_in/state_payload_jwt_encoder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def state_code
end

def sso_not_enabled_for_device_sso_scope?
scope == Constants::Auth::DEVICE_SSO && !client_config.device_sso_enabled?
scope == Constants::Auth::DEVICE_SSO && !client_config.api_sso_enabled?
end

def remove_base64_padding(data)
Expand Down
2 changes: 1 addition & 1 deletion app/services/sign_in/token_exchanger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def validate_shared_sessions_client!
end

def validate_device_sso!
unless current_client_config.device_sso_enabled?
unless current_client_config.api_sso_enabled?
raise Errors::InvalidSSORequestError.new message: 'token exchange requested from invalid client'
end
end
Expand Down
6 changes: 5 additions & 1 deletion app/services/sign_in/token_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def token_json_response

def token_json_payload
payload = {}
payload[:refresh_token] = encrypted_refresh_token
payload[:refresh_token] = encrypted_refresh_token unless web_sso_client
payload[:access_token] = encoded_access_token
payload[:anti_csrf_token] = anti_csrf_token if anti_csrf_enabled_client?
payload[:device_secret] = device_secret if device_secret_enabled_client?
Expand Down Expand Up @@ -124,6 +124,10 @@ def encoded_access_token
@encoded_access_token ||= AccessTokenJwtEncoder.new(access_token: session_container.access_token).perform
end

def web_sso_client
@web_sso_client ||= session_container.web_sso_client
end

def anti_csrf_token
@anti_csrf_token ||= session_container.anti_csrf_token
end
Expand Down
7 changes: 5 additions & 2 deletions app/services/sign_in/user_code_map_creator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ class UserCodeMapCreator
:mhv_credential_uuid,
:request_ip,
:first_name,
:last_name
:last_name,
:web_sso_session_id

def initialize(user_attributes:, state_payload:, verified_icn:, request_ip:)
@state_payload = state_payload
Expand All @@ -26,6 +27,7 @@ def initialize(user_attributes:, state_payload:, verified_icn:, request_ip:)
@request_ip = request_ip
@first_name = user_attributes[:first_name]
@last_name = user_attributes[:last_name]
@web_sso_session_id = user_attributes[:session_id]
end

def perform
Expand Down Expand Up @@ -59,7 +61,8 @@ def create_code_container
user_verification_id: user_verification.id,
credential_email:,
user_attributes: access_token_attributes,
device_sso:).save!
device_sso:,
web_sso_session_id:).save!
end

def device_sso
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
get '/v1/sessions/ssoe_logout', to: 'v1/sessions#ssoe_slo_callback'

get '/v0/sign_in/authorize', to: 'v0/sign_in#authorize'
get '/v0/sign_in/authorize_sso', to: 'v0/sign_in#authorize_sso'
get '/v0/sign_in/callback', to: 'v0/sign_in#callback'
post '/v0/sign_in/refresh', to: 'v0/sign_in#refresh'
post '/v0/sign_in/revoke', to: 'v0/sign_in#revoke'
Expand Down
14 changes: 14 additions & 0 deletions db/seeds/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@
terms_of_use_url: 'http://localhost:3001/terms-of-use',
refresh_token_duration: SignIn::Constants::RefreshToken::VALIDITY_LENGTH_LONG_DAYS)

vaokta = SignIn::ClientConfig.find_or_initialize_by(client_id: 'okta_test')
vaokta.update!(authentication: SignIn::Constants::Auth::API,
anti_csrf: false,
redirect_uri: 'fake://this-is-fake',
access_token_duration: SignIn::Constants::AccessToken::VALIDITY_LENGTH_SHORT_MINUTES,
access_token_audience: 'okta',
pkce: true,
logout_redirect_uri: 'http://localhost:3001',
enforced_terms: SignIn::Constants::Auth::VA_TERMS,
terms_of_use_url: 'http://localhost:3001/terms-of-use',
shared_sessions: true,
json_api_compatibility: false,
refresh_token_duration: SignIn::Constants::RefreshToken::VALIDITY_LENGTH_SHORT_MINUTES)

# Create Config for localhost mocked authentication client
vamobile_mock = SignIn::ClientConfig.find_or_initialize_by(client_id: 'vamobile_test')
vamobile_mock.update!(authentication: SignIn::Constants::Auth::API,
Expand Down
Loading