Initial Twitter strategy

This commit is contained in:
Sean Callan 2015-11-17 19:34:55 -08:00
parent 8330fa308b
commit 2cf6544544
12 changed files with 409 additions and 22 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
/deps
erl_crash.dump
*.ez
.DS_Store

14
CONTRIBUTING.md Normal file
View 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
View 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.

View File

@ -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
View File

@ -0,0 +1 @@
use Mix.Config

View 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

View 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
View File

@ -0,0 +1,2 @@
defmodule UeberauthTwitter do
end

52
mix.exs Normal file
View 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
View 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
View File

@ -0,0 +1 @@
ExUnit.start()

View File

@ -0,0 +1,4 @@
defmodule UeberauthTwitterTest do
use ExUnit.Case
doctest UeberauthTwitter
end