Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel Neighman committed Nov 16, 2015
0 parents commit 81e6a3f
Show file tree
Hide file tree
Showing 10 changed files with 475 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/_build
/cover
/deps
erl_crash.dump
*.ez
/doc
107 changes: 107 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# ÜeberauthSlack

Proivdes an Üeberauth strategy to use Slack as the authentication mechanism.

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:

1. Add ueberauth\_slack to your list of dependencies in `mix.exs`:

````elixir
def deps do
[{:ueberauth_slack, "~> 0.1.0"}]
end
````

2. Ensure oauth2 is started before your application:

```elixir
def application do
[applications: [:oauth2]]
end
````

3. Head over to [slack and create an application](https://api.slack.com/applications). You can use
http://localhost:4000/auth/slack/callback as the url for dev.

4. Add slack to your configuration (Phoenix)

````elixir
# To your ueberauth providers list
config :ueberauth, Ueberauth,
providers: [
slack: { Ueberauth.Strategy.Slack, [] }
]

# To provide access to the slack secrets

config :ueberauth, Ueberauth.Strategy.Slack.OAuth,
client_id: System.get_env("SLACK_CLIENT_ID"),
client_secret: System.get_env("SLACK_CLIENT_SECRET")
````

5. If you haven't already, create a pipeline for your Üeberauth
````elixir
pipeline :ueberauth do
Ueberauth.plug "/auth"
end
scope "/auth" do
pipe_through [:browser, :ueberauth]
# it does not matter which contorller for the request phase
# We just need to trigger the pipeline
get "/slack", PagesController, :index
get "/:provider/callback", AuthController, :callback
end
````
6. Implement your callback action in your controller to deal with an `Ueberauth.Auth` or `Ueberauth.Failure` callback
## Calling
To run through slack, depending on the url you setup with `Ueberauth.plug/1` you
can hit the url for the request phase.
/auth/slack
Or with options
/auth/slack?scope=users:read
By default the scope requested will be "users:read". This can be configured
either explicitly when you call the request path by providing a scope in the
query string, or by setting a default in your configuration.
````elixir
config :ueberauth, Ueberauth,
providers: [
slack: { Ueberauth.Strategy.Slack, [ default_scope: "users:read,users:write" ]
]
````
# License
The MIT License (MIT)
Copyright (c) 2015 Daniel Neighman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
30 changes: 30 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.

# You can configure for your application as:
#
# config :ueberauth_slack, key: :value
#
# And access this configuration in your application as:
#
# Application.get_env(:ueberauth_slack, :key)
#
# Or configure a 3rd-party app:
#
# config :logger, level: :info
#

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env}.exs"
210 changes: 210 additions & 0 deletions lib/ueberauth/strategy/slack.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
defmodule Ueberauth.Strategy.Slack do
@moduledoc """
Implements an ÜeberauthSlack strategy for authentication with slack.com.
When configuring the strategy in the Üeberauth providers, you can specify some defaults.
* `uid_field` - The field to use as the UID field. This can be any populated field in the info struct. Default `:email`
* `default_scope` - The scope to request by default from slack (permissions). Default "users:read"
* `oauth2_module` - The OAuth2 module to use. Default Ueberauth.Strategy.Slack.OAuth
````elixir
config :ueberauth, Ueberauth,
providers: [
slack: { Ueberauth.Strategy.Slack, [uid_field: :nickname, default_scope: "users:read,users:write"] }
]
"""
use Ueberauth.Strategy, uid_field: :email,
default_scope: "users:read",
oauth2_module: Ueberauth.Strategy.Slack.OAuth

alias Ueberauth.Auth.Info
alias Ueberauth.Auth.Credentials
alias Ueberauth.Auth.Extra

# When handling the request just redirect to Slack
@doc false
def handle_request!(conn) do
scopes = conn.params["scope"] || option(conn, :default_scope)
opts = [ scope: scopes ]
if conn.params["state"], do: opts = Keyword.put(opts, :state, conn.params["state"])
callback_url = callback_url(conn)

if String.ends_with?(callback_url, "?"), do: callback_url = String.slice(callback_url, 0..-2)

opts = Keyword.put(opts, :redirect_uri, callback_url)
module = option(conn, :oauth2_module)

redirect!(conn, apply(module, :authorize_url!, [opts]))
end

# When handling the callback, if there was no errors we need to
# make two calls. The first, to fetch the slack auth is so that we can get hold of
# the user id so we can make a query to fetch the user info.
# So that it is available later to build the auth struct, we put it in the private section of the conn.
@doc false
def handle_callback!(%Plug.Conn{ params: %{ "code" => code } } = conn) do
module = option(conn, :oauth2_module)
token = apply(module, :get_token!, [[code: code]])

if token.access_token == nil do
set_errors!(conn, [error(token.other_params["error"], token.other_params["error_description"])])
else
conn
|> store_token(token)
|> fetch_auth(token)
|> fetch_user(token)
end
end

# If we don't match code, then we have an issue
@doc false
def handle_callback!(conn) do
set_errors!(conn, [error("missing_code", "No code received")])
end

# We store the token for use later when fetching the slack auth and user and constructing the auth struct.
@doc false
defp store_token(conn, token) do
put_private(conn, :slack_token, token)
end

# Remove the temporary storage in the conn for our data. Run after the auth struct has been built.
@doc false
def handle_cleanup!(conn) do
conn
|> put_private(:slack_auth, nil)
|> put_private(:slack_user, nil)
|> put_private(:slack_token, nil)
end

# The structure of the requests is such that it is difficult to provide cusomization for the uid field.
# instead, we allow selecting any field from the info struct
@doc false
def uid(conn) do
Map.get(info(conn), option(conn, :uid_field))
end

@doc false
def credentials(conn) do
token = conn.private.slack_token
auth = conn.private.slack_auth
user = conn.private.slack_user

scopes = (token.other_params["scope"] || "")
|> String.split(",")

%Credentials{
token: token.access_token,
refresh_token: token.refresh_token,
expires_at: token.expires_at,
token_type: token.token_type,
expires: !!token.expires_at,
scopes: scopes,
other: %{
user: auth["user"],
user_id: auth["user_id"],
team: auth["team"],
team_id: auth["team_id"],
team_url: auth["url"],
has_2fa: user["has_2fa"],
is_admin: user["is_admin"],
is_owner: user["is_owner"],
is_primary_owner: user["is_primary_owner"],
is_restricted: user["is_restricted"],
is_ultra_restricted: user["is_ultra_restricted"],
}
}
end

@doc false
def info(conn) do
user = conn.private.slack_user
auth = conn.private.slack_auth
image_urls = user["profile"]
|> Map.keys
|> Enum.filter(&(&1 =~ ~r/^image_/))
|> Enum.map(&({ &1, user["profile"][&1] }))
|> Enum.into(%{})

%Info{
name: name_from_user(user),
nickname: user["name"],
email: user["profile"]["email"],
image: user["profile"]["image_48"],
urls: Map.merge(
image_urls,
%{
team_url: auth["url"],
}
)
}
end

@doc false
def extra(conn) do
%Extra {
raw_info: %{
auth: conn.private.slack_auth,
token: conn.private.slack_token,
user: conn.private.slack_user
}
}
end

# Before we can fetch the user, we first need to fetch the auth to find out what the user id is.
defp fetch_auth(conn, token) do
case OAuth2.AccessToken.post(token, "/auth.test", token: token.access_token) do
{ :ok, %OAuth2.Response{status_code: 401, body: _body}} ->
set_errors!(conn, [error("token", "unauthorized")])
{ :ok, %OAuth2.Response{status_code: status_code, body: auth} } when status_code in 200..399 ->
if auth["ok"] do
put_private(conn, :slack_auth, auth)
else
set_errors!(conn, [error(auth["error"], auth["error"])])
end
{ :error, %OAuth2.Error{reason: reason} } ->
set_errors!(conn, [error("OAuth2", reason)])
end
end

# If the call to fetch the auth fails, we're going to have failures already in place.
# If this happens don't try and fetch the user and just let it fail.
defp fetch_user(%Plug.Conn{ assigns: %{ ueberauth_failure: _fails }} = conn, _), do: conn

# Given the auth and token we can now fetch the user.
defp fetch_user(conn, token) do
auth = conn.private.slack_auth

case OAuth2.AccessToken.post(token, "/users.info", token: token.access_token, user: auth["user_id"]) do
{ :ok, %OAuth2.Response{status_code: 401, body: _body}} ->
set_errors!(conn, [error("token", "unauthorized")])
{ :ok, %OAuth2.Response{status_code: status_code, body: user} } when status_code in 200..399 ->
if user["ok"] do
put_private(conn, :slack_user, user["user"])
else
set_errors!(conn, [error(user["error"], user["error"])])
end
{ :error, %OAuth2.Error{reason: reason} } ->
set_errors!(conn, [error("OAuth2", reason)])
end
end

# Fetch the name to use. We try to start with the most specific name avaialble and
# fallback to the least.
defp name_from_user(user) do
[
user["profile"]["real_name_normalized"],
user["profile"]["real_name"],
user["real_name"],
user["name"],
]
|> Enum.reject(&(&1 == "" || &1 == nil))
|> List.first
end

defp option(conn, key) do
Dict.get(options(conn), key, Dict.get(default_options, key))
end
end
41 changes: 41 additions & 0 deletions lib/ueberauth/strategy/slack/oauth.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule Ueberauth.Strategy.Slack.OAuth do
use OAuth2.Strategy

@defaults [
strategy: __MODULE__,
site: "https://slack.com/api",
authorize_url: "https://slack.com/oauth/authorize",
token_url: "https://slack.com/api/oauth.access"
]

def client(opts \\ []) do
opts = Keyword.merge(@defaults, Application.get_env(:ueberauth, Ueberauth.Strategy.Slack.OAuth))
|> Keyword.merge(opts)

OAuth2.Client.new(opts)
end

def authorize_url!(params \\ [], opts \\ []) do
client(opts)
|> OAuth2.Client.authorize_url!(params)
end

def get_token!(params \\ [], options \\ %{}) do
headers = Dict.get(options, :headers, [])
options = Dict.get(options, :options, [])
client_options = Dict.get(options, :client_options, [])
OAuth2.Client.get_token!(client(client_options), params, headers, options)
end

# Strategy Callbacks

def authorize_url(client, params) do
OAuth2.Strategy.AuthCode.authorize_url(client, params)
end

def get_token(client, params, headers) do
client
|> put_header("Accept", "application/json")
|> OAuth2.Strategy.AuthCode.get_token(params, headers)
end
end
Loading

0 comments on commit 81e6a3f

Please sign in to comment.