Skip to content

Commit 373f1b8

Browse files
committed
Implementation based off of Ueberauth Twitter
1 parent 51d991b commit 373f1b8

File tree

7 files changed

+352
-31
lines changed

7 files changed

+352
-31
lines changed

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2015 Sean Callan
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+71-12
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,78 @@
1-
# UeberauthGoodreads
1+
# Überauth Goodreads
22

3-
**TODO: Add description**
3+
> Goodreads strategy for Überauth.
4+
5+
_Note_: Sessions are required for this strategy.
46

57
## Installation
68

7-
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
8-
by adding `ueberauth_goodreads` to your list of dependencies in `mix.exs`:
9+
1. Setup your application at [Gooreads Developers](https://www.goodreads.com/api/keys).
10+
11+
1. Add `:ueberauth_goodreads` to your list of dependencies in `mix.exs`:
12+
13+
```elixir
14+
def deps do
15+
[{:ueberauth_goodreads, "~> 0.1"},
16+
{:oauth, github: "tim/erlang-oauth"}]
17+
end
18+
```
19+
20+
1. Add the strategy to your applications:
21+
22+
```elixir
23+
def application do
24+
[applications: [:ueberauth_goodreads]]
25+
end
26+
```
27+
28+
1. Add Goodreads to your Überauth configuration:
29+
30+
```elixir
31+
config :ueberauth, Ueberauth,
32+
providers: [
33+
goodreads: {Ueberauth.Strategy.Goodreads, []}
34+
]
35+
```
36+
37+
1. Update your provider configuration:
38+
39+
```elixir
40+
config :ueberauth, Ueberauth.Strategy.Goodreads.OAuth,
41+
consumer_key: System.get_env("GOODREADS_CONSUMER_KEY"),
42+
consumer_secret: System.get_env("GOODREADS_CONSUMER_SECRET")
43+
```
44+
45+
1. Include the Überauth plug in your controller:
46+
47+
```elixir
48+
defmodule MyApp.AuthController do
49+
use MyApp.Web, :controller
50+
plug Ueberauth
51+
...
52+
end
53+
```
54+
55+
1. Create the request and callback routes if you haven't already:
56+
57+
```elixir
58+
scope "/auth", MyApp do
59+
pipe_through :browser
60+
61+
get "/:provider", AuthController, :request
62+
get "/:provider/callback", AuthController, :callback
63+
end
64+
```
65+
66+
1. You controller needs to implement callbacks to deal with `Ueberauth.Auth` and `Ueberauth.Failure` responses.
67+
68+
For an example implementation see the [Überauth Example](https://github.com/ueberauth/ueberauth_example) application.
69+
70+
## Calling
71+
72+
Depending on the configured url you can initial the request through:
973
10-
```elixir
11-
def deps do
12-
[{:ueberauth_goodreads, "~> 0.1.0"}]
13-
end
14-
```
74+
/auth/goodreads
1575
16-
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
17-
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
18-
be found at [https://hexdocs.pm/ueberauth_goodreads](https://hexdocs.pm/ueberauth_goodreads).
76+
## License
1977
78+
Please see [LICENSE](https://github.com/tielur/ueberauth_goodreads/blob/master/LICENSE) for licensing details.

lib/strategy/goodreads.ex

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
defmodule Ueberauth.Strategy.Goodreads do
2+
@moduledoc """
3+
Goodreads Strategy for Überauth.
4+
"""
5+
6+
use Ueberauth.Strategy, uid_field: :id_str
7+
8+
alias Ueberauth.Auth.Info
9+
alias Ueberauth.Auth.Credentials
10+
alias Ueberauth.Auth.Extra
11+
alias Ueberauth.Strategy.Goodreads
12+
import SweetXml
13+
14+
@doc """
15+
Handles initial request for Goodreads authentication.
16+
"""
17+
def handle_request!(conn) do
18+
token = Goodreads.OAuth.request_token!([], [redirect_uri: callback_url(conn)])
19+
conn
20+
|> put_session(:goodreads_token, token)
21+
|> redirect!(Goodreads.OAuth.authorize_url!(token))
22+
end
23+
24+
@doc """
25+
Handles the callback from Goodreads.
26+
"""
27+
def handle_callback!(%Plug.Conn{params: %{"authorize" => oauth_verifier}} = conn) do
28+
token = get_session(conn, :goodreads_token)
29+
case Goodreads.OAuth.access_token(token, oauth_verifier) do
30+
{:ok, access_token} -> fetch_user(conn, access_token)
31+
{:error, error} -> set_errors!(conn, [error(error.code, error.reason)])
32+
end
33+
end
34+
35+
@doc false
36+
def handle_callback!(conn) do
37+
set_errors!(conn, [error("missing_code", "No code received")])
38+
end
39+
40+
@doc false
41+
def handle_cleanup!(conn) do
42+
conn
43+
|> put_private(:goodreads_user, nil)
44+
|> put_session(:goodreads_token, nil)
45+
end
46+
47+
@doc """
48+
Fetches the uid field from the response.
49+
"""
50+
def uid(conn) do
51+
uid_field =
52+
conn
53+
|> option(:uid_field)
54+
|> to_string
55+
56+
conn.private.goodreads_user[uid_field]
57+
end
58+
59+
@doc """
60+
Includes the credentials from the Goodreads response.
61+
"""
62+
def credentials(conn) do
63+
{token, secret} = conn.private.goodreads_token
64+
65+
%Credentials{token: token, secret: secret}
66+
end
67+
68+
@doc """
69+
Fetches the fields to populate the info section of the `Ueberauth.Auth` struct.
70+
"""
71+
def info(conn) do
72+
user = conn.private.goodreads_user
73+
74+
%Info{
75+
name: user["name"],
76+
urls: %{
77+
Goodreads: user["link"]
78+
}
79+
}
80+
end
81+
82+
@doc """
83+
Stores the raw information (including the token) obtained from the Goodreads callback.
84+
"""
85+
def extra(conn) do
86+
{token, _secret} = get_session(conn, :goodreads_token)
87+
88+
%Extra{
89+
raw_info: %{
90+
token: token,
91+
user: conn.private.goodreads_user
92+
}
93+
}
94+
end
95+
96+
defp fetch_user(conn, token) do
97+
params = [include_entities: false, skip_status: true, include_email: true]
98+
case Goodreads.OAuth.get("/api/auth_user", params, token) do
99+
{:ok, {{_, 401, _}, _, _}} ->
100+
set_errors!(conn, [error("token", "unauthorized")])
101+
{:ok, {{_, status_code, _}, _, body}} when status_code in 200..399 ->
102+
body = %{
103+
"id" => body |> xpath(~x"//user/@id"),
104+
"name" => body |> xpath(~x"//user/name/text()"),
105+
"link" => body |> xpath(~x"//user/link/text()")
106+
}
107+
conn
108+
|> put_private(:goodreads_token, token)
109+
|> put_private(:goodreads_user, body)
110+
{:ok, {_, _, body}} ->
111+
set_errors!(conn, [error("user", "fetch user error")])
112+
end
113+
end
114+
115+
defp option(conn, key) do
116+
Dict.get(options(conn), key, Dict.get(default_options, key))
117+
end
118+
end

lib/strategy/goodreads/oauth.ex

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
defmodule Ueberauth.Strategy.Goodreads.OAuth do
2+
@moduledoc """
3+
OAuth1 for Goodreads.
4+
Add `consumer_key` and `consumer_secret` to your configuration:
5+
config :ueberauth, Ueberauth.Strategy.Goodreads.OAuth,
6+
consumer_key: System.get_env("GOODREADS_CONSUMER_KEY"),
7+
consumer_secret: System.get_env("GOODREADS_CONSUMER_SECRET"),
8+
redirect_uri: System.get_env("GOODREADS_REDIRECT_URI")
9+
"""
10+
11+
@defaults [access_token: "/oauth/access_token",
12+
authorize_url: "/oauth/authorize",
13+
request_token: "/oauth/request_token",
14+
site: "http://www.goodreads.com"]
15+
16+
def access_token({token, token_secret}, verifier, opts \\ []) do
17+
opts
18+
|> client
19+
|> to_url(:access_token)
20+
|> String.to_char_list
21+
|> :oauth.get([oauth_verifier: verifier], consumer(client), token, token_secret)
22+
|> decode_access_response
23+
end
24+
25+
def access_token!(access_token, verifier, opts \\ []) do
26+
case access_token(access_token, verifier, opts) do
27+
{:ok, token} -> token
28+
{:error, error} -> raise error
29+
end
30+
end
31+
32+
def authorize_url!({token, _token_secret}, opts \\ []) do
33+
opts
34+
|> client
35+
|> to_url(:authorize_url, %{"oauth_token" => List.to_string(token)})
36+
end
37+
38+
def client(opts \\ []) do
39+
config = Application.get_env(:ueberauth, __MODULE__)
40+
41+
@defaults
42+
|> Keyword.merge(config)
43+
|> Keyword.merge(opts)
44+
|> Enum.into(%{})
45+
end
46+
47+
def get(url, access_token), do: get(url, [], access_token)
48+
def get(url, params, {token, token_secret}) do
49+
client
50+
|> to_url(url)
51+
|> String.to_char_list
52+
|> :oauth.get(params, consumer(client), token, token_secret)
53+
end
54+
55+
def request_token(params \\ [], opts \\ []) do
56+
client = client(opts)
57+
params = Keyword.put_new(params, :oauth_callback, client.redirect_uri)
58+
59+
client
60+
|> to_url(:request_token)
61+
|> String.to_char_list
62+
|> :oauth.get(params, consumer(client))
63+
|> decode_request_response
64+
end
65+
66+
def request_token!(params \\ [], opts \\ []) do
67+
case request_token(params, opts) do
68+
{:ok, token} -> token
69+
{:error, error} -> raise error
70+
end
71+
end
72+
73+
defp consumer(client), do: {client.consumer_key, client.consumer_secret, :hmac_sha1}
74+
75+
defp decode_access_response({:ok, {{_, 200, _}, _, _} = resp}) do
76+
params = :oauth.params_decode(resp)
77+
token = :oauth.token(params)
78+
token_secret = :oauth.token_secret(params)
79+
80+
{:ok, {token, token_secret}}
81+
end
82+
defp decode_access_response({:ok, {{_, status_code, status_description}, _, _}}), do: {:error, "#{status_code} - #{status_description}"}
83+
defp decode_access_response({:error, {error, [_, {:inet, [:inet], error_reason}]}}), do: {:error, "#{error}: #{error_reason}"}
84+
defp decode_access_response(error), do: {:error, error}
85+
86+
defp decode_request_response({:ok, {{_, 200, _}, _, _} = resp}) do
87+
params = :oauth.params_decode(resp)
88+
token = :oauth.token(params)
89+
token_secret = :oauth.token_secret(params)
90+
91+
{:ok, {token, token_secret}}
92+
end
93+
defp decode_request_response({:ok, {{_, status_code, status_description}, _, _}}), do: {:error, "#{status_code} - #{status_description}"}
94+
defp decode_request_response({:error, {error, [_, {:inet, [:inet], error_reason}]}}), do: {:error, "#{error}: #{error_reason}"}
95+
defp decode_request_response(error), do: {:error, error}
96+
97+
defp endpoint("/" <> _path = endpoint, client), do: client.site <> endpoint
98+
defp endpoint(endpoint, _client), do: endpoint
99+
100+
defp to_url(client, endpoint, params \\ nil) do
101+
endpoint =
102+
client
103+
|> Map.get(endpoint, endpoint)
104+
|> endpoint(client)
105+
106+
endpoint =
107+
if params do
108+
endpoint <> "?" <> URI.encode_query(params)
109+
else
110+
endpoint
111+
end
112+
113+
endpoint
114+
end
115+
end

lib/ueberauth_goodreads.ex

+2-17
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,3 @@
11
defmodule UeberauthGoodreads do
2-
@moduledoc """
3-
Documentation for UeberauthGoodreads.
4-
"""
5-
6-
@doc """
7-
Hello world.
8-
9-
## Examples
10-
11-
iex> UeberauthGoodreads.hello
12-
:world
13-
14-
"""
15-
def hello do
16-
:world
17-
end
18-
end
2+
@moduledoc false
3+
end

0 commit comments

Comments
 (0)