Class: Parse::User

Inherits:
Object
  • Object
show all
Includes:
MFA::UserExtension
Defined in:
lib/parse/model/classes/user.rb,
lib/parse/two_factor_auth/user_extension.rb,
lib/parse/stack/generators/templates/model_user.rb

Overview

Reopen User class to include MFA extension

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from MFA::UserExtension

#confirm_sms_mfa!, #disable_mfa!, #disable_mfa_admin!, #disable_mfa_master_key!, #login_with_mfa!, #mfa_enabled?, #mfa_provisioning_uri, #mfa_qr_code, #mfa_status, #setup_mfa!, #setup_sms_mfa!

Instance Attribute Details

#active_sessionsArray<Parse::Session>

A has_many relationship to all Session instances for this user. This will query the _Session collection for all sessions which have this user in it's user column.

Returns:

Version:

  • 1.7.1



226
# File 'lib/parse/model/classes/user.rb', line 226

has_many :active_sessions, as: :session

#auth_dataHash

The auth data for this Parse::User. Depending on how this user is authenticated or logged in, the contents may be different, especially if you are using another third-party authentication mechanism like Facebook/Twitter.

Returns:

  • (Hash)

    Auth data hash object.



191
# File 'lib/parse/model/classes/user.rb', line 191

property :auth_data, :object

#emailString

Emails are optional in Parse, but if set, they must be unique.

Returns:

  • (String)

    The email field.



196
# File 'lib/parse/model/classes/user.rb', line 196

property :email

#email_verifiedBoolean

Whether this user's email address has been verified. Set by Parse Server when the user follows the verification link delivered by the email adapter, and applied to the in-memory object by #signup! / signup-on-save when the server includes it in the signup response (see SIGNUP_RESPONSE_APPLY_KEYS).

Returns:

  • (Boolean)


218
# File 'lib/parse/model/classes/user.rb', line 218

property :email_verified, :boolean

#session_tokenString

Returns The session token if this user is logged in.

Returns:

  • (String)

    The session token if this user is logged in.



184
185
186
# File 'lib/parse/model/classes/user.rb', line 184

def session_token
  @session_token
end

#usernameString

All Parse users have a username and must be globally unique.

Returns:

  • (String)

    The user's username.



209
# File 'lib/parse/model/classes/user.rb', line 209

property :username

Class Method Details

.anonymous_signupUser

Create and log in a new anonymous user via the authData.anonymous provider. The returned user instance has a session_token and an objectId, and #anonymous? returns true. Later, after the user has chosen a username and password, upgrade the account in-place with #upgrade_anonymous!.

Parse Server requires the anonymous-provider payload to include a client-generated id; this helper produces one via SecureRandom.uuid so callers don't have to hand-roll the authData shape.

Returns:

  • (User)

    a freshly-created, logged-in anonymous user.

See Also:



796
797
798
# File 'lib/parse/model/classes/user.rb', line 796

def self.
  autologin_service(:anonymous, { id: SecureRandom.uuid })
end

.autologin_service(service_name, auth_data, body: {}) ⇒ User

Automatically and implicitly signup a user if it did not already exists and authenticates them (login) using third-party authentication data. May raise exceptions similar to create depending on what you provide the body parameter.

Parameters:

  • service_name (Symbol)

    the name of the service key (ex. :facebook)

  • auth_data (Hash)

    the specific service data to place in the user's auth-data for this service.

  • body (Hash) (defaults to: {})

    any additional User related fields or properties when signing up this User record.

Returns:

  • (User)

    a logged in user, or nil.

See Also:



771
772
773
774
775
776
777
778
779
780
781
# File 'lib/parse/model/classes/user.rb', line 771

def self.autologin_service(service_name, auth_data, body: {})
  # Trust-mark this call so {.assert_create_body_safe!} permits the
  # +authData+ that we are explicitly responsible for here. The
  # marker is consumed inside {.create} before forwarding to the
  # server.
  body = body.merge({
    authData: { service_name => auth_data },
    __parse_stack_trusted_authdata: true,
  })
  self.create(body)
end

.create(body, **opts) ⇒ User

Creates a new Parse::User given a hash that maps to the fields defined in your Parse::User collection.

