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

Create User with AppMetadata #1280

Open
mosnicholas opened this issue Oct 20, 2023 · 11 comments
Open

Create User with AppMetadata #1280

mosnicholas opened this issue Oct 20, 2023 · 11 comments
Assignees

Comments

@mosnicholas
Copy link

App metadata seems to be the recommended way to pass app-specific information to the user create flow. If it is specified in the createUser api call, it should be used in the create user GoTrue function.

I'm trying to pass the following flag to app_metadata: { is_invited: true } to be able to use in a Postgres trigger and use different logic based on whether a user was invited or not.

I'll be using user_metadata for now, but feels like app_metadata is more appropriate?

/**
   * A custom data object to store the user's application specific metadata. This maps to the `auth.users.app_metadata` column.
   *
   * Only a service role can modify.
   *
   * The `app_metadata` should be a JSON object that includes app-specific info, such as identity providers, roles, and other
   * access control information.
   */
  app_metadata?: object

https://github.com/supabase/gotrue/blob/40aed622f24066b2718e4509a001026fe7d4b76d/internal/api/admin.go#L352-L362

@J0
Copy link
Contributor

J0 commented Oct 23, 2023

Hey @mosnicholas,

Yes, It is possible to create a user and set app_metadata as you wish when creating a user.

const { data, error } = await supabase.auth.admin.createUser({
  email: 'user@email.com',
  password: 'password',
  app_metadata: { is_invited: 'true' }
})

You can choose between using user_metadata and app_metadata as needed. Please let us know if you run into any issues while using the flow or if there are any doubt/concerns that we haven't addressed.

Let us know!
Joel

@mosnicholas
Copy link
Author

@J0 -- I am using a Postgres trigger to check whether users are allowed to sign up / sign in. If a user is invited, I want to short circuit that logic, and always allow the user to be created in our db. I'm passing the is_invited flag through the API call to ensure that my postgres trigger can act on the different logic paths based on whether a user was invited or is signing up from the app.

My understanding from the docs is that the best place to put this flag (is_invited) is in the app_metadata (ie. app-specific access control info). However, the user is being created in the DB before the app_metadata object is passed into the user row, and as a result, I have to use the user_metadata object to pass the flag back to the Postgres trigger.

This is why I raised the issue -- essentially, we're creating the app_metadata object as a fixed object, and then updating the user object after creation. Feels like maybe we should create the app_metadata object with the values passed to the API on the first pass instead of updating it after the user row is created?

For reference, here's my Postgres trigger:

CREATE OR REPLACE FUNCTION public.check_user_email_signup()
 RETURNS trigger
 LANGUAGE plpgsql
 SECURITY DEFINER
AS $function$
DECLARE
  email_domain text;
  domain_exists boolean;
  all_domains text[];
BEGIN
    -- Check if the current user was invited
    IF NEW.raw_user_meta_data->>'is_invited' = 'true' THEN
      return NEW;
    END IF;

    -- If not, check that user is allowed to sign up
    ...

    -- If not, raise an exception
    RAISE EXCEPTION 'Email domain is not allowed';
END;
$function$
;

@J0 J0 self-assigned this Nov 8, 2023
@J0
Copy link
Contributor

J0 commented Nov 8, 2023

Hey @mosnicholas,

Thanks for the comprehensive overview - I may be missing something but I agree that we should probably set the params.AppMetadata together with the provider fields before creating a user.

At the same time, an issue with changing the behaviour is that people may have update triggers or similar depend on the existing behaviour where the update is performed after the user object is created.

I'll discuss with the team and get back.
Thanks!

@mosnicholas
Copy link
Author

Hey @J0 any progress on your conversations here? :) I've had to hijack the raw_user_meta_data json which has resulted in a bit of data loss :(

Screenshot 2023-11-20 at 3 35 04 PM

@mosnicholas
Copy link
Author

@J0 this set up is causing a separate problem all together. Because app_metadata is updated after creating a user row in the database, we can't listen to the user insert call to update app metadata. Eg. if you have a trigger like so:

CREATE TRIGGER add_user_tenant_id AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION add_custom_claim();

and that function updates the app_metadata field. There's now a race condition because after the user is created, my trigger function is called, and app metadata is updated separately.

This trigger would be helpful to set up multitenant mapping and ensure that we have the right tenant ids set up properly. Curious what you think!

@GaryAustin1
Copy link

Without digging into this in detail, it seems that if the update gotrue does on raw_app_meta_data just merged in existing keys from the start then it would not care if the extra metadata was added on the insert trigger or the update trigger.

@craxrev
Copy link

craxrev commented Feb 21, 2024

I have a similar situation where I was following a guide on how to integrate picket, seems to be not working so far, the issue is when I add custom app_metadata and user_metadata using supabase.auth.admin.createdUser() method from the sdk it does not get reflected when inserting in a "profiles" table through an AFTER INSERT trigger, as in both app_metadata and user_metadata have default values and not the ones I provided, so I had to also add an AFTER UPDATE trigger, here's how I did it:

-- inserts a row into public.profiles
CREATE OR REPLACE function public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER set search_path = public
AS $$
BEGIN
  IF (TG_OP = 'INSERT') THEN
    INSERT INTO public.profiles (user_id, email)
    VALUES (NEW.id, NEW.email);
  END IF;
  IF (TG_OP = 'UPDATE') THEN
    UPDATE public.profiles
    SET (wallet_address, username) = (NEW.raw_app_meta_data->>'walletAddress', NEW.raw_user_meta_data->>'username')
    WHERE user_id = NEW.id;
  END IF;
  RETURN NEW;
END;
$$;

-- trigger the function every time a user is created
CREATE OR REPLACE trigger on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();

-- trigger the function every time a app/user metadata is updated
CREATE OR REPLACE trigger on_auth_user_metadata_updated
  AFTER UPDATE ON auth.users
  FOR EACH ROW
  WHEN (OLD.raw_app_meta_data <> NEW.raw_app_meta_data OR OLD.raw_user_meta_data <> NEW.raw_user_meta_data)
  EXECUTE PROCEDURE public.handle_new_user()

But still not sure if this will cause any problems down the line..

Edit: fix inserting null values

@activenode
Copy link

Wanted to push this one. For someone implementing triggers based on app_metadata, this isn't possible right now. I also had to write a weird workaround as not even the AFTER UPDATE worked as it STILL didn't have the app_metadata - so I wrote an SQL logic that runs on update but also checks if the values are set or not - if not, it just continues. That's kinda weird because it allows for orphans.

@J0
Copy link
Contributor

J0 commented Apr 15, 2024

Hey @mosnicholas,

Sorry, I missed the past messages. I've forgotten a large part of the context but you might be able to use custom claims auth hooks to achieve your use case. With respect to this particular issue, I'll check again if it's possible to create the user with app_metadata directly instead of creating and updating.

Hope to circle back with updates soon

@activenode
Copy link

With respect to this particular issue, I'll check again if it's possible to create the user with app_metadata directly instead of creating and updating.

That'd be helpful I think as it would allow to not wait for 2 Updates (I had to bypass an INSERT and another UPDATE and then the following UPDATE did contain the data). Thanks!

@J0
Copy link
Contributor

J0 commented Apr 23, 2024

Short update here - we're doing a quick scan for update triggers to see how we can move ahead

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants