Initial Twitter strategy
This commit is contained in:
parent
8330fa308b
commit
2cf6544544
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
||||
/deps
|
||||
erl_crash.dump
|
||||
*.ez
|
||||
.DS_Store
|
||||
|
14
CONTRIBUTING.md
Normal file
14
CONTRIBUTING.md
Normal file
@ -0,0 +1,14 @@
|
||||
# Contributing to Ueberauth Twitter
|
||||
|
||||
## Pull Requests Welcome
|
||||
1. Fork ueberauth_twitter
|
||||
2. Create a topic branch
|
||||
3. Make logically-grouped commits with clear commit messages
|
||||
4. Push commits to your fork
|
||||
5. Open a pull request against ueberauth_twitter/master
|
||||
|
||||
## Issues
|
||||
|
||||
If you believe there to be a bug, please provide the maintainers with enough
|
||||
detail to reproduce or a link to an app exhibiting unexpected behavior. For
|
||||
help, please start with Stack Overflow.
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Sean Callan
|
||||
|
||||
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.
|
87
README.md
87
README.md
@ -2,35 +2,78 @@
|
||||
|
||||
> Twitter strategy for Überauth.
|
||||
|
||||
### Setup
|
||||
|
||||
Include the provider in your configuration for Überauth:
|
||||
|
||||
```elixir
|
||||
config :ueberauth, Ueberauth,
|
||||
providers: [
|
||||
twitter: [ { Ueberauth.Strategy.Twitter, [] } ]
|
||||
]
|
||||
```
|
||||
|
||||
Then configure your provider:
|
||||
|
||||
```elixir
|
||||
config :ueberauth, Ueberauth.Strategy.Twitter.OAuth,
|
||||
client_id: System.get_env("TWITTER_API_KEY"),
|
||||
client_secret: System.get_env("TWITTER_API_SECRET")
|
||||
```
|
||||
|
||||
For an example implementation see the [Überauth Example](https://github.com/doomspork/ueberauth_example) application.
|
||||
_Note_: Sessions are required for this strategy.
|
||||
|
||||
## Installation
|
||||
|
||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
|
||||
1. Setup your application at [Twitter Developers](https://dev.twitter.com/).
|
||||
|
||||
1. Add `:ueberauth_twitter` to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[{:ueberauth_twitter, "~> 0.1.0"}]
|
||||
[{:ueberauth_twitter, "~> 0.1"},
|
||||
{:oauth, github: "tim/erlang-oauth"}]
|
||||
end
|
||||
```
|
||||
|
||||
1. Add the strategy to your applications:
|
||||
|
||||
```elixir
|
||||
def application do
|
||||
[applications: [:ueberauth_twitter]]
|
||||
end
|
||||
```
|
||||
|
||||
1. Add Twitter to your Überauth configuration:
|
||||
|
||||
```elixir
|
||||
config :ueberauth, Ueberauth,
|
||||
providers: [
|
||||
twitter: [{Ueberauth.Strategy.Twitter, []}]
|
||||
]
|
||||
```
|
||||
|
||||
1. Update your provider configuration:
|
||||
|
||||
```elixir
|
||||
config :ueberauth, Ueberauth.Strategy.Twitter.OAuth,
|
||||
consumer_key: System.get_env("TWITTER_CONSUMER_KEY"),
|
||||
consumer_secret: System.get_env("TWITTER_CONSUMER_SECRET")
|
||||
```
|
||||
|
||||
1. Include the Überauth plug in your controller:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.AuthController do
|
||||
use MyApp.Web, :controller
|
||||
plug Ueberauth
|
||||
...
|
||||
end
|
||||
```
|
||||
|
||||
1. Create the request and callback routes if you haven't already:
|
||||
|
||||
```elixir
|
||||
scope "/auth", MyApp do
|
||||
pipe_through :browser
|
||||
|
||||
get "/:provider", AuthController, :request
|
||||
get "/:provider/callback", AuthController, :callback
|
||||
end
|
||||
```
|
||||
|
||||
1. You controller needs to implement callbacks to deal with `Ueberauth.Auth` and `Ueberauth.Failure` responses.
|
||||
|
||||
For an example implementation see the [Überauth Example](https://github.com/ueberauth/ueberauth_example) application.
|
||||
|
||||
## Calling
|
||||
|
||||
Depending on the configured url you can initial the request through:
|
||||
|
||||
/auth/twitter
|
||||
|
||||
## License
|
||||
|
||||
Please see [LICENSE](https://github.com/ueberauth/ueberauth_twitter/blob/master/LICENSE) for licensing details.
|
||||
|
||||
|
1
config/config.exs
Normal file
1
config/config.exs
Normal file
@ -0,0 +1 @@
|
||||
use Mix.Config
|
123
lib/ueberauth/strategy/twitter.ex
Normal file
123
lib/ueberauth/strategy/twitter.ex
Normal file
@ -0,0 +1,123 @@
|
||||
defmodule Ueberauth.Strategy.Twitter do
|
||||
@moduledoc """
|
||||
Twitter Strategy for Überauth.
|
||||
"""
|
||||
|
||||
use Ueberauth.Strategy, uid_field: :id_str
|
||||
|
||||
alias Ueberauth.Auth.Info
|
||||
alias Ueberauth.Auth.Credentials
|
||||
alias Ueberauth.Auth.Extra
|
||||
alias Ueberauth.Strategy.Twitter
|
||||
|
||||
@doc """
|
||||
Handles initial request for Twitter authentication.
|
||||
"""
|
||||
def handle_request!(conn) do
|
||||
token = Twitter.OAuth.request_token!([], [redirect_uri: callback_url(conn)])
|
||||
|
||||
conn
|
||||
|> put_session(:twitter_token, token)
|
||||
|> redirect!(Twitter.OAuth.authorize_url!(token))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles the callback from Twitter.
|
||||
"""
|
||||
def handle_callback!(%Plug.Conn{params: %{"oauth_verifier" => oauth_verifier}} = conn) do
|
||||
token = get_session(conn, :twitter_token)
|
||||
case Twitter.OAuth.access_token(token, oauth_verifier) do
|
||||
{:ok, access_token} -> fetch_user(conn, access_token)
|
||||
{:error, error} -> set_errors!(conn, [error(error.code, error.reason)])
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_callback!(conn) do
|
||||
set_errors!(conn, [error("missing_code", "No code received")])
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_cleanup!(conn) do
|
||||
conn
|
||||
|> put_private(:twitter_user, nil)
|
||||
|> put_session(:twitter_token, nil)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches the uid field from the response.
|
||||
"""
|
||||
def uid(conn) do
|
||||
uid_field =
|
||||
conn
|
||||
|> option(:uid_field)
|
||||
|> to_string
|
||||
|
||||
conn.private.twitter_user[uid_field]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Includes the credentials from the twitter response.
|
||||
"""
|
||||
def credentials(conn) do
|
||||
{token, _secret} = get_session(conn, :twitter_token
|
||||
|
||||
%Credentials{token: token}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches the fields to populate the info section of the `Ueberauth.Auth` struct.
|
||||
"""
|
||||
def info(conn) do
|
||||
user = conn.private.twitter_user
|
||||
|
||||
%Info{
|
||||
email: user["email"],
|
||||
image: user["profile_image_url"],
|
||||
name: user["name"],
|
||||
nickname: user["screen_name"],
|
||||
description: user["description"],
|
||||
urls: %{
|
||||
Twitter: "https://twitter.com/#{user["screen_name"]}",
|
||||
Website: user["url"]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stores the raw information (including the token) obtained from the twitter callback.
|
||||
"""
|
||||
def extra(conn) do
|
||||
{token, _secret} = get_session(conn, :twitter_token)
|
||||
|
||||
%Extra{
|
||||
raw_info: %{
|
||||
token: token,
|
||||
user: conn.private.twitter_user
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp fetch_user(conn, token) do
|
||||
params = [include_entities: false, skip_status: true, include_email: true]
|
||||
case Twitter.OAuth.get("/1.1/account/verify_credentials.json", params, token) do
|
||||
{:ok, {{_, 401, _}, _, _}} ->
|
||||
set_errors!(conn, [error("token", "unauthorized")])
|
||||
{:ok, {{_, status_code, _}, _, body}} when status_code in 200..399 ->
|
||||
body = body |> List.to_string |> Poison.decode!
|
||||
|
||||
conn
|
||||
|> put_private(:twitter_token, token)
|
||||
|> put_private(:twitter_user, body)
|
||||
{:ok, {_, _, body}} ->
|
||||
body = body |> List.to_string |> Poison.decode!
|
||||
|
||||
error = List.first(body["errors"])
|
||||
set_errors!(conn, [error("token", error["message"])])
|
||||
end
|
||||
end
|
||||
|
||||
defp option(conn, key) do
|
||||
Dict.get(options(conn), key, Dict.get(default_options, key))
|
||||
end
|
||||
end
|
110
lib/ueberauth/strategy/twitter/oauth.ex
Normal file
110
lib/ueberauth/strategy/twitter/oauth.ex
Normal file
@ -0,0 +1,110 @@
|
||||
defmodule Ueberauth.Strategy.Twitter.OAuth do
|
||||
@moduledoc """
|
||||
OAuth1 for Twitter.
|
||||
|
||||
Add `consumer_key` and `consumer_secret` to your configuration:
|
||||
|
||||
config :ueberauth, Ueberauth.Strategy.Twitter.OAuth,
|
||||
consumer_key: System.get_env("TWITTER_CONSUMER_KEY"),
|
||||
consumer_secret: System.get_env("TWITTER_CONSUMER_SECRET"),
|
||||
redirect_uri: System.get_env("TWITTER_REDIRECT_URI")
|
||||
"""
|
||||
|
||||
@defaults [access_token: "/oauth/access_token",
|
||||
authorize_url: "/oauth/authorize",
|
||||
request_token: "/oauth/request_token",
|
||||
site: "https://api.twitter.com"]
|
||||
|
||||
def access_token({token, token_secret}, verifier, opts \\ []) do
|
||||
opts
|
||||
|> client
|
||||
|> to_url(:access_token)
|
||||
|> String.to_char_list
|
||||
|> :oauth.get([oauth_verifier: verifier], consumer(client), token, token_secret)
|
||||
|> decode_access_response
|
||||
end
|
||||
|
||||
def access_token!(access_token, verifier, opts \\ []) do
|
||||
case access_token(access_token, verifier, opts) do
|
||||
{:ok, token} -> token
|
||||
{:error, error} -> raise error
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_url!({token, _token_secret}, opts \\ []) do
|
||||
opts
|
||||
|> client
|
||||
|> to_url(:authorize_url, %{"oauth_token" => List.to_string(token)})
|
||||
end
|
||||
|
||||
def client(opts \\ []) do
|
||||
config = Application.get_env(:ueberauth, __MODULE__)
|
||||
|
||||
@defaults
|
||||
|> Keyword.merge(config)
|
||||
|> Keyword.merge(opts)
|
||||
|> Enum.into(%{})
|
||||
end
|
||||
|
||||
def get(url, access_token), do: get(url, [], access_token)
|
||||
def get(url, params \\ [], {token, token_secret}) do
|
||||
client
|
||||
|> to_url(url)
|
||||
|> String.to_char_list
|
||||
|> :oauth.get(params, consumer(client), token, token_secret)
|
||||
end
|
||||
|
||||
def request_token(params \\ [], opts \\ []) do
|
||||
client = client(opts)
|
||||
params = Keyword.put_new(params, :oauth_callback, client.redirect_uri)
|
||||
|
||||
client
|
||||
|> to_url(:request_token)
|
||||
|> String.to_char_list
|
||||
|> :oauth.get(params, consumer(client))
|
||||
|> decode_request_response
|
||||
end
|
||||
|
||||
def request_token!(params \\ [], opts \\ []) do
|
||||
case request_token(params, opts) do
|
||||
{:ok, token} -> token
|
||||
{:error, error} -> raise error
|
||||
end
|
||||
end
|
||||
|
||||
defp consumer(client), do: {client.consumer_key, client.consumer_secret, :hmac_sha1}
|
||||
|
||||
defp decode_access_response({:ok, {{_, 200, _}, _, _} = resp}) do
|
||||
params = :oauth.params_decode(resp)
|
||||
token = :oauth.token(params)
|
||||
token_secret = :oauth.token_secret(params)
|
||||
|
||||
{:ok, {token, token_secret}}
|
||||
end
|
||||
defp decode_access_response(error), do: {:error, error}
|
||||
|
||||
defp decode_request_response({:ok, {{_, 200, _}, _, _} = resp}) do
|
||||
params = :oauth.params_decode(resp)
|
||||
token = :oauth.token(params)
|
||||
token_secret = :oauth.token_secret(params)
|
||||
|
||||
{:ok, {token, token_secret}}
|
||||
end
|
||||
defp decode_request_response(error), do: {:error, error}
|
||||
|
||||
defp endpoint("/" <> _path = endpoint, client), do: client.site <> endpoint
|
||||
defp endpoint(endpoint, _client), do: endpoint
|
||||
|
||||
defp to_url(client, endpoint, params \\ nil) do
|
||||
endpoint =
|
||||
client
|
||||
|> Map.get(endpoint, endpoint)
|
||||
|> endpoint(client)
|
||||
|
||||
unless params == nil do
|
||||
endpoint = endpoint <> "?" <> URI.encode_query(params)
|
||||
end
|
||||
|
||||
endpoint
|
||||
end
|
||||
end
|
2
lib/ueberauth_twitter.ex
Normal file
2
lib/ueberauth_twitter.ex
Normal file
@ -0,0 +1,2 @@
|
||||
defmodule UeberauthTwitter do
|
||||
end
|
52
mix.exs
Normal file
52
mix.exs
Normal file
@ -0,0 +1,52 @@
|
||||
defmodule UeberauthTwitter.Mixfile do
|
||||
use Mix.Project
|
||||
|
||||
@version "0.1.0"
|
||||
@url "https://github.com/doomspork/ueberauth_twitter"
|
||||
|
||||
def project do
|
||||
[app: :ueberauth_twitter,
|
||||
version: @version,
|
||||
name: "Ueberauth Twitter Strategy",
|
||||
package: package,
|
||||
elixir: "~> 1.1",
|
||||
build_embedded: Mix.env == :prod,
|
||||
start_permanent: Mix.env == :prod,
|
||||
source_url: @url,
|
||||
homepage_url: @url,
|
||||
description: description,
|
||||
deps: deps,
|
||||
docs: docs]
|
||||
end
|
||||
|
||||
def application do
|
||||
[applications: [:logger, :httpoison, :oauth, :ueberauth]]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[{:ueberauth, "~> 0.1"},
|
||||
{:oauth, github: "tim/erlang-oauth"},
|
||||
{:httpoison, "~> 0.7"},
|
||||
{:ex_doc, "~> 0.1", only: :dev},
|
||||
{:earmark, ">= 0.0.0", only: :dev}]
|
||||
end
|
||||
|
||||
defp docs do
|
||||
[extras: docs_extras, main: "extra-readme"]
|
||||
end
|
||||
|
||||
defp docs_extras do
|
||||
["README.md"]
|
||||
end
|
||||
|
||||
defp description do
|
||||
"An Uberauth strategy for Twitter authentication."
|
||||
end
|
||||
|
||||
defp package do
|
||||
[files: ["lib", "mix.exs", "README.md", "LICENSE"],
|
||||
maintainers: ["Sean Callan"],
|
||||
licenses: ["MIT"],
|
||||
links: %{"GitHub": @url}]
|
||||
end
|
||||
end
|
15
mix.lock
Normal file
15
mix.lock
Normal file
@ -0,0 +1,15 @@
|
||||
%{"certifi": {:hex, :certifi, "0.1.1"},
|
||||
"earmark": {:hex, :earmark, "0.1.19"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.10.0"},
|
||||
"hackney": {:hex, :hackney, "1.4.4"},
|
||||
"httpoison": {:hex, :httpoison, "0.8.0"},
|
||||
"idna": {:hex, :idna, "1.0.2"},
|
||||
"mimerl": {:hex, :mimerl, "1.0.0"},
|
||||
"mimetype_parser": {:hex, :mimetype_parser, "0.1.0"},
|
||||
"oauth": {:git, "https://github.com/tim/erlang-oauth.git", "cd31addc828179983564fd57738f1680a4a4d1ff", []},
|
||||
"oauth2": {:hex, :oauth2, "0.5.0"},
|
||||
"oauther": {:hex, :oauther, "1.0.2"},
|
||||
"plug": {:hex, :plug, "1.0.2"},
|
||||
"poison": {:hex, :poison, "1.5.0"},
|
||||
"ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"},
|
||||
"ueberauth": {:hex, :ueberauth, "0.1.0"}}
|
1
test/test_helper.exs
Normal file
1
test/test_helper.exs
Normal file
@ -0,0 +1 @@
|
||||
ExUnit.start()
|
4
test/ueber_twitter_test.exs
Normal file
4
test/ueber_twitter_test.exs
Normal file
@ -0,0 +1,4 @@
|
||||
defmodule UeberauthTwitterTest do
|
||||
use ExUnit.Case
|
||||
doctest UeberauthTwitter
|
||||
end
|
Loading…
Reference in New Issue
Block a user