Mass-assignment of +authData+/+auth_data+/+objectId+ is refused. If you intend to create-or-login a user via federated identity, use autologin_service or link_or_create_with_auth_data. Passing those keys directly bypasses the SDK's federated-identity wrapper and risks returning a victim's sessionToken to whoever submitted the request.

Parameters:

  • body (Hash)

    The hash containing the Parse::User fields. The field username and password are required.

  • opts (Hash)

    a customizable set of options

Options Hash (**opts):

  • :master_key (Boolean)

    Whether the master key should be used for this request.

Returns:

  • (User)

    Returns a successfully created Parse::User.

Raises:



696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
# File 'lib/parse/model/classes/user.rb', line 696

def self.create(body, **opts)
  # Consume and clear the SDK-internal trust marker before validation
  # or wire transit. This prevents trusted-authdata flag smuggling
  # through callers that copy hashes from a request parameter.
  trusted = body.is_a?(Hash) ? (body.delete(:__parse_stack_trusted_authdata) ||
                                body.delete("__parse_stack_trusted_authdata")) : false
  assert_create_body_safe!(body) unless trusted
  strip_server_controlled_keys!(body)
  response = client.create_user(body, **opts)
  if response.success?
    body.delete :password # clear password before merging
    # Self-fetch trust: the response.result describes the user we
    # just created, so any returned authData IS that user's own
    # federated-identity payload — allow it through the hydration
    # strip in {#apply_attributes!}.
    return with_authdata_trust { Parse::User.build body.merge(response.result) }
  end

  case response.code
  when Parse::Response::ERROR_USERNAME_MISSING
    raise Parse::Error::UsernameMissingError, response
  when Parse::Response::ERROR_PASSWORD_MISSING
    raise Parse::Error::PasswordMissingError, response
  when Parse::Response::ERROR_USERNAME_TAKEN
    raise Parse::Error::UsernameTakenError, response
  when Parse::Response::ERROR_EMAIL_TAKEN
    raise Parse::Error::EmailTakenError, response
  end
  raise Parse::Client::ResponseError, response
end

.login(username, password) ⇒ User

Login and return a Parse::User with this username/password combination.

Parameters:

  • username (String)

    the user's username

  • password (String)

    the user's password

Returns:

  • (User)

    a logged in user for the provided username. Returns nil otherwise.

See Also:



817
818
819
820
821
822
823
# File 'lib/parse/model/classes/user.rb', line 817

def self.(username, password)
  response = client.(username.to_s, password.to_s)
  return nil unless response.success?
  # Self-fetch trust: the login response IS the authenticating user;
  # any returned authData belongs to them.
  with_authdata_trust { Parse::User.build(response.result) }
end

.login!(username, password) ⇒ User

Login and return a Parse::User with this username/password combination, raising on failure instead of returning nil. Mirrors the find_by_username! / find! conventions: callers who treat an unsuccessful login as an exceptional condition shouldn't have to build their own raise if .nil? boilerplate around every call site.

Parameters:

  • username (String)

    the user's username.

  • password (String)

    the user's password.

Returns:

  • (User)

    the logged-in user.

Raises:

  • (Parse::Error::AuthenticationError)

    when Parse Server rejects the credentials, the request is rate-limited at the server, or the response is otherwise unsuccessful.

See Also:



838
839
840
841
842
843
844
845
846
847
848
# File 'lib/parse/model/classes/user.rb', line 838

def self.login!(username, password)
  response = client.(username.to_s, password.to_s)
  if response.success?
    # Self-fetch trust: see {.login}.
    with_authdata_trust { Parse::User.build(response.result) }
  else
    raise Parse::Error::AuthenticationError,
          "Parse::User.login! failed for #{username.inspect}: " \
          "#{response.error || "HTTP #{response.http_status}"} (code=#{response.code.inspect})"
  end
end

.request_password_reset(email) ⇒ Boolean

Request a password reset for a registered email.

Examples:

user = Parse::User.first

# pass a user object
Parse::User.request_password_reset user
# or email
Parse::User.request_password_reset("user@example.com")

Parameters:

  • email (String)

    The user's email address.

