Skip to content

Commit 6fb6a96

Browse files
ollymnicknisi
andauthored
expose authentication object even if expired (#398)
* expose authentication object even if expired * remove unused variable * disable rubocop for long line * Fix tests * refactor to use keyword argument for backwards compatibility * fix rubocop errors --------- Co-authored-by: Nick Nisi <[email protected]>
1 parent f6d6780 commit 6fb6a96

File tree

2 files changed

+76
-32
lines changed

2 files changed

+76
-32
lines changed

lib/workos/session.rb

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ def initialize(user_management:, client_id:, session_data:, cookie_password:)
2929
end
3030

3131
# Authenticates the user based on the session data
32+
# @param include_expired [Boolean] If true, returns decoded token data even when expired (default: false)
3233
# @return [Hash] A hash containing the authentication response and a reason if the authentication failed
33-
# rubocop:disable Metrics/AbcSize
34-
def authenticate
34+
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
35+
def authenticate(include_expired: false)
3536
return { authenticated: false, reason: 'NO_SESSION_COOKIE_PROVIDED' } if @session_data.nil?
3637

3738
begin
@@ -41,23 +42,41 @@ def authenticate
4142
end
4243

4344
return { authenticated: false, reason: 'INVALID_SESSION_COOKIE' } unless session[:access_token]
44-
return { authenticated: false, reason: 'INVALID_JWT' } unless is_valid_jwt(session[:access_token])
45-
46-
decoded = JWT.decode(session[:access_token], nil, true, algorithms: @jwks_algorithms, jwks: @jwks).first
47-
48-
{
49-
authenticated: true,
50-
session_id: decoded['sid'],
51-
organization_id: decoded['org_id'],
52-
role: decoded['role'],
53-
roles: decoded['roles'],
54-
permissions: decoded['permissions'],
55-
entitlements: decoded['entitlements'],
56-
feature_flags: decoded['feature_flags'],
57-
user: session[:user],
58-
impersonator: session[:impersonator],
59-
reason: nil,
60-
}
45+
46+
begin
47+
decoded = JWT.decode(
48+
session[:access_token],
49+
nil,
50+
true,
51+
algorithms: @jwks_algorithms,
52+
jwks: @jwks,
53+
verify_expiration: false,
54+
).first
55+
56+
expired = decoded['exp'] && decoded['exp'] < Time.now.to_i
57+
58+
# Early return for expired tokens when not including expired data (backward compatible)
59+
return { authenticated: false, reason: 'INVALID_JWT' } if expired && !include_expired
60+
61+
# Return full data for valid tokens or when include_expired is true
62+
{
63+
authenticated: !expired,
64+
session_id: decoded['sid'],
65+
organization_id: decoded['org_id'],
66+
role: decoded['role'],
67+
roles: decoded['roles'],
68+
permissions: decoded['permissions'],
69+
entitlements: decoded['entitlements'],
70+
feature_flags: decoded['feature_flags'],
71+
user: session[:user],
72+
impersonator: session[:impersonator],
73+
reason: expired ? 'INVALID_JWT' : nil,
74+
}
75+
rescue JWT::DecodeError
76+
{ authenticated: false, reason: 'INVALID_JWT' }
77+
rescue StandardError => e
78+
{ authenticated: false, reason: e.message }
79+
end
6180
end
6281

6382
# Refreshes the session data using the refresh token stored in the session data
@@ -66,7 +85,6 @@ def authenticate
6685
# @option options [String] :organization_id The organization ID to use for refreshing the session
6786
# @return [Hash] A hash containing a new sealed session, the authentication response,
6887
# and a reason if the refresh failed
69-
# rubocop:disable Metrics/PerceivedComplexity
7088
def refresh(options = nil)
7189
cookie_password = options.nil? || options[:cookie_password].nil? ? @cookie_password : options[:cookie_password]
7290

@@ -168,17 +186,5 @@ def create_remote_jwk_set(uri)
168186

169187
jwks
170188
end
171-
172-
# Validates a JWT token using the JWKS set
173-
# @param token [String] The JWT token to validate
174-
# @return [Boolean] True if the token is valid, false otherwise
175-
# rubocop:disable Naming/PredicateName
176-
def is_valid_jwt(token)
177-
JWT.decode(token, nil, true, algorithms: @jwks_algorithms, jwks: @jwks)
178-
true
179-
rescue StandardError
180-
false
181-
end
182-
# rubocop:enable Naming/PredicateName
183189
end
184190
end

spec/lib/workos/session_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,44 @@
160160
expect(result).to eq({ authenticated: false, reason: 'INVALID_JWT' })
161161
end
162162

163+
it 'returns INVALID_JWT without token data when session is expired' do
164+
session = WorkOS::Session.new(
165+
user_management: user_management,
166+
client_id: client_id,
167+
session_data: session_data,
168+
cookie_password: cookie_password,
169+
)
170+
allow_any_instance_of(JWT::Decode).to receive(:verify_signature).and_return(true)
171+
allow(Time).to receive(:now).and_return(Time.at(9_999_999_999))
172+
result = session.authenticate
173+
expect(result).to eq({ authenticated: false, reason: 'INVALID_JWT' })
174+
end
175+
176+
it 'returns INVALID_JWT with full token data when session is expired and include_expired is true' do
177+
session = WorkOS::Session.new(
178+
user_management: user_management,
179+
client_id: client_id,
180+
session_data: session_data,
181+
cookie_password: cookie_password,
182+
)
183+
allow_any_instance_of(JWT::Decode).to receive(:verify_signature).and_return(true)
184+
allow(Time).to receive(:now).and_return(Time.at(9_999_999_999))
185+
result = session.authenticate(include_expired: true)
186+
expect(result).to eq({
187+
authenticated: false,
188+
session_id: 'session_id',
189+
organization_id: 'org_id',
190+
role: 'role',
191+
roles: ['role'],
192+
permissions: ['read'],
193+
feature_flags: nil,
194+
entitlements: nil,
195+
user: 'user',
196+
impersonator: 'impersonator',
197+
reason: 'INVALID_JWT',
198+
})
199+
end
200+
163201
it 'authenticates successfully with valid session_data' do
164202
session = WorkOS::Session.new(
165203
user_management: user_management,

0 commit comments

Comments
 (0)