Skip to content
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

Handle token in cookie, fix refresh session bug. #76

Merged
merged 6 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
29 changes: 21 additions & 8 deletions lib/descope/api/v1/auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ def generate_jwt_response(response_body: nil, refresh_cookie: nil, audience: nil
end

jwt_response = generate_auth_info(response_body, refresh_cookie, true, audience)
@logger.debug "jwt_response: #{jwt_response}"
jwt_response['user'] = response_body.key?('user') ? response_body['user'] : {}
jwt_response['firstSeen'] = response_body.key?('firstSeen') ? response_body['firstSeen'] : true

Expand Down Expand Up @@ -62,6 +61,8 @@ def select_tenant(tenant_id: nil, refresh_token: nil)
validate_refresh_token_not_nil(refresh_token)
res = post(SELECT_TENANT_PATH, { tenantId: tenant_id }, {}, refresh_token)
@logger.debug "select_tenant response: #{res}"
cookies = res.fetch('cookies')
generate_jwt_response(response_body: res, refresh_cookie: cookies.fetch(REFRESH_SESSION_COOKIE_NAME, nil))
generate_jwt_response(
response_body: res,
refresh_cookie: res['refreshJwt']
Expand Down Expand Up @@ -231,24 +232,36 @@ def validate_token(token, _audience = nil)
private

def generate_auth_info(response_body, refresh_token, user_jwt, audience = nil)
@logger.debug "generating auth info: #{response_body}, #{refresh_token}, #{user_jwt}, #{audience}"
@logger.debug "generating auth info: response_body: #{response_body}, refresh_token: #{refresh_token}, user_jwt: #{user_jwt}, audience: #{audience}"
jwt_response = {}

# validate the session token if sessionJwt is not empty
st_jwt = response_body.fetch('sessionJwt', '')
unless st_jwt.empty?
@logger.debug "validating session token with refresh_token: #{refresh_token}" if st_jwt
jwt_response[SESSION_TOKEN_NAME] = validate_token(st_jwt, audience) if st_jwt
@logger.debug 'found sessionJwt in response body, adding to jwt_response'
jwt_response[SESSION_TOKEN_NAME] = validate_token(st_jwt, audience)
end

# validate refresh token if refresh_token was passed or if refreshJwt is not empty
rt_jwt = response_body.fetch('refreshJwt', '')

if !refresh_token.nil? || !refresh_token.to_s.empty?
@logger.debug "validating refresh token: #{refresh_token}" if refresh_token
jwt_response[REFRESH_SESSION_TOKEN_NAME] = validate_token(refresh_token, audience)
elsif !rt_jwt.empty?
if !rt_jwt.empty?
jwt_response[REFRESH_SESSION_TOKEN_NAME] = validate_token(rt_jwt, audience)
elsif refresh_token && !refresh_token.empty?
# if refresh_token is in response body (local storage)
jwt_response[REFRESH_SESSION_TOKEN_NAME] = validate_token(refresh_token, audience)
else
cookies = response_body.fetch('cookies', {})
# else if refresh token is in response cookie
cookies.each do |cookie_name, cookie_value|
if cookie_name == REFRESH_SESSION_COOKIE_NAME
jwt_response[REFRESH_SESSION_TOKEN_NAME] = validate_token(cookie_value, audience)
end
end
end

if jwt_response[REFRESH_SESSION_TOKEN_NAME].nil?
raise Descope::AuthException.new('Unable to validate refresh token', code: 500)
end

jwt_response = adjust_properties(jwt_response, user_jwt)
Expand Down
4 changes: 3 additions & 1 deletion lib/descope/api/v1/auth/enchantedlink.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ def enchanted_link_verify_token(token = nil)
def enchanted_link_get_session(pending_ref = nil)
# @see https://docs.descope.com/api/openapi/enchantedlink/operation/GetEnchantedLinkSession/
res = post(GET_SESSION_ENCHANTEDLINK_AUTH_PATH, { pendingRef: pending_ref })
generate_jwt_response(response_body: res, refresh_cookie: res['refreshJwt'])
cookies = res.fetch(COOKIE_DATA_NAME, {})
refresh_cookie = cookies.fetch(REFRESH_SESSION_COOKIE_NAME, nil) || res.fetch('refreshJwt', nil)
generate_jwt_response(response_body: res, refresh_cookie:)
end

private
Expand Down
4 changes: 3 additions & 1 deletion lib/descope/api/v1/auth/magiclink.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ def magiclink_sign_up_or_in(method: nil, login_id: nil, uri: nil, login_options:
def magiclink_verify_token(token = nil)
validate_token_not_empty(token)
res = post(VERIFY_MAGICLINK_AUTH_PATH, { token: })
generate_jwt_response(response_body: res)
cookies = res.fetch(COOKIE_DATA_NAME, {})
refresh_cookie = cookies.fetch(REFRESH_SESSION_COOKIE_NAME, nil) || res.fetch('refreshJwt', nil)
generate_jwt_response(response_body: res, refresh_cookie:)
end

def magiclink_update_user_email(login_id: nil, email: nil, uri: nil, add_to_login_ids: nil, on_merge_use_existing: nil, provider_id: nil, template_id: nil, template_options: nil, refresh_token: nil)
Expand Down
4 changes: 3 additions & 1 deletion lib/descope/api/v1/auth/otp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ def otp_verify_code(method: nil, login_id: nil, code: nil)
code:
}
res = post(uri, request_params)
generate_jwt_response(response_body: res, refresh_cookie: res.fetch('refreshJwt', {}))
cookies = res.fetch(COOKIE_DATA_NAME, {})
refresh_cookie = cookies.fetch(REFRESH_SESSION_COOKIE_NAME, nil) || res.fetch('refreshJwt', nil)
generate_jwt_response(response_body: res, refresh_cookie:)
end

def otp_update_user_email(login_id: nil, email: nil, refresh_token: nil, add_to_login_ids: false,
Expand Down
8 changes: 6 additions & 2 deletions lib/descope/api/v1/auth/password.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ def password_sign_up(login_id: nil, password: nil, user: nil)

request_params[:user] = password_user_compose_update_body(**user) unless user.nil?
res = post(SIGN_UP_PASSWORD_PATH, request_params)
generate_jwt_response(response_body: res)
cookies = res.fetch(COOKIE_DATA_NAME, {})
refresh_cookie = cookies.fetch(REFRESH_SESSION_COOKIE_NAME, nil) || res.fetch('refreshJwt', nil)
generate_jwt_response(response_body: res, refresh_cookie:)
end

def password_sign_in(login_id: nil, password: nil, sso_app_id: nil)
Expand All @@ -38,7 +40,9 @@ def password_sign_in(login_id: nil, password: nil, sso_app_id: nil)
ssoAppId: sso_app_id
}
res = post(SIGN_IN_PASSWORD_PATH, request_params)
generate_jwt_response(response_body: res)
cookies = res.fetch(COOKIE_DATA_NAME, {})
refresh_cookie = cookies.fetch(REFRESH_SESSION_COOKIE_NAME, nil) || res.fetch('refreshJwt', nil)
generate_jwt_response(response_body: res, refresh_cookie:)
end

def password_replace(login_id: nil, old_password: nil, new_password: nil)
Expand Down
4 changes: 3 additions & 1 deletion lib/descope/api/v1/auth/totp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ def totp_sign_in_code(login_id: nil, login_options: nil, code: nil)
uri = VERIFY_TOTP_PATH
body = totp_compose_signin_body(login_id, code, login_options)
res = post(uri, body, {}, nil)
generate_jwt_response(response_body: res, refresh_cookie: res.fetch('refreshJwt', {}))
cookies = res.fetch(COOKIE_DATA_NAME, {})
refresh_cookie = cookies.fetch(REFRESH_SESSION_COOKIE_NAME, nil) || res.fetch('refreshJwt', nil)
generate_jwt_response(response_body: res, refresh_cookie:)
end

def totp_sign_up(login_id: nil, user: nil, sso_app_id: nil)
Expand Down
5 changes: 4 additions & 1 deletion lib/descope/api/v1/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ def refresh_session(refresh_token: nil, audience: nil)
validate_refresh_token_not_nil(refresh_token)
validate_token(refresh_token, audience)
res = post(REFRESH_TOKEN_PATH, {}, {}, refresh_token)
generate_jwt_response(response_body: res, refresh_cookie: refresh_token, audience:)

cookies = res.fetch(COOKIE_DATA_NAME, {})
refresh_cookie = cookies.fetch(REFRESH_SESSION_COOKIE_NAME, nil) || res.fetch('refreshJwt', nil)
generate_jwt_response(response_body: res, refresh_cookie:, audience:)
end

def me(refresh_token = nil)
Expand Down
13 changes: 8 additions & 5 deletions lib/descope/mixins/http.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true
require "addressable/uri"
require 'addressable/uri'
require 'retryable'
require_relative '../exception'

Expand Down Expand Up @@ -44,9 +44,11 @@ def retry_options
}
end

def safe_parse_json(body)
def safe_parse_json(body, cookies: nil)
@logger.debug "response => #{JSON.parse(body.to_s)}"
JSON.parse(body.to_s)
res = JSON.parse(body.to_s)
res['cookies'] = cookies unless cookies.nil?
res
rescue JSON::ParserError
body
end
Expand Down Expand Up @@ -94,11 +96,12 @@ def request(method, uri, body = {}, extra_headers = {})
call(method, encode_uri(uri), timeout, @headers, body.to_json)
end

raise Descope::Unsupported.new("No response from server", code: 400) unless result && result.respond_to?(:code)
raise Descope::Unsupported.new('No response from server', code: 400) unless result.respond_to?(:code)

@logger.info("API Request: [#{method}] #{uri} - Response Code: #{result.code}")

case result.code
when 200...226 then safe_parse_json(result.body)
when 200...226 then safe_parse_json(result.body, cookies: result.cookies)
when 400 then raise Descope::BadRequest.new(result.body, code: result.code, headers: result.headers)
when 401 then raise Descope::Unauthorized.new(result.body, code: result.code, headers: result.headers)
when 403 then raise Descope::AccessDenied.new(result.body, code: result.code, headers: result.headers)
Expand Down
46 changes: 46 additions & 0 deletions spec/integration/lib.descope/api/v1/auth/session_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

require 'spec_helper'

describe Descope::Api::V1::Session do
before(:all) do
@client = DescopeClient.new(Configuration.config)
end

after(:all) do
@client.logger.info('Cleaning up test users...')
all_users = @client.search_all_users
all_users['users'].each do |user|
if user['middleName'] == 'Ruby SDK User'
@client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
@client.delete_user(user['loginIds'][0])
end
end
end

context 'test session methods' do
it 'should refresh session with refresh token' do
@password = SpecUtils.generate_password
user = build(:user)

@client.logger.info('1. Sign up with password')
res = @client.password_sign_up(login_id: user[:login_id], password: @password, user:)
@client.logger.info("sign up with password res: #{res}")
original_refresh_token = res[REFRESH_SESSION_TOKEN_NAME]['jwt']

@client.logger.info('2. Sign in with password')
login_res = @client.password_sign_in(login_id: user[:login_id], password: @password)
@client.logger.info("login_res: #{login_res}")

@client.logger.info('3. sleep 1 second before calling refresh_session')
sleep(1)

@client.logger.info('4. Refresh session')
login_res = @client.refresh_session(refresh_token: login_res[REFRESH_SESSION_TOKEN_NAME]['jwt'])
new_refresh_token = login_res['refreshSessionToken']['jwt']

@client.logger.info('5. Check new refresh token is not the same as the original one')
expect(original_refresh_token).not_to eq(new_refresh_token)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
@tenant_id = @client.create_tenant(name: 'some-new-tenant')['id']
@client.logger.info('creating access key')
@access_key = @client.create_access_key(name: @key_name, key_tenants: [{ tenant_id: @tenant_id }])
@client.logger.info("waiting for access key #{@access_key['key']['id']} to be active 60 seconds")
sleep 60
end

Expand Down
3 changes: 1 addition & 2 deletions spec/integration/lib.descope/api/v1/management/audit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,14 @@
created_user = @client.create_user(**user)['user']

expect do
res = @client.audit_create_event(
@client.audit_create_event(
user_id: created_user['loginId'],
action: 'pencil.created',
type: 'info',
tenant_id:,
actor_id: created_user['loginIds'][0],
data: { 'key' => 'value' }
)
expect(res).to eq({})
end.not_to raise_error
end
end
6 changes: 3 additions & 3 deletions spec/integration/lib.descope/api/v1/management/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@
@client.expire_password(user['loginIds'][0])
@client.password_sign_in(login_id: user['loginIds'][0], password:)
rescue Descope::ServerError => e
expect(e.message).to match(/"errorCode":"E062909"/)
expect(e.message).to match(/"errorCode":"E062903"/)
end
end

Expand All @@ -226,8 +226,8 @@
new_password = SpecUtils.generate_password
@client.set_password(login_id: user['loginIds'][0], password: new_password)
@client.password_sign_in(login_id: user['loginIds'][0], password:)
rescue Descope::Unauthorized => e
expect(e.message).to match(/"errorDescription":"Invalid signin credentials"/)
rescue Descope::ServerError => e
expect(e.message).to match(/"errorDescription":"Password signin failed"/)
end
end

Expand Down
13 changes: 11 additions & 2 deletions spec/lib.descope/api/v1/auth/enchantedlink_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@

it 'is expected to validate refresh token and not raise an error with refresh token and valid login options' do
expect do
@instance.send(:validate_refresh_token_provided, { mfa: true, stepup: true }, 'some-token')
@instance.send(:validate_refresh_token_provided, { mfa: true, stepup: true }, 'some-token')
end.not_to raise_error
end

Expand Down Expand Up @@ -148,7 +148,16 @@
end

it 'is expected to get session by pending ref with enchanted link' do
jwt_response = { 'fake': 'response' }
jwt_response = {
'sessionJwt' => 'fake_session_jwt',
'refreshJwt' => 'fake_refresh_jwt',
'cookies' => {
'refresh_token' => 'fake_refresh_cookie'
}
}
allow(@instance).to receive(:post).with(
GET_SESSION_ENCHANTEDLINK_AUTH_PATH, { pendingRef: 'pendingRef' }
).and_return(jwt_response)
allow(@instance).to receive(:generate_jwt_response).and_return(jwt_response)

expect do
Expand Down
11 changes: 10 additions & 1 deletion spec/lib.descope/api/v1/auth/password_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,18 @@
end

it 'is expected to sign in with password' do
response_body = {
'sessionJwt' => 'fake_session_jwt',
'refreshJwt' => 'fake_refresh_jwt',
'cookies' => {
'refresh_token' => 'fake_refresh_cookie'
}
}

expect(@instance).to receive(:post).with(
SIGN_IN_PASSWORD_PATH, { loginId: 'test', password: 's3cr3t', ssoAppId: nil }
)
).and_return(response_body)

# stub the jwt_get_unverified_header method to return the kid of the public key created above
allow(@instance).to receive(:generate_jwt_response).and_return({})
expect { @instance.password_sign_in(login_id: 'test', password: 's3cr3t') }.not_to raise_error
Expand Down
22 changes: 17 additions & 5 deletions spec/lib.descope/api/v1/auth_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,13 @@
end

it 'is expected to select tenant' do
jwt_response = { 'fake': 'response' }
jwt_response = {
'sessionJwt' => 'fake_session_jwt',
'refreshJwt' => 'fake_refresh_jwt',
'cookies' => {
'refresh_token' => 'fake_refresh_cookie'
}
}

expect(@instance).to receive(:post).with(
SELECT_TENANT_PATH, { tenantId: 'tenant123' }, {}, 'refresh-token'
Expand Down Expand Up @@ -390,20 +396,26 @@
end

it 'is expected to successfully exchange access key without login_options' do
jwt_response = { 'fake': 'response' }
jwt_response = {
'sessionJwt' => 'fake_session_jwt',
'refreshJwt' => 'fake_refresh_jwt'
}
access_key = 'abc'

expect(@instance).to receive(:post).with(
EXCHANGE_AUTH_ACCESS_KEY_PATH, { loginOptions: {}, audience: 'IT' }, {}, access_key
).and_return(jwt_response)

allow(@instance).to receive(:generate_jwt_response).and_return(jwt_response)
allow(@instance).to receive(:generate_auth_info).and_return(jwt_response)

expect { @instance.exchange_access_key(access_key:, audience: 'IT') }.not_to raise_error
end

it 'is expected to successfully exchange access key with login_options' do
jwt_response = { 'fake': 'response' }
jwt_response = {
'sessionJwt' => 'fake_session_jwt',
'refreshJwt' => 'fake_refresh_jwt'
}
access_key = 'abc'

expect(@instance).to receive(:post).with(
Expand All @@ -413,7 +425,7 @@
access_key
).and_return(jwt_response)

allow(@instance).to receive(:generate_jwt_response).and_return(jwt_response)
allow(@instance).to receive(:generate_auth_info).and_return(jwt_response)

expect { @instance.exchange_access_key(access_key:, login_options: { customClaims: { k1: 'v1' } }, audience: 'IT') }.not_to raise_error
end
Expand Down
Loading
Loading