Returns:

  • (Boolean)

    True/false if successful.



860
861
862
863
864
865
# File 'lib/parse/model/classes/user.rb', line 860

def self.request_password_reset(email)
  email = email.email if email.is_a?(Parse::User)
  return false if email.blank?
  response = client.request_password_reset(email)
  response.success?
end

.session(token, opts = {}) ⇒ User

Same as session! but returns nil if a user was not found or sesion token was invalid.

Returns:

  • (User)

    the user matching this active token, otherwise nil.

See Also:

  • #session!


870
871
872
873
874
# File 'lib/parse/model/classes/user.rb', line 870

def self.session(token, opts = {})
  self.session! token, opts
rescue Parse::Error::InvalidSessionTokenError
  nil
end

.session!(token, opts = {}) ⇒ User

Return a Parse::User for this active session token.

Returns:

  • (User)

    the user matching this active token

Raises:

  • (InvalidSessionTokenError)

    Invalid session token.

  • (ArgumentError)

    when opts smuggles a conflicting :session_token key — the positional token argument is the only source of truth; rejecting the kwarg prevents a silent override that would authenticate as a different user.

See Also:



884
885
886
887
888
889
890
891
892
893
894
895
896
897
# File 'lib/parse/model/classes/user.rb', line 884

def self.session!(token, opts = {})
  if opts.is_a?(Hash) && (opts.key?(:session_token) || opts.key?("session_token"))
    raise ArgumentError,
          "Parse::User.session! takes the session token as its positional " \
          "argument; do not also pass it via opts[:session_token]"
  end
  # support Parse::Session objects
  token = token.session_token if token.respond_to?(:session_token)
  response = client.current_user(token, **opts)
  return nil unless response.success?
  # Self-fetch trust: `/users/me` returns the row owned by the
  # supplied session token, so authData here is that user's own.
  with_authdata_trust { Parse::User.build(response.result) }
end

.signup(username, password, email = nil, body: {}) ⇒ Object

This method will signup a new user using the parameters below. The required fields to create a user in Parse is the username and password fields. The email field is optional. Both username and email (if provided), must be unique. At a minimum, it is recommended you perform a query using the supplied username first to verify do not already have an account with that username. This method will raise all the exceptions from the similar create method.

See Also:



806
807
808
809
810
# File 'lib/parse/model/classes/user.rb', line 806

def self.(username, password, email = nil, body: {})
  body = body.merge({ username: username, password: password })
  body[:email] = email if email.present?
  self.create(body)
end

.signup_body_self_only_acl_safe?(body) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

True when the signup-body objectId and ACL together describe the safe self-only ownership pattern that acl_policy produces under owner: :self: the body has a client-assigned objectId matching the Parse-id format, and the ACL has exactly one entry granting read+write to that same objectId. Any deviation — multiple keys, a non-self key, a * (public) entry, a role: entry, missing or extra permissions — fails the check and the strip-everything fallback in #signup_create / #signup! runs as before.

Parameters:

  • body (Hash)

    signup request body, with symbol or string keys.

Returns:

  • (Boolean)


1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
# File 'lib/parse/model/classes/user.rb', line 1140

def self.(body)
  return false unless body.is_a?(Hash)
  oid = body[:objectId] || body["objectId"]
  acl = body[:ACL] || body["ACL"]
  return false unless oid.is_a?(String) && oid.match?(PARSE_OBJECT_ID_FORMAT)
  return false unless acl.is_a?(Hash) && acl.size == 1
  perms = acl[oid] || acl[oid.to_s]
  return false unless perms.is_a?(Hash)
  normalized = perms.transform_keys(&:to_s)
  normalized == { "read" => true, "write" => true }
end

Instance Method Details

#acl_roles(max_depth: 10, master: false, as: nil) ⇒ Set<String>

Return the transitive upward closure of role names this user inherits permissions from.

Authorization

The role graph is privileged data: Parse Server's _Role class ships with acl_policy :private precisely so anonymous clients cannot enumerate role memberships. This method therefore routes through the mongo-direct fast path under an EXPLICIT authorization scope.

