Module: Descope::Api::V1::Auth

Includes:
EnchantedLink, MagicLink, OAuth, OTP, Password, SAML, TOTP, Mixins::Common, Mixins::Common::EndpointsV1, Mixins::Common::EndpointsV2
Included in:
Descope::Api::V1
Defined in:
lib/descope/api/v1/auth.rb,
lib/descope/api/v1/auth/otp.rb,
lib/descope/api/v1/auth/saml.rb,
lib/descope/api/v1/auth/totp.rb,
lib/descope/api/v1/auth/oauth.rb,
lib/descope/api/v1/auth/password.rb,
lib/descope/api/v1/auth/magiclink.rb,
lib/descope/api/v1/auth/enchantedlink.rb

Overview

Holds all the management API calls

Defined Under Namespace

Modules: EnchantedLink, MagicLink, OAuth, OTP, Password, SAML, TOTP

Constant Summary collapse

ALGORITHM_KEY =
'alg'

Constants included from Mixins::Common::EndpointsV2

Mixins::Common::EndpointsV2::PUBLIC_KEY_PATH

Constants included from Mixins::Common::EndpointsV1

Mixins::Common::EndpointsV1::AUTH_SAML_START_PATH, Mixins::Common::EndpointsV1::EXCHANGE_AUTH_ACCESS_KEY_PATH, Mixins::Common::EndpointsV1::GET_SESSION_ENCHANTEDLINK_AUTH_PATH, Mixins::Common::EndpointsV1::GET_SESSION_MAGICLINK_AUTH_PATH, Mixins::Common::EndpointsV1::HISTORY_PATH, Mixins::Common::EndpointsV1::LOGOUT_ALL_PATH, Mixins::Common::EndpointsV1::LOGOUT_PATH, Mixins::Common::EndpointsV1::ME_PATH, Mixins::Common::EndpointsV1::OAUTH_CREATE_REDIRECT_URL_FOR_SIGN_IN_REQUEST_PATH, Mixins::Common::EndpointsV1::OAUTH_CREATE_REDIRECT_URL_FOR_SIGN_UP_REQUEST_PATH, Mixins::Common::EndpointsV1::OAUTH_EXCHANGE_TOKEN_PATH, Mixins::Common::EndpointsV1::OAUTH_START_PATH, Mixins::Common::EndpointsV1::PASSWORD_POLICY_PATH, Mixins::Common::EndpointsV1::REFRESH_TOKEN_PATH, Mixins::Common::EndpointsV1::REPLACE_PASSWORD_PATH, Mixins::Common::EndpointsV1::SAML_EXCHANGE_TOKEN_PATH, Mixins::Common::EndpointsV1::SELECT_TENANT_PATH, Mixins::Common::EndpointsV1::SEND_RESET_PASSWORD_PATH, Mixins::Common::EndpointsV1::SIGN_IN_AUTH_ENCHANTEDLINK_PATH, Mixins::Common::EndpointsV1::SIGN_IN_AUTH_MAGICLINK_PATH, Mixins::Common::EndpointsV1::SIGN_IN_AUTH_OTP_PATH, Mixins::Common::EndpointsV1::SIGN_IN_AUTH_WEBAUTHN_FINISH_PATH, Mixins::Common::EndpointsV1::SIGN_IN_AUTH_WEBAUTHN_START_PATH, Mixins::Common::EndpointsV1::SIGN_IN_PASSWORD_PATH, Mixins::Common::EndpointsV1::SIGN_UP_AUTH_ENCHANTEDLINK_PATH, Mixins::Common::EndpointsV1::SIGN_UP_AUTH_MAGICLINK_PATH, Mixins::Common::EndpointsV1::SIGN_UP_AUTH_OTP_PATH, Mixins::Common::EndpointsV1::SIGN_UP_AUTH_TOTP_PATH, Mixins::Common::EndpointsV1::SIGN_UP_AUTH_WEBAUTHN_FINISH_PATH, Mixins::Common::EndpointsV1::SIGN_UP_AUTH_WEBAUTHN_START_PATH, Mixins::Common::EndpointsV1::SIGN_UP_OR_IN_AUTH_ENCHANTEDLINK_PATH, Mixins::Common::EndpointsV1::SIGN_UP_OR_IN_AUTH_MAGICLINK_PATH, Mixins::Common::EndpointsV1::SIGN_UP_OR_IN_AUTH_OTP_PATH, Mixins::Common::EndpointsV1::SIGN_UP_OR_IN_AUTH_WEBAUTHN_START_PATH, Mixins::Common::EndpointsV1::SIGN_UP_PASSWORD_PATH, Mixins::Common::EndpointsV1::UPDATE_AUTH_WEBAUTHN_FINISH_PATH, Mixins::Common::EndpointsV1::UPDATE_AUTH_WEBAUTHN_START_PATH, Mixins::Common::EndpointsV1::UPDATE_PASSWORD_PATH, Mixins::Common::EndpointsV1::UPDATE_TOTP_PATH, Mixins::Common::EndpointsV1::UPDATE_USER_EMAIL_ENCHANTEDLINK_PATH, Mixins::Common::EndpointsV1::UPDATE_USER_EMAIL_MAGICLINK_PATH, Mixins::Common::EndpointsV1::UPDATE_USER_EMAIL_OTP_PATH, Mixins::Common::EndpointsV1::UPDATE_USER_PHONE_MAGICLINK_PATH, Mixins::Common::EndpointsV1::UPDATE_USER_PHONE_OTP_PATH, Mixins::Common::EndpointsV1::VALIDATE_SESSION_PATH, Mixins::Common::EndpointsV1::VERIFY_CODE_AUTH_PATH, Mixins::Common::EndpointsV1::VERIFY_ENCHANTEDLINK_AUTH_PATH, Mixins::Common::EndpointsV1::VERIFY_MAGICLINK_AUTH_PATH, Mixins::Common::EndpointsV1::VERIFY_TOTP_PATH

Constants included from Mixins::Common

Mixins::Common::COOKIE_DATA_NAME, Mixins::Common::DEFAULT_BASE_URL, Mixins::Common::DEFAULT_JWT_VALIDATION_LEEWAY, Mixins::Common::DEFAULT_TIMEOUT_SECONDS, Mixins::Common::PHONE_REGEX, Mixins::Common::REDIRECT_LOCATION_COOKIE_NAME, Mixins::Common::REFRESH_SESSION_COOKIE_NAME, Mixins::Common::REFRESH_SESSION_TOKEN_NAME, Mixins::Common::SESSION_COOKIE_NAME, Mixins::Common::SESSION_TOKEN_NAME

Instance Method Summary collapse

Methods included from TOTP

#totp_add_update_key, #totp_sign_in_code, #totp_sign_up

Methods included from Mixins::Validation

#validate_code, #validate_email, #validate_login_id, #validate_password, #validate_phone, #validate_redirect_url, #validate_refresh_token_not_nil, #validate_scim_group_id, #validate_tenant, #validate_tenants, #validate_token_not_empty, #validate_user_id, #verify_provider

Methods included from Mixins::Common

#deep_copy, #get_method_string

Methods included from SAML

#saml_exchange_token, #saml_sign_in

Methods included from OTP

#otp_sign_in, #otp_sign_up, #otp_sign_up_or_in, #otp_update_user_email, #otp_update_user_phone, #otp_verify_code

Methods included from OAuth

#oauth_create_redirect_url_for_sign_in_request, #oauth_create_redirect_url_for_sign_up_request, #oauth_exchange_token, #oauth_start

Methods included from MagicLink

#magiclink_sign_in, #magiclink_sign_up, #magiclink_sign_up_or_in, #magiclink_update_user_email, #magiclink_update_user_phone, #magiclink_verify_token

Methods included from EnchantedLink

#enchanted_link_get_session, #enchanted_link_sign_in, #enchanted_link_sign_up, #enchanted_link_sign_up_or_in, #enchanted_link_update_user_email, #enchanted_link_verify_token

Methods included from Password

#get_password_policy, #password_replace, #password_reset, #password_sign_in, #password_sign_up, #password_update

Instance Method Details

