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

Better sign_in_with_slack integration #42

Open
JustLey opened this issue Oct 5, 2016 · 6 comments
Open

Better sign_in_with_slack integration #42

JustLey opened this issue Oct 5, 2016 · 6 comments

Comments

@JustLey
Copy link

JustLey commented Oct 5, 2016

I've been reading the issues about how to get both Add to Slack and Sign in with Slack and they've been pretty insightful, but I got a different situation.

My application is a Slack App that keeps a lot of things synced with Slack. Users can both add it to Slack AND sign in to an external web app. When the current workaround is used, you get two registries for the same user, because the provider differs through each button and 'first_or_create' don't find anything in the database already with both provider and uid.

I'm currently forcing my model's 'from_omniauth' method to use 'slack' as a provider when it detects 'sign_in_with slack', but it feels kinda off to force a provider change. Is there any way we can make it better? Or am I missing something?

Thanks for your time and for the great work :D

@ginjo
Copy link

ginjo commented Oct 12, 2016

@JustLey, If I understand correctly - which I may not 😄 - it sounds like you are using different omniauth provider 'aliases' (same Slack, but different omniauth provider lable with different scopes) for what is essentially the same Slack account. I went down that road for awhile, thinking I needed different providers for different levels of access. I started keeping multiple 'identity' records for each user and had to sort out which one was the most current, how or when to merge the identities, and what exactly where my user's current access scopes. My app was quickly getting out of control with user/identity management complexity.

After digging deeper into the Slack API and the omniauth-slack gem, I started to realize that there is no need to manage separate providers, identities, or sets of scopes for what is really a single user with clearly defined access privileges. The ah-ha moment was after reading a line in Slack's docs that said something to the effect of (paraphrasing here), "Authorize your users in stages, if your app requires different scopes at different points, making multiple passes through the oauth process, each time adding scopes to that users' token. Once a user is "all scoped up", you only need a simple sign-in-with-slack to retrieve the token containing all of their scopes). So I realized that no matter what 'provider' you call, or what scopes you request, the token you get back is always the same token for that user and always has ALL of the scopes ever authorized for that user, forever (or until revoked by the user, admin, or app owner).

So I switched to using a single slack provider in omniauth, and now I maintain only a single Identity record for each user, updated each time they log in. This works much better for me, and my app is finally out of the fire-swamps.

The caveat to get this working, however, is that I needed the list of authorized scopes returned by each oauth authorization response - and I couldn't find any versions of omniauth-slack that had that information. So I forked the gem and added that feature. I also bridged the perceived divide between "sign in with slack" and "add to slack". In my opinion, the only difference is in the scopes requested and the format of the returned auth_hash. Slack's documentation gives the impression that there is much more of a difference, but the actual API functionality doesn't really have such a divide, in my opinion (ok there are some exceptions, like the scope 'incomming-webhook' which actually creates & returns a new webhook url).

So, bottom line: one provider for Slack, and one identity record per user. Authorize your methods/actions/whatever based on real scopes for that user. Use multiple passes through the oauth process, if it suits your app.

Ok, wayyy more than I intended to write here. Hopefully it's more helpful than confusing. 😅

@JustLey
Copy link
Author

JustLey commented Oct 16, 2016

I get what you're saying.

I'm trying to avoid this multiple passes because passing multiple times sequentially in the OAuth process looks terrible from the client's point of view.

