Class: WorkOS::Session

Inherits:
Object
  • Object
show all
Defined in:
lib/workos/session.rb

Overview

The Session class provides helper methods for working with WorkOS sessions This class is not meant to be instantiated in a user space, and is instantiated internally but exposed.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(user_management:, client_id:, session_data:, cookie_password:) ⇒ Session

Returns a new instance of Session.

Raises:

  • (ArgumentError)


17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/workos/session.rb', line 17

def initialize(user_management:, client_id:, session_data:, cookie_password:)
  raise ArgumentError, 'cookiePassword is required' if cookie_password.nil? || cookie_password.empty?

  @user_management = user_management
  @cookie_password = cookie_password
  @session_data = session_data
  @client_id = client_id

  @jwks = Cache.fetch("jwks_#{client_id}", expires_in: 5 * 60) do
    create_remote_jwk_set(URI(@user_management.get_jwks_url(client_id)))
  end
  @jwks_algorithms = @jwks.map { |key| key[:alg] }.compact.uniq
end

Instance Attribute Details

#client_idObject

Returns the value of attribute client_id.



15
16
17
# File 'lib/workos/session.rb', line 15

def client_id
  @client_id
end

Returns the value of attribute cookie_password.



15
16
17
# File 'lib/workos/session.rb', line 15

def cookie_password
  @cookie_password
end

#jwksObject

Returns the value of attribute jwks.



15
16
17
# File 'lib/workos/session.rb', line 15

def jwks
  @jwks
end

#jwks_algorithmsObject

Returns the value of attribute jwks_algorithms.



15
16
17
# File 'lib/workos/session.rb', line 15

def jwks_algorithms
  @jwks_algorithms
end

#session_dataObject

Returns the value of attribute session_data.



15
16
17
# File 'lib/workos/session.rb', line 15

def session_data
  @session_data
end

#user_managementObject

Returns the value of attribute user_management.



15
16
17
# File 'lib/workos/session.rb', line 15

def user_management
  @user_management
end

Class Method Details

.seal_data(data, key) ⇒ String

Encrypts and seals data using AES-256-GCM

Parameters:

  • data (Hash)

    The data to seal

  • key (String)

    The key to use for encryption

Returns:

  • (String)

    The sealed data



141
142
143
144
145
146
147
148
149
150
151
# File 'lib/workos/session.rb', line 141

def self.seal_data(data, key)
  iv = SecureRandom.random_bytes(12)

  encrypted_data = Encryptor.encrypt(
    value: JSON.generate(data),
    key: key,
    iv: iv,
    algorithm: 'aes-256-gcm',
  )
  Base64.encode64(iv + encrypted_data) # Combine IV with encrypted data and encode as base64
end

.unseal_data(sealed_data, key) ⇒ Hash

Decrypts and unseals data using AES-256-GCM

Parameters:

  • sealed_data (String)

    The sealed data to unseal

  • key (String)

    The key to use for decryption

Returns:

  • (Hash)

    The unsealed data



157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/workos/session.rb', line 157

def self.unseal_data(sealed_data, key)
  decoded_data = Base64.decode64(sealed_data)
  iv = decoded_data[0..11] # Extract the IV (first 12 bytes)
  encrypted_data = decoded_data[12..-1] # Extract the encrypted data

  decrypted_data = Encryptor.decrypt(
    value: encrypted_data,
    key: key,
    iv: iv,
    algorithm: 'aes-256-gcm',
  )

  JSON.parse(decrypted_data, symbolize_names: true) # Parse the decrypted JSON string back to original data
end

Instance Method Details

#authenticate(include_expired: false) ⇒ Hash

Authenticates the user based on the session data rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity

Parameters:

  • include_expired (Boolean) (defaults to: false)

    If true, returns decoded token data even when expired (default: false)

Returns:

  • (Hash)

    A hash containing the authentication response and a reason if the authentication failed



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
# File 'lib/workos/session.rb', line 35

def authenticate(include_expired: false)
  return { authenticated: false, reason: 'NO_SESSION_COOKIE_PROVIDED' } if @session_data.nil?

  begin
    session = Session.unseal_data(@session_data, @cookie_password)
  rescue StandardError
    return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' }
  end

  return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' } unless session[:access_token]

  begin
    decoded = JWT.decode(
      session[:access_token],
      nil,
      true,
      algorithms: @jwks_algorithms,
      jwks: @jwks,
      verify_expiration: false,
    ).first

    expired = decoded['exp'] && decoded['exp'] < Time.now.to_i

    # Early return for expired tokens when not including expired data (backward compatible)
    return { authenticated: false, reason: 'INVALID_JWT' } if expired && !include_expired

    # Return full data for valid tokens or when include_expired is true
    {
      authenticated: !expired,
      session_id: decoded['sid'],
      organization_id: decoded['org_id'],
      role: decoded['role'],
      roles: decoded['roles'],
      permissions: decoded['permissions'],
      entitlements: decoded['entitlements'],
      feature_flags: decoded['feature_flags'],
      user: session[:user],
      impersonator: session[:impersonator],
      reason: expired ? 'INVALID_JWT' : nil,
    }
  rescue JWT::DecodeError
    { authenticated: false, reason: 'INVALID_JWT' }
  rescue StandardError => e
    { authenticated: false, reason: e.message }
  end
end

#get_logout_url(return_to: nil) ⇒ String

Returns a URL to redirect the user to for logging out

Parameters:

  • return_to (String) (defaults to: nil)

    The URL to redirect the user to after logging out

Returns:

  • (String)

    The URL to redirect the user to for logging out



127
128
129
130
131
132
133
134
135
# File 'lib/workos/session.rb', line 127

def get_logout_url(return_to: nil)
  auth_response = authenticate

  unless auth_response[:authenticated]
    raise "Failed to extract session ID for logout URL: #{auth_response[:reason]}"
  end

  @user_management.get_logout_url(session_id: auth_response[:session_id], return_to: return_to)
end

#refresh(options = nil) ⇒ Hash

Refreshes the session data using the refresh token stored in the session data and a reason if the refresh failed

Parameters:

  • options (Hash) (defaults to: nil)

    Options for refreshing the session

Options Hash (options):

  • :cookie_password (String)

    The password to use for unsealing the session data

  • :organization_id (String)

    The organization ID to use for refreshing the session

Returns:

  • (Hash)

    A hash containing a new sealed session, the authentication response,



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
# File 'lib/workos/session.rb', line 88

def refresh(options = nil)
  cookie_password = options.nil? || options[:cookie_password].nil? ? @cookie_password : options[:cookie_password]

  begin
    session = Session.unseal_data(@session_data, cookie_password)
  rescue StandardError
    return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' }
  end

  return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' } unless session[:refresh_token] && session[:user]

  begin
    auth_response = @user_management.authenticate_with_refresh_token(
      client_id: @client_id,
      refresh_token: session[:refresh_token],
      organization_id: options.nil? || options[:organization_id].nil? ? nil : options[:organization_id],
      session: { seal_session: true, cookie_password: cookie_password },
    )

    @session_data = auth_response.sealed_session
    @cookie_password = cookie_password

    {
      authenticated: true,
      sealed_session: auth_response.sealed_session,
      session: auth_response,
      reason: nil,
    }
  rescue StandardError => e
    { authenticated: false, reason: e.message }
  end
end