#exchange_access_key(access_key: nil, login_options: {}, audience: nil) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/descope/api/v1/auth.rb', line 43

def exchange_access_key(access_key: nil, login_options: {}, audience: nil)
  # Return a new session token for the given access key
  #   Args:
  #     access_key (str): The access key
  #     audience (str|Iterable[str]|nil): Optional recipients that the JWT is intended for
  #              (must be equal to the 'aud' claim on the provided token)
  #     login_options (hash): Optional advanced controls over login parameters
  #     Return value (Hash): returns the session token from the server together with the expiry and key id
  #                          (sessionToken:Hash, keyId:str, expiration:int)
  unless (access_key.is_a?(String) || access_key.nil?) && !access_key.to_s.empty?
    raise AuthException.new('Access key should be a string!', code: 400)
  end

  res = post(EXCHANGE_AUTH_ACCESS_KEY_PATH, { loginOptions: , audience: audience }, {}, access_key)
  generate_auth_info(res, nil, false, audience)
end

#generate_jwt_response(response_body: nil, refresh_cookie: nil, audience: nil) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
# File 'lib/descope/api/v1/auth.rb', line 31

def generate_jwt_response(response_body: nil, refresh_cookie: nil, audience: nil)
  if response_body.nil? || response_body.empty?
    raise AuthException.new('Unable to generate jwt response. Response body is empty', code: 500)
  end

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

  jwt_response
end

#select_tenant(tenant_id: nil, refresh_token: nil) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
# File 'lib/descope/api/v1/auth.rb', line 60

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']
  )
end

#validate_permissions(jwt_response: nil, permissions: nil) ⇒ Object



72
73
74
75
76
# File 'lib/descope/api/v1/auth.rb', line 72

def validate_permissions(jwt_response: nil, permissions: nil)
  # Validate that a jwt_response has been granted the specified permissions.
  # For a multi-tenant environment use validate_tenant_permissions function
  validate_tenant_permissions(jwt_response: jwt_response, permissions: permissions)
end

#validate_roles(jwt_response: nil, roles: nil) ⇒ Object



123
124
125
126
127
# File 'lib/descope/api/v1/auth.rb', line 123

def validate_roles(jwt_response: nil, roles: nil)
  # Validate that a jwt_response has been granted the specified roles.
  # For a multi-tenant environment use validate_tenant_roles function
  validate_tenant_roles(jwt_response: jwt_response, tenant: '', roles: roles)
end

#validate_tenant_permissions(jwt_response: nil, tenant: nil, permissions: nil) ⇒ Object

rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/MethodLength



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
# File 'lib/descope/api/v1/auth.rb', line 79

def validate_tenant_permissions(jwt_response: nil, tenant: nil, permissions: nil)
  # Validate that a jwt_response has been granted the specified permissions on the specified tenant.
  # For a multi-tenant environment use validate_tenant_permissions function
  if permissions.is_a?(String)
    permissions = [permissions]
  else
    permissions ||= []
  end

  unless jwt_response.is_a?(Hash)
    raise Descope::ArgumentException.new(
      'Invalid JWT response hash', code: 400
    )
  end

  return false unless jwt_response

  granted_permissions = if tenant.nil? || tenant.to_s.empty?
                          jwt_response.fetch('permissions', [])
                        else
                          # ensure that the tenant is associated with the jwt_response
                          @logger.debug "tenant associated jwt: #{jwt_response['tenants']&.key?(tenant)}"
                          return false unless jwt_response['tenants'].key?(tenant)

                          # dig is a method in Ruby for safely navigating nested data structures like hashes
                          # and arrays. It allows you to access deeply nested values without worrying about
                          # raising an error if a middle value is nil.
                          tenant_permission = jwt_response.dig('tenants', tenant, 'permissions') || []
                          tenant_permission = [] if tenant_permission.nil?
                          if tenant_permission.is_a?(String)
                            @logger.debug "tenant_permission string: #{tenant_permission}"
                            [tenant_permission]
                          else
                            @logger.debug "tenant_permission array: #{tenant_permission}"
                            tenant_permission
                          end
                        end

  # Validate all permissions are granted
  permissions.all? do |permission|
    granted_permissions.include?(permission)
  end
