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

GitHub OAuth with identity_resource seems to be broken #555

Open
yasoob opened this issue Jan 25, 2024 · 7 comments
Open

GitHub OAuth with identity_resource seems to be broken #555

yasoob opened this issue Jan 25, 2024 · 7 comments

Comments

@yasoob
Copy link
Contributor

yasoob commented Jan 25, 2024

Hi,

I followed the guides and was able to get the GitHub Oauth working. However, this was without the identity_resource being configured. Then I decided to configure the identity_resource and make use of that but now I am getting this error:

%Ash.Error.Invalid{
        errors: [
          %Ash.Error.Changes.InvalidAttribute{
            field: :uid,
            message: "is invalid",
            private_vars: nil,
            value: 3696393,
            changeset: nil,
            query: nil,
            error_context: [],
            vars: [],
            path: [],
            stacktrace: #Stacktrace<>,
            class: :invalid
          }
        ],
...

Here is the complete changeset:

%Ash.Error.Invalid{
  errors: [
    %Ash.Error.Changes.InvalidAttribute{
      field: :uid,
      message: "is invalid",
      private_vars: nil,
      value: 3696393,
      changeset: nil,
      query: nil,
      error_context: [],
      vars: [],
      path: [],
      stacktrace: #Stacktrace<>,
      class: :invalid
    }
  ],
  stacktraces?: true,
  changeset: #Ash.Changeset<
    action_type: :create,
    action: :register_with_github,
    attributes: %{email: #Ash.CiString<"[email protected]">},
    relationships: %{},
    arguments: %{
      user_info: %{
        "email" => "[email protected]",
        "email_verified" => true,
        "name" => "M.Yasoob Ullah Khalid ☺",
        "picture" => "https://avatars.githubusercontent.com/u/3696393?v=4",
        "preferred_username" => "yasoob",
        "profile" => "https://github.com/yasoob",
        "sub" => 3696393
      },
      oauth_tokens: %{
        "access_token" => "gho_redacted",
        "scope" => "read:user,user:email",
        "token_type" => "bearer"
      }
    },
    errors: [
      %Ash.Error.Invalid{
        errors: [
          %Ash.Error.Changes.InvalidAttribute{
            field: :uid,
            message: "is invalid",
            private_vars: nil,
            value: 3696393,
            changeset: nil,
            query: nil,
            error_context: [],
            vars: [],
            path: [],
            stacktrace: #Stacktrace<>,
            class: :invalid
          }
        ],
        stacktraces?: true,
        changeset: #Ash.Changeset<
          api: Daakia.Accounts,
          action_type: :create,
          action: :upsert,
          attributes: %{
            id: "64eacf25-2578-4af9-b45d-4ee574e955fa",
            strategy: "github",
            user_id: "944e9955-8a5d-4547-9b97-e358f677f709",
            access_token: "gho_redacted",
            access_token_expires_at: nil
          },
          relationships: %{},
          arguments: %{
            user_info: %{
              "email" => "[email protected]",
              "email_verified" => true,
              "name" => "M.Yasoob Ullah Khalid ☺",
              "picture" => "https://avatars.githubusercontent.com/u/3696393?v=4",
              "preferred_username" => "yasoob",
              "profile" => "https://github.com/yasoob",
              "sub" => 3696393
            },
            oauth_tokens: %{
              "access_token" => "gho_redacted",
              "scope" => "read:user,user:email",
              "token_type" => "bearer"
            },
            user_id: "944e9955-8a5d-4547-9b97-e358f677f709"
          },
          errors: [
            %Ash.Error.Changes.InvalidAttribute{
              field: :uid,
              message: "is invalid",
              private_vars: nil,
              value: 3696393,
              changeset: nil,
              query: nil,
              error_context: [],
              vars: [],
              path: [],
              stacktrace: #Stacktrace<>,
              class: :invalid
            }
          ],
          data: #Daakia.Accounts.UserIdentity<
            user: #Ash.NotLoaded<:relationship>,
            __meta__: #Ecto.Schema.Metadata<:built, "user_identities">,
            refresh_token: nil,
            access_token_expires_at: nil,
            access_token: nil,
            uid: nil,
            strategy: nil,
            id: nil,
            user_id: nil,
            aggregates: %{},
            calculations: %{},
            ...
          >,
          context: %{authorize?: false, actor: nil},
          valid?: false
        >,
        query: nil,
        error_context: [nil],
        vars: [],
        path: [],
        stacktrace: #Stacktrace<>,
        class: :invalid
      },
      %Ash.Error.Changes.InvalidAttribute{
        field: :uid,
        message: "is invalid",
        private_vars: nil,
        value: 3696393,
        changeset: nil,
        query: nil,
        error_context: [],
        vars: [],
        path: [],
        stacktrace: #Stacktrace<>,
        class: :invalid
      }
    ],
    data: #Daakia.Accounts.User<
      identities: #Ash.NotLoaded<:relationship>,
      __meta__: #Ecto.Schema.Metadata<:built, "users">,
      id: nil,
      email: nil,
      aggregates: %{},
      calculations: %{},
      ...
    >,
    context: %{authorize?: false, actor: nil},
    valid?: true
  >,
  query: nil,
  error_context: [nil, nil, nil],
  vars: [],
  path: [],
  stacktrace: #Stacktrace<>,
  class: :invalid
}

This is how my register_with_github actions looks like:

create :register_with_github do
      argument :user_info, :map, allow_nil?: false
      argument :oauth_tokens, :map, allow_nil?: false
      upsert? true
      upsert_identity :unique_email

      # Required if you have token generation enabled.
      change AshAuthentication.GenerateTokenChange

      # Required if you have the `identity_resource` configuration enabled.
      change AshAuthentication.Strategy.OAuth2.IdentityChange

      change fn changeset, _ ->
        dbg(changeset)
        user_info = Ash.Changeset.get_argument(changeset, :user_info)

        Ash.Changeset.change_attributes(changeset, Map.take(user_info, ["email"]))
      end
    end

And this is how my user identity looks like:

defmodule Daakia.Accounts.UserIdentity do
  use Ash.Resource,
    data_layer: AshPostgres.DataLayer,
    extensions: [AshAuthentication.UserIdentity]

  user_identity do
    api Daakia.Accounts
    user_resource Daakia.Accounts.User
  end

  postgres do
    table "user_identities"
    repo Daakia.Repo
  end
end
@zachdaniel
Copy link
Collaborator

What does Ash.Resource.Info.attributes(Daakia.Accounts.UserIdentity) show?

@yasoob
Copy link
Contributor Author

yasoob commented Jan 25, 2024

[
  %Ash.Resource.Attribute{
    name: :refresh_token,
    type: Ash.Type.String,
    allow_nil?: true,
    generated?: false,
    primary_key?: false,
    private?: false,
    writable?: true,
    always_select?: false,
    default: nil,
    update_default: nil,
    description: nil,
    source: :refresh_token,
    match_other_defaults?: false,
    sensitive?: false,
    filterable?: true,
    constraints: [allow_empty?: false, trim?: true]
  },
  %Ash.Resource.Attribute{
    name: :access_token_expires_at,
    type: Ash.Type.UtcDatetimeUsec,
    allow_nil?: true,
    generated?: false,
    primary_key?: false,
    private?: false,
    writable?: true,
    always_select?: false,
    default: nil,
    update_default: nil,
    description: nil,
    source: :access_token_expires_at,
    match_other_defaults?: false,
    sensitive?: false,
    filterable?: true,
    constraints: [precision: :microsecond, timezone: :utc]
  },
  %Ash.Resource.Attribute{
    name: :access_token,
    type: Ash.Type.String,
    allow_nil?: true,
    generated?: false,
    primary_key?: false,
    private?: false,
    writable?: true,
    always_select?: false,
    default: nil,
    update_default: nil,
    description: nil,
    source: :access_token,
    match_other_defaults?: false,
    sensitive?: false,
    filterable?: true,
    constraints: [allow_empty?: false, trim?: true]
  },
  %Ash.Resource.Attribute{
    name: :uid,
    type: Ash.Type.String,
    allow_nil?: false,
    generated?: false,
    primary_key?: false,
    private?: false,
    writable?: true,
    always_select?: false,
    default: nil,
    update_default: nil,
    description: nil,
    source: :uid,
    match_other_defaults?: false,
    sensitive?: false,
    filterable?: true,
    constraints: [allow_empty?: false, trim?: true]
  },
  %Ash.Resource.Attribute{
    name: :strategy,
    type: Ash.Type.String,
    allow_nil?: false,
    generated?: false,
    primary_key?: false,
    private?: false,
    writable?: true,
    always_select?: false,
    default: nil,
    update_default: nil,
    description: nil,
    source: :strategy,
    match_other_defaults?: false,
    sensitive?: false,
    filterable?: true,
    constraints: [allow_empty?: false, trim?: true]
  },
  %Ash.Resource.Attribute{
    name: :id,
    type: Ash.Type.UUID,
    allow_nil?: false,
    generated?: false,
    primary_key?: true,
    private?: false,
    writable?: true,
    always_select?: false,
    default: &Ash.UUID.generate/0,
    update_default: nil,
    description: nil,
    source: :id,
    match_other_defaults?: false,
    sensitive?: false,
    filterable?: true,
    constraints: []
  },
  %Ash.Resource.Attribute{
    name: :user_id,
    type: Ash.Type.UUID,
    allow_nil?: true,
    generated?: false,
    primary_key?: false,
    private?: false,
    writable?: true,
    always_select?: false,
    default: nil,
    update_default: nil,
    description: nil,
    source: :user_id,
    match_other_defaults?: false,
    sensitive?: false,
    filterable?: true,
    constraints: []
  }
]

@zachdaniel
Copy link
Collaborator

Interesting :) So the problem here is that the id from GH is an integer, and AshAuthentication expects a string. We can solve this one of two ways:

  1. Ash.Type.cast_input(:string, 1234) could automatically convert to a string. this seems reasonable to me, but I'll want to think bout it a bit more.
  2. you could update the sub key to be a string yourself
  3. we could add some specific support for integers, like converting to a string, in AshAuthentication.

@yasoob
Copy link
Contributor Author

yasoob commented Jan 25, 2024

At what stage should I be updating sub to be a string in my action? Can you please provide some sample code? IMO it would be better to have this support merged in master sometime as this isn't something one would expect to have to do on their own.

@zachdaniel
Copy link
Collaborator

You'd need to put something like this above change AshAuthentication.Strategy.OAuth2.IdentityChange

      change fn changeset, _ ->
        new_user_info = 
          changeset
          |> Ash.Changeset.get_argument(:user_info)
          |> Map.update!("sub", &String.to_integer/1)
       
        Ash.Changeset.set_argument(changeset, :user_info, new_user_info)
      end

@yasoob
Copy link
Contributor Author

yasoob commented Jan 25, 2024

It should be Integer.to_string/1 but otherwise it looks good. Thanks! For future reference, here is what I update my action to:

create :register_with_github do
      argument :user_info, :map, allow_nil?: false
      argument :oauth_tokens, :map, allow_nil?: false
      upsert? true
      upsert_identity :unique_email

      change fn changeset, _ ->
        IO.puts("Debugging the changeset before:")
        dbg(changeset)
      end

      # Required if you have token generation enabled.
      change AshAuthentication.GenerateTokenChange

      # Required if you have the `identity_resource` configuration enabled.
      change AshAuthentication.Strategy.OAuth2.IdentityChange

      change fn changeset, _ ->
        IO.puts("Debugging the changeset after 1:")
        dbg(changeset)

        user_info =
          Ash.Changeset.get_argument(changeset, :user_info)
          |> Map.update!("sub", &Integer.to_string/1)

        Ash.Changeset.change_attributes(changeset, Map.take(user_info, ["email"]))
        |> Ash.Changeset.set_argument(:user_info, user_info)
        |> dbg()
      end

@yasoob
Copy link
Contributor Author

yasoob commented Jan 25, 2024

I will leave the issue open till it is fixed in main 👍

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

2 participants