Also, I have two buttons. The "Add to Slack" ends up passing two times: one for bot/slash commands/whatever, other to sign in the user. The second button is pure sign in (if someone in the team had already installed the application). But in the end, I have only one provider (because obviously my first pass don't need to keep a record yet).

Now I'm having another "issue", which is another thing some people tried to fix around here: Add to Slack can't return a users info, though this PR and this one tried to fix it. That forces me to redirect Add to Slack to Sign In with Slack OAuth flow, which is exactly what I was trying not to do because I had some complaints like "why is this asking me twice?" since the user don't know want is happening behind the curtains.

Thanks for taking the time to help me :D

Edit: forgot to say, I was starting to have the same issue as you with multiple records (although I had only the provider being different, so, two records per user) when I forced the 'sign_in_with_slack' to save as 'slack' in my user's 'provider' column.

Edit 2: I didn't meant "client" in the second sentence, I meant "user". Sorry

@ginjo
Copy link

ginjo commented Oct 26, 2016

I'm still trying to make sense of Slack's different api-flows myself. Even though they have lots of good documentation on each type, I still can't visualize the full difference.

It would appear that if you want all three of these

  • sign-in-with-slack (identity.xxxx scopes only)
  • add-to-slack (incoming-webhook scope)
  • deeper user/team/whatever information (users:read, team:read, chat:write:bot scopes, etc.)

... you will need three passes thru the oauth cycle, as each of those sets of scopes are supposedly incompatible with each other (though I can't find definitive documentation on that). In my own app, I don't use webhooks. I go straight thru the API for all posts, so I only need two passes thru oauth. That's ok with me, as I'll have a "Add to / Sign up / Get started" button for initial setup. Then forever after, for that user, it's just a "Sign in with Slack" button. The max number of oauth passes thru Slack's chooser pages should only be two, for any given user in my app.

I have no trouble getting user info during the oauth cycle, long as I'm not requesting the incoming-webhook scope. Incoming-webhook scope would appear to preclude any further user information scopes, at least during that pass thru oauth.

I'm also using my own fork of this gem, which should pull any available user info, if at all possible. Give it a run, and see if it helps. I've tried to make it as universally compatible as possible.

For example and reference, here is my Identity model, based on the gem's authorization response to a 'incoming-webhook' oauth request. All sensitive data has been xxxxx'd out. Slack added the 'identify' scope automatically.

Ultimately, I hope Slack decides to reduce the incompatibilities between different scopes. Otherwise, I'm not sure if there's anything we can do on the ruby side to reduce the complexity of the authorization process. If anyone else has info or opinions, please chime in! 😃

- !ruby/object:MyIdentityRecordBasedOnOmniAuthHash
  id: 173
  user_id: 3
  email: NA
  provider: slack
  uid: xxxxxxxxx-xxxxxxx
  info: !ruby/hash:OmniAuth::AuthHash::InfoHash
    name: NA
    email: NA
    user_id: xxxxxxxxx
    team: xxxxxxxxxx
    team_id: xxxxxxxxxx
    image: 
    first_name: 
    last_name: 
    phone: 
    skype: 
    avatar_hash: 
    real_name: 
    real_name_normalized: 
    deleted: 
    status: 
    color: 
    tz: 
    tz_label: 
    tz_offset: 
    is_admin: 
    is_owner: 
    is_primary_owner: 
    is_restricted: 
    is_ultra_restricted: 
    is_bot: 
    has_2fa: 
    team_domain: 
    team_image: 
    team_email_domain: 
    nickname: 
  credentials: !ruby/hash:OmniAuth::AuthHash
    token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    expires: false
    scope: identify,incoming-webhook
  extra: !ruby/hash:OmniAuth::AuthHash
    web_hook_info: !ruby/hash:OmniAuth::AuthHash
      channel: "#random"
      channel_id: xxxxxxxxxx
      configuration_url: https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
      url: https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    bot_info: 
    auth: !ruby/hash:OmniAuth::AuthHash
      ok: true
      scope: identify,incoming-webhook
      user_id: xxxxxxxxxxxxx
      team_name: xxxxxxxxxxxxx
      team_id: xxxxxxxxxxxx
      incoming_webhook: !ruby/hash:OmniAuth::AuthHash
        channel: "#random"
        channel_id: xxxxxxxxxxx
        configuration_url: https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        url: https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxxxxxxxx
      token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    identity: 
    user_info: 
    user_profile: 
    team_info: 
    raw_info: !ruby/hash:OmniAuth::AuthHash
      auth: !ruby/object:OAuth2::AccessToken
        token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        refresh_token: 
        expires_in: 
        expires_at: 
        options:
          :mode: :query
          :header_format: Bearer %s
          :param_name: token
        params:
          ok: true
          scope: identify,incoming-webhook
          user_id: xxxxxxxxxxx
          team_name: xxxxxxxxxx
          team_id: xxxxxxxxxx
          incoming_webhook:
            channel: "#random"
            channel_id: xxxxxxxxxxx
            configuration_url: https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
            url: https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
      identity: 
      user_info: 
      user_profile: 
      team_info: 
      bot_info: 
  created_at: 2016-10-25 12:23:08.399538000 -07:00
  updated_at: 2016-10-25 15:35:51.916497000 -07:00

@samst-test
Copy link

Hey @ginjo – sorry to bring up such an old issue, but how do you handle those multiple passes which need different scopes? If I've got scope: 'identity.basic' set in devise.rb, how can I then send someone (it'll only happen once, like you mention above) through the OAuth flow to add my app, for example with the commands scope?

Thanks in advance!

@ginjo
Copy link

ginjo commented Jan 13, 2018

Hi @samst-test , no problem. I'll look into my code and see what I can find. I've been focusing on some other stuff the last few months, but I've been meaning to review my slack project and omniauth-slack fork and compare it with master. I'll post back here, once I've re-loaded my brain 😄

@ginjo
Copy link

ginjo commented Feb 21, 2018

Hey @samst-test - Unfortunately, I'm not familiar with Devise, as I use my own authentication management library based on Warden and Omniauth. But here's what I'm doing to handle the multiple-pass complexities of Slack OAUTH in my app.

I have two buttons: add-to-slack and sign-in-with-slack. The add-to-slack button uses what I call a 'deep' scope of identify,channels:read,chat:write:bot,users.profile:read. This scope will get pretty much all the data and permissions I need to perform the functions of my app. The deep scope will cause the Slack OAUTH process to poll the user for permissions on the various scopes requested.

The sign-in-with-slack button uses what I call a 'shallow' scope of identity.basic,identity.email,identity.team,identity.avatar. This scope retrieves just enough data to match a slack user with their local account record in my app and to manage the user's current session. This scope also returns a couple pieces of data that I didn't get with the deep scope. The shallow scope should only poll the user the first time it is run against that Slack account... At least that's how it worked when I wrote the app. I see now that Slack polls the user regardless of which scope I use, although the shallow scope results in a simpler polling.

Both scopes use the same omiauth-slack provider definition. The only difference is the :scope option I pass when redirecting to the onmiauth-slack URL.

In my local database, I keep one identity record for each slack-team-user. I update the record with whatever data comes back from the OAUTH response, regardless of which scope was used.

You can see this app-in-progress here. It's a simple translator that forwards Rackspace webhook notifications to a Slack channel.

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

3 participants