end

#validate_tenant_roles(jwt_response: nil, tenant: nil, roles: nil) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/descope/api/v1/auth.rb', line 129

def validate_tenant_roles(jwt_response: nil, tenant: nil, roles: nil)
  # Validate that a jwt_response has been granted the specified roles on the specified tenant.
  # For a multi-tenant environment use validate_tenant_roles function
  @logger.debug "Validate_tenant_roles: #{jwt_response}, #{tenant}, #{roles}"
  if roles.is_a?(String)
    roles = [roles]
  else
    roles ||= []
  end

  unless jwt_response.is_a?(Hash)
    raise Descope::ArgumentException.new(
      'Invalid JWT response hash', code: 400
    )
  end

  return false unless jwt_response

  granted_roles = if tenant.nil? || tenant.to_s.empty?
                    jwt_response.fetch('roles', [])
                  else
                    # ensure that the tenant is associated with the jwt_response
                    return false unless jwt_response['tenants'].key?(tenant)

                    # dig is a method in Ruby for safely navigating nested data structures like hashes
                    # and arrays. It allows you to access deeply nested values without worrying about
                    # raising an error if a middle value is nil.
                    tenant_roles = jwt_response.dig('tenants', tenant, 'roles') || []
                    tenant_roles = [] if tenant_roles.nil?
                    if tenant_roles.is_a?(String)
                      [tenant_roles]
                    else
                      tenant_roles
                    end
                  end

  @logger.debug "granted_roles: #{granted_roles}"
  # Validate all roles are granted
  roles.all? do |role|
    @logger.debug "granted_roles.include?(#{role}): #{granted_roles.include?(role)}"
    granted_roles.include?(role)
  end
end

#validate_token(token, _audience = nil) ⇒ Object

Raises:



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/descope/api/v1/auth.rb', line 173

def validate_token(token, _audience = nil)
  @logger.debug "validating token: #{token}"
  raise AuthException.new('Token validation received empty token', code: 500) if token.nil? || token.to_s.empty?

  unverified_header = jwt_get_unverified_header(token)
  @logger.debug "unverified_header: #{unverified_header}"
  alg_header = unverified_header[ALGORITHM_KEY]
  @logger.debug "alg_header: #{alg_header}"

  if alg_header.nil? || alg_header == 'none'
    raise AuthException.new('Token header is missing property: alg', code: 500)
  end

  kid = unverified_header['kid']
  @logger.debug "kid: #{kid}"
  raise AuthException.new('Token header is missing property: kid', code: 500) if kid.nil?

  found_key = nil
  @mlock.synchronize do
    if @public_keys.nil? || @public_keys == {} || @public_keys.to_s.empty? || @public_keys[kid].nil?
      @logger.debug 'fetching public keys'
      # fetch keys from /v2/keys and set them in @public_keys
      fetch_public_keys
    end

    found_key = @public_keys[kid]
    @logger.debug "found_key: #{found_key}"
    raise AuthException.new('Unable to validate public key. Public key not found.', code: 500) if found_key.nil?
  end

  # save reference to the found key
  # (as another thread can change the self.public_keys hash)
  @logger.debug 'checking if alg_header matches alg_from_key'
  alg_from_key = found_key[1]
  if alg_header != alg_from_key
    raise AuthException.new(
      'Algorithm signature in JWT header does not match the algorithm signature in the Public key.',
      code: 500
    )
  end

  begin
    @logger.debug 'decoding token'
    claims = JWT.decode(
      token,
      found_key[0].public_key,
      true,
      { algorithm: alg_header, exp_leeway: @jwt_validation_leeway }
    )[0] # the payload is the first index in the decoded array
  rescue JWT::ExpiredSignature => e
    raise AuthException.new(
      "Received Invalid token times error due to time glitch (between machines) during jwt validation, try to set the jwt_validation_leeway parameter (in DescopeClient) to higher value than 5sec which is the default: #{e.message}", code: 500
    )
  end
  claims['jwt'] = token
  @logger.debug "claims: #{claims}"
  claims
end