Module: Common::Client::Concerns::MhvFhirSessionClient

Extended by:
ActiveSupport::Concern
Includes:
MHVJwtSessionClient
Included in:
MedicalRecords::Client
Defined in:
lib/common/client/concerns/mhv_fhir_session_client.rb

Overview

Module mixin for overriding session logic when making MHV JWT/FHIR-based client connections.

All refrences to “session” in this module refer to the upstream MHV/FHIR session.

Constant Summary collapse

LOCK_RETRY_DELAY =

Number of seconds to wait between attempts to acquire a session lock

1
RETRY_ATTEMPTS =

How many times to attempt to acquire a session lock

10

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from MHVJwtSessionClient

#auth_body, #auth_headers, #decode_jwt_token, #extract_token_expiration, #get_jwt_from_headers, #get_session_tagged, #jwt_bearer_token, #patient_fhir_id, #session_config_key, #user_key, #validate_session_params

Methods included from MhvLockedSessionClient

#authenticate, #initialize, #lock_and_get_session, #obtain_redis_lock, #refresh_session, #release_redis_lock

Methods included from SentryLogging

#log_exception_to_sentry, #log_message_to_sentry, #non_nil_hash?, #normalize_level, #rails_logger, #set_sentry_metadata

Instance Attribute Details

#sessionHash (readonly)

Returns a hash containing session information.

Returns:

  • (Hash)

    a hash containing session information



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/common/client/concerns/mhv_fhir_session_client.rb', line 19

module MhvFhirSessionClient
  extend ActiveSupport::Concern
  include MHVJwtSessionClient

  protected

  LOCK_RETRY_DELAY = 1 # Number of seconds to wait between attempts to acquire a session lock
  RETRY_ATTEMPTS = 10 # How many times to attempt to acquire a session lock

  def incomplete?(session)
    session.icn.blank? || session.patient_fhir_id.blank? || session.token.blank? || session.expires_at.blank?
  end

  def invalid?(session)
    session.expired? || incomplete?(session)
  end

  ##
  # Creates and saves an MHV/FHIR session for a patient. If any step along the way fails, save
  # the partial session before raising the exception.
  #
  # @return [MedicalRecords::ClientSession] if a MR (Medical Records) client session
  #
  def get_session
    exception = nil

    perform_phr_refresh

    begin
      # Call MHVJwtSessionClient's get_session method for JWT-session creation.
      jwt_session = super
      session.token = jwt_session.token
      session.expires_at = jwt_session.expires_at
    rescue => e
      exception ||= e
    end

    begin
      get_patient_fhir_id(session.token) if session.token && patient_fhir_id.nil?
    rescue MedicalRecords::PatientNotFound
      # Don't raise this exception, as it will bubble up to a shared class where we don't
      # want to handle it. Instead we simply don't set the Patient FHIR ID and raise an
      # exception later in medical_records/client.rb where it will be easier to handle.
    rescue => e
      exception ||= e
    end

    new_session = save_session
    raise exception if exception

    new_session
  end

  private

  ##
  # Checks to see if a PHR refresh is necessary, performs the refresh, and updates the refresh timestamp.
  #
  # @return [DateTime] the refresh timestamp
  #
  def perform_phr_refresh
    return unless session.refresh_time.nil?

    # Perform an async PHR refresh for the user. This job will not raise any errors, it only logs them.
    MHV::PhrUpdateJob.perform_async(session.icn, session.user_id)
    # Record that the refresh has happened for this session. Don't run this more than once per session duration.
    session.refresh_time = DateTime.now
  end

  ##
  # Decodes a JWT to get the patient's subjectId, then uses that to fetch their FHIR ID
  #
  # @param jwt_token [String] a JWT token
  # @return [String] the patient's FHIR ID
  #
  def get_patient_fhir_id(jwt_token)
    decoded_token = decode_jwt_token(jwt_token)
    session.patient_fhir_id = fetch_patient_by_subject_id(sessionless_fhir_client(jwt_token),
                                                          decoded_token[0]['subjectId'])
  end

  ##
  # Fetch a Patient FHIR record by subjectId
  #
  # @param fhir_client [FHIR::Client] a FHIR client with which to get the record
  # @param subject_id [String] the patient's subjectId from the JWT
  # @return [FHIR::Patient] the patient's FHIR record
  #
  def fetch_patient_by_subject_id(fhir_client, subject_id)
    raise Common::Exceptions::Unauthorized, detail: 'JWT token does not contain subjectId' if subject_id.nil?

    # Get the patient's FHIR ID
    patient = get_patient_by_identifier(fhir_client, subject_id)
    if patient.nil? || patient.entry.empty? || !patient.entry[0].resource.respond_to?(:id)
      raise Common::Exceptions::Unauthorized,
            detail: 'Patient record not found or does not contain a valid FHIR ID'
    end
    session.patient_fhir_id = patient.entry[0].resource.id
  end

  ##
  # Takes information from the session variable and saves a new session instance in redis.
  #
  # @return [MedicalRecords::ClientSession] the updated session
  #
  def save_session
    new_session = @session.class.new(user_id: session.user_id.to_s,
                                     patient_fhir_id: session.patient_fhir_id,
                                     icn: session.icn,
                                     expires_at: session.expires_at,
                                     token: session.token,
                                     refresh_time: session.refresh_time)
    new_session.save
    new_session
  end
end

Instance Method Details

#fetch_patient_by_subject_id(fhir_client, subject_id) ⇒ FHIR::Patient (private)

Fetch a Patient FHIR record by subjectId

Parameters:

  • fhir_client (FHIR::Client)

    a FHIR client with which to get the record

  • subject_id (String)

    the patient’s subjectId from the JWT

Returns:

  • (FHIR::Patient)

    the patient’s FHIR record

Raises:



107
108
109
110
111
112
113
114
115
116
117
# File 'lib/common/client/concerns/mhv_fhir_session_client.rb', line 107

def fetch_patient_by_subject_id(fhir_client, subject_id)
  raise Common::Exceptions::Unauthorized, detail: 'JWT token does not contain subjectId' if subject_id.nil?

  # Get the patient's FHIR ID
  patient = get_patient_by_identifier(fhir_client, subject_id)
  if patient.nil? || patient.entry.empty? || !patient.entry[0].resource.respond_to?(:id)
    raise Common::Exceptions::Unauthorized,
          detail: 'Patient record not found or does not contain a valid FHIR ID'
  end
  session.patient_fhir_id = patient.entry[0].resource.id
end

#get_patient_fhir_id(jwt_token) ⇒ String (private)

Decodes a JWT to get the patient’s subjectId, then uses that to fetch their FHIR ID

Parameters:

  • jwt_token (String)

    a JWT token

Returns:

  • (String)

    the patient’s FHIR ID



94
95
96
97
98
# File 'lib/common/client/concerns/mhv_fhir_session_client.rb', line 94

def get_patient_fhir_id(jwt_token)
  decoded_token = decode_jwt_token(jwt_token)
  session.patient_fhir_id = fetch_patient_by_subject_id(sessionless_fhir_client(jwt_token),
                                                        decoded_token[0]['subjectId'])
end

#get_sessionMedicalRecords::ClientSession (protected)

Creates and saves an MHV/FHIR session for a patient. If any step along the way fails, save the partial session before raising the exception.

Returns:



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/common/client/concerns/mhv_fhir_session_client.rb', line 42

def get_session
  exception = nil

  perform_phr_refresh

  begin
    # Call MHVJwtSessionClient's get_session method for JWT-session creation.
    jwt_session = super
    session.token = jwt_session.token
    session.expires_at = jwt_session.expires_at
  rescue => e
    exception ||= e
  end

  begin
    get_patient_fhir_id(session.token) if session.token && patient_fhir_id.nil?
  rescue MedicalRecords::PatientNotFound
    # Don't raise this exception, as it will bubble up to a shared class where we don't
    # want to handle it. Instead we simply don't set the Patient FHIR ID and raise an
    # exception later in medical_records/client.rb where it will be easier to handle.
  rescue => e
    exception ||= e
  end

  new_session = save_session
  raise exception if exception

  new_session
end

#incomplete?(session) ⇒ Boolean (protected)

Returns:

  • (Boolean)


28
29
30
# File 'lib/common/client/concerns/mhv_fhir_session_client.rb', line 28

def incomplete?(session)
  session.icn.blank? || session.patient_fhir_id.blank? || session.token.blank? || session.expires_at.blank?
end

#invalid?(session) ⇒ Boolean (protected)

Returns:

  • (Boolean)


32
33
34
# File 'lib/common/client/concerns/mhv_fhir_session_client.rb', line 32

def invalid?(session)
  session.expired? || incomplete?(session)
end

#perform_phr_refreshDateTime (private)

Checks to see if a PHR refresh is necessary, performs the refresh, and updates the refresh timestamp.

Returns:

  • (DateTime)

    the refresh timestamp



79
80
81
82
83
84
85
86
# File 'lib/common/client/concerns/mhv_fhir_session_client.rb', line 79

def perform_phr_refresh
  return unless session.refresh_time.nil?

  # Perform an async PHR refresh for the user. This job will not raise any errors, it only logs them.
  MHV::PhrUpdateJob.perform_async(session.icn, session.user_id)
  # Record that the refresh has happened for this session. Don't run this more than once per session duration.
  session.refresh_time = DateTime.now
end

#save_sessionMedicalRecords::ClientSession (private)

Takes information from the session variable and saves a new session instance in redis.

Returns:



124
125
126
127
128
129
130
131
132
133
# File 'lib/common/client/concerns/mhv_fhir_session_client.rb', line 124

def save_session
  new_session = @session.class.new(user_id: session.user_id.to_s,
                                   patient_fhir_id: session.patient_fhir_id,
                                   icn: session.icn,
                                   expires_at: session.expires_at,
                                   token: session.token,
                                   refresh_time: session.refresh_time)
  new_session.save
  new_session
end