By default, as: is set to self — the user instance itself, meaning "I (this user) am asking about my own roles". The scope is resolved via ACLScope and CLP is enforced against _Role: the call succeeds iff the user's permission set (["*", user.id, "role:..."]) is permitted to find on _Role. Under Parse Server's default _Role CLP (master-only, which Role's acl_policy :private does not change), the user's scope is NOT permitted, so this call raises CLPScope::Denied. Apps that have explicitly opened _Role CLP for authenticated users (e.g. find: { requiresAuthentication: true }) will have the call succeed.

Callers performing privileged work (computing ACL permission sets, e.g. server-side filters) should pass master: true to bypass the CLP check.

Breaking change: Previously this method bypassed the authorization check entirely (callers could construct a Parse::User with any objectId via Parse::User.new.tap { |u| u.id = victim_id } and enumerate the victim's roles). The new contract is explicit-auth-required; use master: true for the previous behavior.

Examples:

# User reading their own roles (subject to _Role CLP):
permission_set = (["*", user.id] + user.acl_roles.map { |n| "role:#{n}" }).uniq
# Admin/SDK-internal code building ACL filters:
permission_set = (["*", user.id] + user.acl_roles(master: true).map { |n| "role:#{n}" }).uniq

Parameters:

  • max_depth (Integer) (defaults to: 10)

    maximum BFS depth (default: 10).

  • master (Boolean) (defaults to: false)

    when true, bypass _Role CLP and run the role-graph lookup under master mode. Use for ACL-building code paths inside the SDK or in admin tooling.

  • as (Parse::User, Parse::Pointer, nil) (defaults to: nil)

    caller-scope. When nil, defaults to self (the user-asking-about-their-own-roles case). Pass a different user to ask "what would this caller see when introspecting this user's roles?"; the scope's permission set is checked against _Role CLP.

Returns:

  • (Set<String>)

    role names (no role: prefix). Empty set when the user has no objectId yet or holds no roles.

Raises:



1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
# File 'lib/parse/model/classes/user.rb', line 1083

def acl_roles(max_depth: 10, master: false, as: nil)
  return Set.new unless id.is_a?(String) && !id.empty?
  # Default `as:` to self so the common "user reading their own
  # roles" case works without ceremony when _Role CLP permits the
  # user. The CLP check + scope resolution happens inside
  # Parse::Role.all_for_user → Parse::MongoDB.role_names_for_user.
  effective_as = as.nil? && master != true ? self : as
  Parse::Role.all_for_user(
    self, max_depth: max_depth, master: master, as: effective_as,
  )
end

#active_session_countInteger

Get the count of active (non-expired) sessions for this user. Requires an authenticated session (see #logout_all! for the rationale).

Examples:

count = user.active_session_count
puts "User is logged in on #{count} devices"

Returns:

  • (Integer)

    the number of active sessions

Raises:



997
998
999
1000
1001
1002
1003
# File 'lib/parse/model/classes/user.rb', line 997

def active_session_count
  return 0 unless id.present?
  require_self_session!("active_session_count")
  Parse.with_session(@session_token) do
    Parse::Session.active_count_for_user(self)
  end
end

#anonymous?Boolean

Returns true if this user is anonymous (i.e. created via the authData.anonymous provider rather than via signup with a username/password or a real OAuth provider).

Returns:

  • (Boolean)

    true if this user is anonymous (i.e. created via the authData.anonymous provider rather than via signup with a username/password or a real OAuth provider).



315
316
317
# File 'lib/parse/model/classes/user.rb', line 315

def anonymous?
  !anonymous_id.nil?
end

#anonymous_idString

Returns the anonymous identifier only if this user is anonymous.

Returns:

  • (String)

    The anonymous identifier for this anonymous user.

See Also:



322
323
324
# File 'lib/parse/model/classes/user.rb', line 322

def anonymous_id
  auth_data["anonymous"]["id"] if auth_data.present? && auth_data["anonymous"].is_a?(Hash)
end

#any_session!String

If the current session token for this instance is nil, this method finds the most recent active Parse::Session token for this user and applies it to the instance. The user instance will now be authenticated and logged in with the selected session token. Useful if you need to call save or destroy methods on behalf of a logged in user.

Returns:

  • (String)

    The session token or nil if no session was found for this user.



929
930
931
932
933
934
935
# File 'lib/parse/model/classes/user.rb', line 929

def any_session!
  unless @session_token.present?
    _active_session = active_sessions(restricted: false, order: :updated_at.desc).first
    self.session_token = _active_session.session_token if _active_session.present?
  end
  @session_token
end

Adds the third-party authentication data to for a given service.

Parameters:

  • service_name (Symbol)

    The name of the service (ex. :facebook)

  • data (Hash)

    The body of the OAuth data. Dependent on each service.

Raises:



330
331
332
333
334
# File 'lib/parse/model/classes/user.rb', line 330

def link_auth_data!(service_name, **data)
  response = client.set_service_auth_data(id, service_name, data)
  raise Parse::Client::ResponseError, response if response.error?
  self.class.with_authdata_trust { apply_attributes!(response.result) }
end

#logged_in?Boolean

Returns true if this user has a session token.

Returns:

  • (Boolean)

    true if this user has a session token.



447
448
449
# File 'lib/parse/model/classes/user.rb', line 447

def logged_in?
  self.session_token.present?
end

#login!(passwd = nil) ⇒ Boolean

Login and get a session token for this user.

Parameters:

  • passwd (String) (defaults to: nil)

    The password for this user.

Returns:

  • (Boolean)

    True/false if we received a valid session token.



574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
# File 'lib/parse/model/classes/user.rb', line 574

def login!(passwd = nil)
  self.password = passwd || self.password
  response = client.(username.to_s, password.to_s)
  if response.success?
    # Unlike signup, login's response is the canonical state of an
    # existing user, including any linked authData. Applying the
    # full response body here is intentional -- the server is
    # telling us what the account currently looks like. (Compare
    # signup, where we narrow to an allow-list because a brand-new
    # account has no legitimate authData to report.)
    self.class.with_authdata_trust { apply_attributes! response.result }
    # Drop the plaintext password from memory now that the login
    # has succeeded. Direct ivar assignment so the dirty tracker
    # doesn't record this clear as a pending change.
    @password = nil
    # Clear dirty state so a subsequent user.save! does not re-send
    # `password` (which Parse Server would treat as a password
    # change and use to revoke the session this login just issued).
    # See the matching note in #signup!.
    changes_applied!
    clear_partial_fetch_state!
  end
  self.session_token.present?
end

#logoutBoolean

Invalid the current session token for this logged in user.

Returns:

  • (Boolean)

    True/false if successful



601
602
603
604
605
606
607
608
# File 'lib/parse/model/classes/user.rb', line 601

def logout
  return true if self.session_token.blank?
  client.logout session_token
  self.session_token = nil
  true
rescue
  false
end

#logout_all!(keep_current: false) ⇒ Integer

Logout from all sessions, effectively signing out on all devices. Optionally keep the current session active.

Self-guard. Requires the user instance to carry a session token — i.e. to have been obtained via login/signup or attached via #session_token=. Without this, user.id = victim_id; user.logout_all! could revoke another user's sessions if the deployment has loose _Session write CLP. The guard fails closed in the SDK so the deployment's CLP isn't the only line of defense.

Examples:

# Logout from all devices
user.logout_all!

# Logout from all devices except current
user.logout_all!(keep_current: true)

Parameters:

  • keep_current (Boolean) (defaults to: false)

    if true, keeps the current session active (default: false)

Returns:

  • (Integer)

    the number of sessions revoked

Raises:



960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
# File 'lib/parse/model/classes/user.rb', line 960

def logout_all!(keep_current: false)
  return 0 unless id.present?
  require_self_session!("logout_all!")
  current_token = @session_token
  # Self-scope the _Session query: in client mode the ambient client
  # has no auth, so the query must carry this user's session token to
  # be authorized against /classes/_Session. Master-key mode ignores
  # the ambient since the master key still wins.
  count = Parse.with_session(current_token) do
    # Always revoke the OTHER sessions first under the live token —
    # destroying the calling session mid-loop invalidates the token
    # and the remaining deletes 401. Then, if not keeping current,
    # close the calling session via the dedicated logout endpoint.
    n = Parse::Session.revoke_all_for_user(self, except: current_token)
    unless keep_current
      begin
        Parse.client.logout(current_token)
        n += 1
      rescue Parse::Error::InvalidSessionTokenError
        # The calling session was already gone (server-side TTL or
        # concurrent revoke). Idempotent: count what we destroyed.
      end
    end
    n
  end
  @session_token = nil unless keep_current
  @session = nil unless keep_current
  count
end

#multi_session?Boolean

Check if this user has multiple active sessions (logged in on multiple devices).

Examples:

if user.multi_session?
  puts "User is logged in on multiple devices"
end

Returns:

  • (Boolean)

    true if user has more than one active session



1027
1028
1029
# File 'lib/parse/model/classes/user.rb', line 1027

def multi_session?
  active_session_count > 1
end

#password=(value) ⇒ String

You may set a password for this user when you are creating them. Parse never returns a Parse::User's password when a record is fetched. Therefore, normally this getter is nil. While this API exists, it is recommended you use either the #login! or #signup! methods. (see #login!)

Returns:

  • (String)

    The password you set.



204
# File 'lib/parse/model/classes/user.rb', line 204

property :password

#request_password_resetBoolean

Request a password reset for this user

Returns:

  • (Boolean)

    true if it was successful requested. false otherwise.

See Also:



454
455
456
457
# File 'lib/parse/model/classes/user.rb', line 454

def request_password_reset
  return false if email.nil?
  Parse::User.request_password_reset(email)
end

#sessionSession

Returns the session corresponding to the user's session token.

Returns:

  • (Session)

    the session corresponding to the user's session token.



617
618
619
620
621
622
623
624
625
626
627
628
# File 'lib/parse/model/classes/user.rb', line 617

def session
  if @session.blank? && @session_token.present?
    response = client.fetch_session(@session_token)
    # Trusted hydration: +response.result+ is the server-side
    # _Session row, which legitimately includes +sessionToken+,
    # +createdAt+, +updatedAt+, and other protected keys. Route
    # through {Parse::Object.build} which handles the trusted-init
    # signalling.
    @session ||= Parse::Object.build(response.result, Parse::Model::CLASS_SESSION)
  end
  @session
end

#sessionsArray<Parse::Session>

Get all active sessions for this user. Requires an authenticated session (see #logout_all! for the rationale).

Examples:

user.sessions.each do |session|
  puts "Session created: #{session.created_at}"
end

Returns:

Raises:



1013
1014
1015
1016
1017
1018
1019
# File 'lib/parse/model/classes/user.rb', line 1013

def sessions
  return [] unless id.present?
  require_self_session!("sessions")
  Parse.with_session(@session_token) do
    Parse::Session.for_user(self).all
  end
end

#signup!(passwd = nil) ⇒ Boolean

You may set a password for this user when you are creating them. Parse never returns a

Parameters:

  • passwd (defaults to: nil)

    The user's password to be used for signing up.

Returns:

  • (Boolean)

    True if signup it was successful. If it fails an exception is thrown.

Raises:



468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
# File 'lib/parse/model/classes/user.rb', line 468

def signup!(passwd = nil)
  self.password = passwd || password
  if username.blank?
    raise Parse::Error::UsernameMissingError, "Signup requires a username."
  end

  if password.blank?
    raise Parse::Error::PasswordMissingError, "Signup requires a password."
  end

   = attribute_updates
  # See {#signup_create} for the rationale on the safe-pattern check.
  if self.class.()
    .except!(:createdAt, :updatedAt, "createdAt", "updatedAt")
  else
    .except!(*Parse::Properties::BASE_FIELD_MAP.flatten)
  end
  self.class.strip_server_controlled_keys!()

  # first signup the user, then save any additional attributes
  response = client.create_user 

  if response.success?
    # Restrict what the server can plant into the in-memory user via
    # the signup response, matching the defense in {#signup_create}.
    # `POST /parse/users` legitimately returns objectId, createdAt,
    # updatedAt (extracted into @-vars directly below), sessionToken,
    # and emailVerified. Any other key in the response body --
    # `authData`, `_rperm`, `_wperm`, `roles`, etc. -- is dropped, so
    # a compromised or MITM'd Parse Server cannot use this code path
    # to plant credentials/permissions onto the user we just signed
    # up. The previous `apply_attributes! response.result` accepted
    # every key the server returned through the typed property
    # writers (`authData_set_attribute!` exists because we declare
    # `property :auth_data, :object`), which was a footgun the
    # save-as-signup path had already addressed.
    result = response.result
    @id = result[Parse::Model::OBJECT_ID] || @id
    @created_at = result["createdAt"] || @created_at
    @updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
    set_attributes!(result.slice(*SIGNUP_RESPONSE_APPLY_KEYS))
    # Drop the plaintext password from memory now that the server
    # has it hashed and we no longer need it. Matches the Parse JS
    # SDK behavior of clearing the password attribute after a
    # successful save/signup. Uses direct ivar assignment so the
    # dirty tracker doesn't record this clear as a pending change
    # that would be re-sent on the next save.
    @password = nil
    # Mirror Parse::Object#save: a successful round-trip means the
    # locally-set credential fields are now in sync with the server
    # and must NOT be re-sent on the next save. Without this, a
    # subsequent user.save! re-transmits `password`, which Parse
    # Server treats as a password change under
    # revokeSessionOnPasswordReset and revokes the session just
    # minted by this signup.
    changes_applied!
    clear_partial_fetch_state!
    return true
  end

  case response.code
  when Parse::Response::ERROR_USERNAME_MISSING
    raise Parse::Error::UsernameMissingError, response
  when Parse::Response::ERROR_PASSWORD_MISSING
    raise Parse::Error::PasswordMissingError, response
  when Parse::Response::ERROR_USERNAME_TAKEN
    raise Parse::Error::UsernameTakenError, response
  when Parse::Response::ERROR_EMAIL_TAKEN
    raise Parse::Error::EmailTakenError, response
  when Parse::Response::ERROR_EMAIL_INVALID
    raise Parse::Error::InvalidEmailAddress, response
  end
  raise Parse::Client::ResponseError, response
end

#signup_on_saveBoolean

When true (default), saving a new Parse::User that has a password value routes through Parse Server's signup endpoint (POST /parse/users) with the X-Parse-Revocable-Session header set, so the signup response returns a session token that is applied to the in-memory user object via the standard sessionToken_set_attribute! hydration path. Without this flag, Parse::User.new(...).save! left session_token nil because the underlying create path did not request a revocable session.

Set to false to always create users without requesting a revocable session token - for example, when a master-key server-side script is provisioning user rows that will receive credentials later. New users created with no password always fall through to the standard create path regardless of this flag.

auth_data (federated identity / OAuth) signup is deliberately NOT triggered by this flag. POST /parse/users treats auth_data as a claim against an existing account, so allowing mass-assigned auth_data to trigger a revocable-session signup would let attacker-controlled params plant another user's session token onto the in-memory object. Use autologin_service or signup (the explicit class methods) for OAuth-driven signup; both bypass the mass-assignment filter because the caller is explicitly choosing the federated-identity flow.

Inherited through subclasses via ActiveSupport::Concern's class_attribute, so an application-specific subclass may override the default without affecting Parse::User itself.

Returns:

  • (Boolean)


180
# File 'lib/parse/model/classes/user.rb', line 180

class_attribute :signup_on_save, instance_writer: false

Removes third-party authentication data for this user

Parameters:

  • service_name (Symbol)

    The name of the third-party service (ex. :facebook)

Returns:

  • (Boolean)

    True/false if successful.

Raises:



340
341
342
343
344
# File 'lib/parse/model/classes/user.rb', line 340

def unlink_auth_data!(service_name)
  response = client.set_service_auth_data(id, service_name, nil)
  raise Parse::Client::ResponseError, response if response.error?
  self.class.with_authdata_trust { apply_attributes!(response.result) }
end

#upgrade_anonymous!(username:, password:, email: nil) ⇒ Boolean

Upgrade an anonymous user (one created via the authData.anonymous provider) into a full username/password account. This is the SDK-side counterpart of the Parse JS SDK's _linkWith('username', ...) flow — it sends a single PUT /users/:id with the new credentials and an explicit authData: { anonymous: nil } unlink in the same body, then narrowly applies the server's response to the in-memory user.

The authData.anonymous unlink is essential: leaving the anonymous provider attached after assigning a username would let anyone else who somehow learned the (random) anonymous id silently log in as the freshly-named account, a documented Parse foot-gun.

Parameters:

  • username (String)

    the username to claim. Must be unique.

  • password (String)

    the password to set on the account.

  • email (String, nil) (defaults to: nil)

    optional email address. Must be unique if provided.

Returns:

  • (Boolean)

    true on success.

Raises:



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'lib/parse/model/classes/user.rb', line 375

def upgrade_anonymous!(username:, password:, email: nil)
  require_self_session!(:upgrade_anonymous!)
  if @id.nil? || @id.to_s.empty?
    raise Parse::Error::AuthenticationError,
          "Parse::User#upgrade_anonymous! requires a saved user (no objectId)"
  end
  unless anonymous?
    raise Parse::Error::AuthenticationError,
          "Parse::User#upgrade_anonymous! is only valid for anonymous users " \
          "(authData.anonymous is not present on this instance)"
  end
  if username.nil? || username.to_s.empty?
    raise Parse::Error::UsernameMissingError, "upgrade_anonymous! requires a username."
  end
  if password.nil? || password.to_s.empty?
    raise Parse::Error::PasswordMissingError, "upgrade_anonymous! requires a password."
  end

  body = {
    username: username.to_s,
    password: password.to_s,
    # Explicitly unlink the anonymous provider in the same request
    # that claims the new credentials — otherwise the account
    # remains takeover-able via the anonymous id.
    authData: { anonymous: nil },
  }
  body[:email] = email.to_s if email.is_a?(String) && !email.empty?

  response = client.update_user(@id, body, session_token: @session_token)

  if response.success?
    result = response.result || {}
    @updated_at = result["updatedAt"] || @updated_at
    # Parse Server may rotate the session token on a credential
    # change; apply it narrowly if present without going through the
    # full property writer chain.
    if result["sessionToken"].is_a?(String) && !result["sessionToken"].empty?
      @session_token = result["sessionToken"]
    end
    @auth_data.delete("anonymous") if @auth_data.is_a?(Hash)
    @username = username.to_s
    @email = email.to_s if email.is_a?(String) && !email.empty?
    @password = nil
    changes_applied!
    clear_partial_fetch_state!
    return true
  end

  case response.code
  when Parse::Response::ERROR_USERNAME_MISSING
    raise Parse::Error::UsernameMissingError, response
  when Parse::Response::ERROR_PASSWORD_MISSING
    raise Parse::Error::PasswordMissingError, response
  when Parse::Response::ERROR_USERNAME_TAKEN
    raise Parse::Error::UsernameTakenError, response
  when Parse::Response::ERROR_EMAIL_TAKEN
    raise Parse::Error::EmailTakenError, response
  when Parse::Response::ERROR_EMAIL_INVALID
    raise Parse::Error::InvalidEmailAddress, response
  end
  raise Parse::Client::ResponseError, response
end

#with_session { ... } ⇒ Object

Block-scoped sugar around Parse.with_session: runs the block with this user's session_token as the ambient session token for the current fiber. Every Parse call inside the block that doesn't explicitly pass session_token: or use_master_key: true will be sent as this user.

Examples:

user = Parse::User.login!("alice", "pw")
user.with_session do
  Post.all                # scoped to alice
  post.save               # scoped to alice
end

Yields:

  • runs the block with the user's session in ambient scope.

Returns:

  • (Object)

    the block's return value.

Raises:



914
915
916
917
918
919
920
921
922
# File 'lib/parse/model/classes/user.rb', line 914

def with_session(&block)
  raise ArgumentError, "Parse::User#with_session requires a block" unless block_given?
  unless @session_token.is_a?(String) && !@session_token.empty?
    raise Parse::Error::AuthenticationError,
          "Parse::User#with_session requires an authenticated session — " \
          "obtain the instance via login/signup or call `user.session_token = '...'` first"
  end
  Parse.with_session(@session_token, &block)
end