diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..0a70dc0 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,5 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + line_length: 120 +] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..09f3be6 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# Order is important. The last matching pattern takes the most precedence. +# Default owners for everything in the repo. +* @ueberauth/developers diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5d9d95c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: Continuous Integration + +on: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: + - 'master' +jobs: + Test: + runs-on: ubuntu-20.04 + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: '1.11' + otp-version: '22.3' + + - name: Install Dependencies + run: | + mix local.rebar --force + mix local.hex --force + mix deps.get + - name: Run Tests + run: mix test + + Linting: + runs-on: ubuntu-20.04 + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: '1.11' + otp-version: '22.3' + + - name: Install Dependencies + run: | + mix local.rebar --force + mix local.hex --force + mix deps.get + - name: Run Formatter + run: mix format --check-formatted + + - name: Run Credo + run: mix credo diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..198cf3e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: Hexpm Release + +on: + release: + types: [published] + +jobs: + publish: + name: Publish + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: '1.11' + otp-version: '22.3' + - name: Restore dependencies cache + uses: actions/cache@v3 + with: + path: deps + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix- + - name: Install dependencies + run: | + mix local.rebar --force + mix local.hex --force + mix deps.get + - name: Run Hex Publish + run: mix hex.publish --yes + env: + HEX_API_KEY: ${{ secrets.HEX_API_KEY }} diff --git a/.gitignore b/.gitignore index e4840bc..f28fecd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,58 @@ + +# Created by https://www.gitignore.io/api/elixir,osx,vim + +### Elixir ### /_build +/cover /deps +/tmp +/.fetch erl_crash.dump +ueberauth_google-*.tar *.ez -.idea \ No newline at end of file +*.beam + +### Elixir Patch ### +/doc +### OSX ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Vim ### +# swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags + +# End of https://www.gitignore.io/api/elixir,osx,vim diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..523cdf0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: elixir + +cache: + directories: + - ~/.hex + - ~/.mix + - deps + +elixir: + - '1.8' + - '1.9' + +script: + - mix test diff --git a/CHANGELOG.md b/CHANGELOG.md index 290cbb1..16676a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,68 @@ -# v 0.6.0 +# Changelog -* Add support for access_type per request using url parameter +## (Unreleased) -# v 0.5.0 +## v0.12.0 -* Add support for new params: access_type, approval_prompt, state. -* Fix Elixir warnings +* Add support to hl param in handle_request! [102](https://github.com/ueberauth/ueberauth_google/pull/102) -# v 0.4.0 +## v0.11.0 -* Target Elixir 1.3 and greater -* Fix OAuth bug with 0.6.0 pin +* Allow using a function to generate the client secret [101](https://github.com/ueberauth/ueberauth_google/pull/101) -# v 0.3.0 +## v0.10.3 -* Use OpenID endpoint for profile information -* Update authorize and token URLs +* Handle `%OAuth2.Response{status_code: 503}` with no `error_description` in `get_access_token` [99](https://github.com/ueberauth/ueberauth_google/pull/99) -# v 0.2.0 +## v0.10.2 -* Release 0.2.0 to follow Ueberauth +* Prefer Local Over Global Configuration [95](https://github.com/ueberauth/ueberauth_google/pull/95) + +## v0.10.1 + +* Misc doc changes [81](https://github.com/ueberauth/ueberauth_google/pull/81) +* Upgrade Ueberauth and Refactor CSRF State Logic [82](https://github.com/ueberauth/ueberauth_google/pull/82) + +## v0.10.0 - 2020-10-20 + +### Enhancement + +* Updated docs [#69](https://github.com/ueberauth/ueberauth_google/pull/69) [#70](https://github.com/ueberauth/ueberauth_google/pull/70) +* Support for birthday [#73](https://github.com/ueberauth/ueberauth_google/pull/73) +* Allow for userinfo endpoint to be configured [#75](https://github.com/ueberauth/ueberauth_google/pull/75) +* Updated plug and ueberauth packages [#76](https://github.com/ueberauth/ueberauth_google/pull/76) + +Thanks goes to all the contributes + +## v0.9.0 - 2019-08-21 + +### Enhancement + +* Add support for optional login_hint param [#61](https://github.com/ueberauth/ueberauth_google/pull/61) +* Use `json_library` method from Ueberauth config [#58](https://github.com/ueberauth/ueberauth_google/pull/58) +* Allows specifying `{m, f, a}` tuples for things such as Client ID + and Client Secret [#60](https://github.com/ueberauth/ueberauth_google/pull/60) +* Allows the newest oauth2 package versions with potential security fixes [#68](https://github.com/ueberauth/ueberauth_google/pull/68) + +## v0.6.0 - 2017-07-18 + +* Add support for `access_type` per request using `url` parameter. + +## v0.5.0 - 2016-12-27 + +* Add support for new params: `access_type`, `approval_prompt`, `state`. +* Fix Elixir warnings. + +## v0.4.0 - 2016-09-21 + +* Target Elixir 1.3 and greater. +* Fix OAuth bug with 0.6.0 pin. + +## v0.3.0 - 2016-08-15 + +* Use OpenID endpoint for profile information. +* Update authorize and token URLs. + +## v0.2.0 - 2016-12-10 + +* Release 0.2.0 to follow Ueberauth. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 98fada1..c748f44 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,7 @@ -# Contributing to Ueberauth Google +# Contributing ## Pull Requests Welcome + 1. Fork ueberauth_google 2. Create a topic branch 3. Make logically-grouped commits with clear commit messages diff --git a/LICENSE b/LICENSE index 473a36e..4f1532b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Sean +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 @@ -19,4 +19,3 @@ 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. - diff --git a/README.md b/README.md index 1f0aece..1a88519 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,31 @@ # Überauth Google +[![Continuous Integration](https://github.com/ueberauth/ueberauth_google/actions/workflows/ci.yml/badge.svg)](https://github.com/ueberauth/ueberauth_google/actions/workflows/ci.yml) +[![Build Status](https://travis-ci.org/ueberauth/ueberauth_google.svg?branch=master)](https://travis-ci.org/ueberauth/ueberauth_google) +[![Module Version](https://img.shields.io/hexpm/v/ueberauth_google.svg)](https://hex.pm/packages/ueberauth_google) +[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ueberauth_google/) +[![Total Download](https://img.shields.io/hexpm/dt/ueberauth_google.svg)](https://hex.pm/packages/ueberauth_google) +[![License](https://img.shields.io/hexpm/l/ueberauth_google.svg)](https://github.com/ueberauth/ueberauth_google/blob/master/LICENSE) +[![Last Updated](https://img.shields.io/github/last-commit/ueberauth/ueberauth_google.svg)](https://github.com/ueberauth/ueberauth_google/commits/master) + + > Google OAuth2 strategy for Überauth. ## Installation -1. Setup your application at [Google Developer Console](https://console.developers.google.com/home). +1. Setup your application at [Google Developer Console](https://console.developers.google.com/home). -1. Add `:ueberauth_google` to your list of dependencies in `mix.exs`: +2. Add `:ueberauth_google` to your list of dependencies in `mix.exs`: ```elixir def deps do - [{:ueberauth_google, "~> 0.5"}] - end - ``` - -1. Add the strategy to your applications: - - ```elixir - def application do - [applications: [:ueberauth_google]] + [ + {:ueberauth_google, "~> 0.10"} + ] end ``` -1. Add Google to your Überauth configuration: +3. Add Google to your Überauth configuration: ```elixir config :ueberauth, Ueberauth, @@ -31,7 +34,10 @@ ] ``` -1. Update your provider configuration: +4. Update your provider configuration: + + Use that if you want to read client ID/secret from the environment + variables in the compile time: ```elixir config :ueberauth, Ueberauth.Strategy.Google.OAuth, @@ -39,7 +45,16 @@ client_secret: System.get_env("GOOGLE_CLIENT_SECRET") ``` -1. Include the Überauth plug in your controller: + Use that if you want to read client ID/secret from the environment + variables in the run time: + + ```elixir + config :ueberauth, Ueberauth.Strategy.Google.OAuth, + client_id: {System, :get_env, ["GOOGLE_CLIENT_ID"]}, + client_secret: {System, :get_env, ["GOOGLE_CLIENT_SECRET"]} + ``` + +5. Include the Überauth plug in your controller: ```elixir defmodule MyApp.AuthController do @@ -49,7 +64,7 @@ end ``` -1. Create the request and callback routes if you haven't already: +6. Create the request and callback routes if you haven't already: ```elixir scope "/auth", MyApp do @@ -60,13 +75,13 @@ end ``` -1. Your controller needs to implement callbacks to deal with `Ueberauth.Auth` and `Ueberauth.Failure` responses. +7. Your 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: +Depending on the configured url you can initiate the request through: /auth/google @@ -79,21 +94,44 @@ By default the requested scope is "email". Scope can be configured either explic ```elixir config :ueberauth, Ueberauth, providers: [ - google: {Ueberauth.Strategy.Google, [default_scope: "emails profile plus.me"]} + google: {Ueberauth.Strategy.Google, [default_scope: "email profile plus.me"]} ] ``` -You can also pass options such as the `hd` parameter to limit sign-in to a particular Google Apps hosted domain, or `approval_prompt` and `access_type` options to request refresh_tokens and offline access. +You can also pass options such as the `hd` parameter to suggest a particular Google Apps hosted domain (caution, can still be overridden by the user), `prompt` and `access_type` options to request refresh_tokens and offline access (both have to be present), or `include_granted_scopes` parameter to allow [incremental authorization](https://developers.google.com/identity/protocols/oauth2/web-server#incrementalAuth). ```elixir config :ueberauth, Ueberauth, providers: [ - google: {Ueberauth.Strategy.Google, [hd: "example.com", approval_prompt: "force", access_type: "offline"]} + google: {Ueberauth.Strategy.Google, [hd: "example.com", prompt: "select_account", access_type: "offline", include_granted_scopes: true]} + ] +``` + +In some cases, it may be necessary to update the user info endpoint, such as when deploying to countries that block access to the default endpoint. + +```elixir +config :ueberauth, Ueberauth, + providers: [ + google: {Ueberauth.Strategy.Google, [userinfo_endpoint: "https://www.googleapis.cn/oauth2/v3/userinfo"]} + ] +``` + +This may also be set via runtime configuration by passing a 2 or 3 argument tuple. To use this feature, the first argument must be the atom `:system`, and the second argument must represent the environment variable containing the endpoint url. +A third argument may be passed representing a default value if the environment variable is not found, otherwise the library default will be used. + +```elixir +config :ueberauth, Ueberauth, + providers: [ + google: {Ueberauth.Strategy.Google, [ + userinfo_endpoint: {:system, "GOOGLE_USERINFO_ENDPOINT", "https://www.googleapis.cn/oauth2/v3/userinfo"} + ]} ] ``` To guard against client-side request modification, it's important to still check the domain in `info.urls[:website]` within the `Ueberauth.Auth` struct if you want to limit sign-in to a specific domain. -## License +## Copyright and License + +Copyright (c) 2015 Sean Callan -Please see [LICENSE](https://github.com/ueberauth/ueberauth_google/blob/master/LICENSE) for licensing details. +Released under the MIT License, which can be found in the repository in [LICENSE](https://github.com/ueberauth/ueberauth_google/blob/master/LICENSE). diff --git a/config/config.exs b/config/config.exs index d2d855e..fc261c2 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1 +1,11 @@ -use Mix.Config +import Config + +config :ueberauth, Ueberauth, + providers: [ + google: {Ueberauth.Strategy.Google, []} + ] + +config :ueberauth, Ueberauth.Strategy.Google.OAuth, + client_id: "client_id", + client_secret: "client_secret", + token_url: "token_url" diff --git a/lib/ueberauth/strategy/google.ex b/lib/ueberauth/strategy/google.ex index 5a42d60..b83b10d 100644 --- a/lib/ueberauth/strategy/google.ex +++ b/lib/ueberauth/strategy/google.ex @@ -3,7 +3,11 @@ defmodule Ueberauth.Strategy.Google do Google Strategy for Überauth. """ - use Ueberauth.Strategy, uid_field: :sub, default_scope: "email", hd: nil + use Ueberauth.Strategy, + uid_field: :sub, + default_scope: "email", + hd: nil, + userinfo_endpoint: "https://www.googleapis.com/oauth2/v3/userinfo" alias Ueberauth.Auth.Info alias Ueberauth.Auth.Credentials @@ -15,30 +19,49 @@ defmodule Ueberauth.Strategy.Google do def handle_request!(conn) do scopes = conn.params["scope"] || option(conn, :default_scope) - opts = + params = [scope: scopes] |> with_optional(:hd, conn) - |> with_optional(:approval_prompt, conn) + |> with_optional(:prompt, conn) |> with_optional(:access_type, conn) + |> with_optional(:login_hint, conn) + |> with_optional(:include_granted_scopes, conn) |> with_param(:access_type, conn) |> with_param(:prompt, conn) - |> with_param(:state, conn) - |> Keyword.put(:redirect_uri, callback_url(conn)) + |> with_param(:login_hint, conn) + |> with_param(:hl, conn) + |> with_state_param(conn) - redirect!(conn, Ueberauth.Strategy.Google.OAuth.authorize_url!(opts)) + opts = oauth_client_options_from_conn(conn) + redirect!(conn, Ueberauth.Strategy.Google.OAuth.authorize_url!(params, opts)) end @doc """ Handles the callback from Google. """ def handle_callback!(%Plug.Conn{params: %{"code" => code}} = conn) do - opts = [redirect_uri: callback_url(conn)] - token = Ueberauth.Strategy.Google.OAuth.get_token!([code: code], opts) + params = [code: code] + opts = oauth_client_options_from_conn(conn) - if token.access_token == nil do - set_errors!(conn, [error(token.other_params["error"], token.other_params["error_description"])]) - else - fetch_user(conn, token) + case Ueberauth.Strategy.Google.OAuth.get_access_token(params, opts) do + {:ok, token} -> + fetch_user(conn, token) + + {:error, {error_code, error_description}} -> + set_errors!(conn, [error(error_code, error_description)]) + end + end + + @doc """ + Handles the callback from app. + """ + def handle_callback!(%Plug.Conn{params: %{"id_token" => id_token}} = conn) do + client = Ueberauth.Strategy.Google.OAuth.client + case verify_token(conn, client, id_token) do + {:ok, user} -> + put_user(conn, user) + {:error, reason} -> + set_errors!(conn, [error("token", reason)]) end end @@ -83,9 +106,9 @@ defmodule Ueberauth.Strategy.Google do Includes the credentials from the google response. """ def credentials(conn) do - token = conn.private.google_token - scope_string = (token.other_params["scope"] || "") - scopes = String.split(scope_string, ",") + token = conn.private.google_token + scope_string = token.other_params["scope"] || "" + scopes = String.split(scope_string, ",") %Credentials{ expires: !!token.expires_at, @@ -109,6 +132,7 @@ defmodule Ueberauth.Strategy.Google do image: user["picture"], last_name: user["family_name"], name: user["name"], + birthday: user["birthday"], urls: %{ profile: user["profile"], website: user["hd"] @@ -128,24 +152,42 @@ defmodule Ueberauth.Strategy.Google do } end - defp fetch_user(conn, token) do conn = put_private(conn, :google_token, token) # userinfo_endpoint from https://accounts.google.com/.well-known/openid-configuration - path = "https://www.googleapis.com/oauth2/v3/userinfo" - resp = Ueberauth.Strategy.Google.OAuth.get(token, path) + # the userinfo_endpoint may be overridden in options when necessary. + resp = Ueberauth.Strategy.Google.OAuth.get(token, get_userinfo_endpoint(conn)) case resp 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 -> + + {:ok, %OAuth2.Response{status_code: status_code, body: user}} + when status_code in 200..399 -> put_private(conn, :google_user, user) + + {:error, %OAuth2.Response{status_code: status_code}} -> + set_errors!(conn, [error("OAuth2", status_code)]) + {:error, %OAuth2.Error{reason: reason}} -> set_errors!(conn, [error("OAuth2", reason)]) end end + defp get_userinfo_endpoint(conn) do + case option(conn, :userinfo_endpoint) do + {:system, varname, default} -> + System.get_env(varname) || default + + {:system, varname} -> + System.get_env(varname) || Keyword.get(default_options(), :userinfo_endpoint) + + other -> + other + end + end + defp put_user(conn, user) do token = %OAuth2.AccessToken{} conn = put_private(conn, :google_token, token) @@ -160,6 +202,17 @@ defmodule Ueberauth.Strategy.Google do if option(conn, key), do: Keyword.put(opts, key, option(conn, key)), else: opts end + defp oauth_client_options_from_conn(conn) do + base_options = [redirect_uri: callback_url(conn)] + request_options = conn.private[:ueberauth_request_options].options + + case {request_options[:client_id], request_options[:client_secret]} do + {nil, _} -> base_options + {_, nil} -> base_options + {id, secret} -> [client_id: id, client_secret: secret] ++ base_options + end + end + defp option(conn, key) do Keyword.get(options(conn), key, Keyword.get(default_options(), key)) end diff --git a/lib/ueberauth/strategy/google/oauth.ex b/lib/ueberauth/strategy/google/oauth.ex index e77102e..1f299bd 100644 --- a/lib/ueberauth/strategy/google/oauth.ex +++ b/lib/ueberauth/strategy/google/oauth.ex @@ -4,18 +4,19 @@ defmodule Ueberauth.Strategy.Google.OAuth do Add `client_id` and `client_secret` to your configuration: - config :ueberauth, Ueberauth.Strategy.Google.OAuth, - client_id: System.get_env("GOOGLE_APP_ID"), - client_secret: System.get_env("GOOGLE_APP_SECRET") + config :ueberauth, Ueberauth.Strategy.Google.OAuth, + client_id: System.get_env("GOOGLE_APP_ID"), + client_secret: System.get_env("GOOGLE_APP_SECRET") + """ use OAuth2.Strategy @defaults [ - strategy: __MODULE__, - site: "https://accounts.google.com", - authorize_url: "/o/oauth2/v2/auth", - token_url: "https://www.googleapis.com/oauth2/v4/token" - ] + strategy: __MODULE__, + site: "https://accounts.google.com", + authorize_url: "/o/oauth2/v2/auth", + token_url: "https://www.googleapis.com/oauth2/v4/token" + ] @doc """ Construct a client for requests to Google. @@ -25,14 +26,16 @@ defmodule Ueberauth.Strategy.Google.OAuth do These options are only useful for usage outside the normal callback phase of Ueberauth. """ def client(opts \\ []) do - config = Application.get_env(:ueberauth, Ueberauth.Strategy.Google.OAuth) - - opts = - @defaults - |> Keyword.merge(config) - |> Keyword.merge(opts) + config = Application.get_env(:ueberauth, __MODULE__, []) + json_library = Ueberauth.json_library() - OAuth2.Client.new(opts) + @defaults + |> Keyword.merge(config) + |> Keyword.merge(opts) + |> resolve_values() + |> generate_secret() + |> OAuth2.Client.new() + |> OAuth2.Client.put_serializer("application/json", json_library) end @doc """ @@ -51,12 +54,22 @@ defmodule Ueberauth.Strategy.Google.OAuth do |> OAuth2.Client.get(url, headers, opts) end - def get_token!(params \\ [], opts \\ []) do - client = - opts - |> client() - |> OAuth2.Client.get_token!(params) - client.token + def get_access_token(params \\ [], opts \\ []) do + case opts |> client |> OAuth2.Client.get_token(params) do + {:error, %OAuth2.Response{body: %{"error" => error}} = response} -> + description = Map.get(response.body, "error_description", "") + {:error, {error, description}} + + {:error, %OAuth2.Error{reason: reason}} -> + {:error, {"error", to_string(reason)}} + + {:ok, %OAuth2.Client{token: %{access_token: nil} = token}} -> + %{"error" => error, "error_description" => description} = token.other_params + {:error, {error, description}} + + {:ok, %OAuth2.Client{token: token}} -> + {:ok, token} + end end # Strategy Callbacks @@ -71,4 +84,23 @@ defmodule Ueberauth.Strategy.Google.OAuth do |> put_header("Accept", "application/json") |> OAuth2.Strategy.AuthCode.get_token(params, headers) end + + defp resolve_values(list) do + for {key, value} <- list do + {key, resolve_value(value)} + end + end + + defp resolve_value({m, f, a}) when is_atom(m) and is_atom(f), do: apply(m, f, a) + defp resolve_value(v), do: v + + defp generate_secret(opts) do + if is_tuple(opts[:client_secret]) do + {module, fun} = opts[:client_secret] + secret = apply(module, fun, [opts]) + Keyword.put(opts, :client_secret, secret) + else + opts + end + end end diff --git a/mix.exs b/mix.exs index 2e9fb2f..671993e 100644 --- a/mix.exs +++ b/mix.exs @@ -1,47 +1,58 @@ defmodule UeberauthGoogle.Mixfile do use Mix.Project - @version "0.5.0" - @url "https://github.com/ueberauth/ueberauth_google" + @source_url "https://github.com/ueberauth/ueberauth_google" + @version "0.12.0" def project do - [app: :ueberauth_google, - version: @version, - name: "Ueberauth Google Strategy", - package: package(), - elixir: "~> 1.3", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - source_url: @url, - homepage_url: @url, - description: description(), - deps: deps(), - docs: docs()] + [ + app: :ueberauth_google, + version: @version, + name: "Üeberauth Google", + elixir: "~> 1.8", + start_permanent: Mix.env() == :prod, + package: package(), + deps: deps(), + docs: docs() + ] end def application do - [applications: [:logger, :oauth2, :ueberauth]] + [ + extra_applications: [:logger, :oauth2, :ueberauth] + ] end defp deps do [ - {:oauth2, "~> 0.8"}, - {:ueberauth, "~> 0.4"}, + {:oauth2, "~> 1.0 or ~> 2.0"}, + {:ueberauth, "~> 0.10.0"}, + {:credo, ">= 0.0.0", only: [:dev, :test], runtime: false}, + {:ex_doc, ">= 0.0.0", only: [:dev], runtime: false}, + {:mock, "~> 0.3", only: :test} ] end defp docs do - [extras: ["README.md", "CONTRIBUTING.md"]] - end - - defp description do - "An Uberauth strategy for Google authentication." + [ + extras: ["CHANGELOG.md", "CONTRIBUTING.md", "README.md"], + main: "readme", + source_url: @source_url, + homepage_url: @source_url, + formatters: ["html"] + ] end defp package do - [files: ["lib", "mix.exs", "README.md", "LICENSE"], - maintainers: ["Sean Callan"], - licenses: ["MIT"], - links: %{"GitHub": @url}] + [ + description: "An Uberauth strategy for Google authentication.", + files: ["lib", "mix.exs", "README.md", "CHANGELOG.md", "CONTRIBUTING.md", "LICENSE"], + maintainers: ["Sean Callan"], + licenses: ["MIT"], + links: %{ + Changelog: "https://hexdocs.pm/ueberauth_google/changelog.html", + GitHub: @source_url + } + ] end end diff --git a/mix.lock b/mix.lock index 0082650..0c8eaaa 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,33 @@ %{ - "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], [], "hexpm", "f7182e85b4ece9d1371c46699793dd3dee8f2c55be3f6967a6b84b8c02bab7d2"}, - "hackney": {:hex, :hackney, "1.6.3", "d489d7ca2d4323e307bedc4bfe684323a7bf773ecfd77938f3ee8074e488e140", [:mix, :rebar3], [{:certifi, "0.7.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "1.2.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "b2c483bc28ca6fd02b15a23e98156757b7de0dc1863427b058f46f1ad6c5cc4c"}, - "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], [], "hexpm", "1d724cdafb66397e61774ead242c9b725de7033cde8ea98fa4a91e64ac5ef5b3"}, + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, + "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "credo": {:hex, :credo, "1.6.4", "ddd474afb6e8c240313f3a7b0d025cc3213f0d171879429bf8535d7021d9ad78", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c28f910b61e1ff829bffa056ef7293a8db50e87f2c57a9b5c3f57eee124536b7"}, + "earmark": {:hex, :earmark, "1.3.5", "0db71c8290b5bc81cb0101a2a507a76dca659513984d683119ee722828b424f6", [:mix], [], "hexpm", "762b999fd414fb41e297944228aa1de2cd4a3876a07f968c8b11d1e9a2190d07"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"}, + "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, + "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"}, + "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, + "exvcr": {:hex, :exvcr, "0.11.1", "a5e5f57a67538e032e16cfea6cfb1232314fb146e3ceedf1cde4a11f12fb7a58", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "984a4d52d9e01d5f0e28d45718565a41dffab3ac18e029ae45d42f16a2a58a1d"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], [], "hexpm", "8aad5eef6d9d20899918868b10e79fc2dafe72a79102882c2947999c10b30cd9"}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm", "7a4c8e1115a2732a67d7624e28cf6c9f30c66711a9e92928e745c255887ba465"}, - "oauth2": {:hex, :oauth2, "0.8.0", "9650476a695a22c75fa9a0a8fed8094a135ba1972a7f421450e9b10cba3547dd", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d9db58949cb28920c9ff094b7b4e08a4ae63f14cfa9303112a31b01b5d8b7760"}, - "plug": {:hex, :plug, "1.2.0", "496bef96634a49d7803ab2671482f0c5ce9ce0b7b9bc25bc0ae8e09859dd2004", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "2788381ac3aa424a6dbf308aad08a1ebdefdcaad4fc8d21be56817d23a923ace"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm", "4f8805eb5c8a939cf2359367cb651a3180b27dfb48444846be2613d79355d65e"}, - "ueberauth": {:hex, :ueberauth, "0.4.0", "bc72d5e5a7bdcbfcf28a756e34630816edabc926303bdce7e171f7ac7ffa4f91", [:mix], [{:plug, "~> 1.2", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d3bcb678a8fdcd0add619eacb3e45e51003f50aa434ea732746ea25c37f6c92b"}, + "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "mock": {:hex, :mock, "0.3.7", "75b3bbf1466d7e486ea2052a73c6e062c6256fb429d6797999ab02fa32f29e03", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "4da49a4609e41fd99b7836945c26f373623ea968cfb6282742bcb94440cf7e5c"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"}, + "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, + "ueberauth": {:hex, :ueberauth, "0.10.1", "6706b410ee6bd9d67eac983ed9dc7fdc1f06b18677d7b8ba71d5725e07cc8826", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bb715b562395c4cc26b2d8e637c6bb0eb8c67d50c0ea543c0f78f06b7e8efdb1"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/test/strategy/google/oauth_test.exs b/test/strategy/google/oauth_test.exs new file mode 100644 index 0000000..1039d76 --- /dev/null +++ b/test/strategy/google/oauth_test.exs @@ -0,0 +1,20 @@ +defmodule Ueberauth.Strategy.Google.OAuthTest do + use ExUnit.Case, async: true + + alias Ueberauth.Strategy.Google.OAuth + + defmodule MyApp.Google do + def client_secret(_opts), do: "custom_client_secret" + end + + describe "client/1" do + test "uses client secret in the config when it is not a tuple" do + assert %OAuth2.Client{client_secret: "client_secret"} = OAuth.client() + end + + test "generates client secret when it is using a tuple config" do + options = [client_secret: {MyApp.Google, :client_secret}] + assert %OAuth2.Client{client_secret: "custom_client_secret"} = OAuth.client(options) + end + end +end diff --git a/test/strategy/google_test.exs b/test/strategy/google_test.exs new file mode 100644 index 0000000..234a880 --- /dev/null +++ b/test/strategy/google_test.exs @@ -0,0 +1,203 @@ +defmodule Ueberauth.Strategy.GoogleTest do + use ExUnit.Case, async: true + use Plug.Test + + import Mock + import Plug.Conn + import Ueberauth.Strategy.Helpers + + setup_with_mocks([ + {OAuth2.Client, [:passthrough], + [ + get_token: &oauth2_get_token/2, + get: &oauth2_get/4 + ]} + ]) do + # Create a connection with Ueberauth's CSRF cookies so they can be recycled during tests + routes = Ueberauth.init([]) + csrf_conn = conn(:get, "/auth/google", %{}) |> Ueberauth.call(routes) + csrf_state = with_state_param([], csrf_conn) |> Keyword.get(:state) + + {:ok, csrf_conn: csrf_conn, csrf_state: csrf_state} + end + + def set_options(routes, conn, opt) do + case Enum.find_index(routes, &(elem(&1, 0) == {conn.request_path, conn.method})) do + nil -> + routes + + idx -> + update_in(routes, [Access.at(idx), Access.elem(1), Access.elem(2)], &%{&1 | options: opt}) + end + end + + defp token(client, opts), do: {:ok, %{client | token: OAuth2.AccessToken.new(opts)}} + defp response(body, code \\ 200), do: {:ok, %OAuth2.Response{status_code: code, body: body}} + + def oauth2_get_token(client, code: "success_code"), do: token(client, "success_token") + def oauth2_get_token(client, code: "uid_code"), do: token(client, "uid_token") + def oauth2_get_token(client, code: "userinfo_code"), do: token(client, "userinfo_token") + def oauth2_get_token(_client, code: "oauth2_error"), do: {:error, %OAuth2.Error{reason: :timeout}} + + def oauth2_get_token(_client, code: "error_response"), + do: {:error, %OAuth2.Response{body: %{"error" => "some error", "error_description" => "something went wrong"}}} + + def oauth2_get_token(_client, code: "error_response_no_description"), + do: {:error, %OAuth2.Response{body: %{"error" => "internal_failure"}}} + + def oauth2_get(%{token: %{access_token: "success_token"}}, _url, _, _), + do: response(%{"sub" => "1234_fred", "name" => "Fred Jones", "email" => "fred_jones@example.com"}) + + def oauth2_get(%{token: %{access_token: "uid_token"}}, _url, _, _), + do: response(%{"uid_field" => "1234_daphne", "name" => "Daphne Blake"}) + + def oauth2_get(%{token: %{access_token: "userinfo_token"}}, "https://www.googleapis.com/oauth2/v3/userinfo", _, _), + do: response(%{"sub" => "1234_velma", "name" => "Velma Dinkley"}) + + def oauth2_get(%{token: %{access_token: "userinfo_token"}}, "example.com/shaggy", _, _), + do: response(%{"sub" => "1234_shaggy", "name" => "Norville Rogers"}) + + def oauth2_get(%{token: %{access_token: "userinfo_token"}}, "example.com/scooby", _, _), + do: response(%{"sub" => "1234_scooby", "name" => "Scooby Doo"}) + + defp set_csrf_cookies(conn, csrf_conn) do + conn + |> init_test_session(%{}) + |> recycle_cookies(csrf_conn) + |> fetch_cookies() + end + + test "handle_request! redirects to appropriate auth uri" do + conn = conn(:get, "/auth/google", %{hl: "es"}) + # Make sure the hd and scope params are included for good measure + routes = Ueberauth.init() |> set_options(conn, hd: "example.com", default_scope: "email openid") + + resp = Ueberauth.call(conn, routes) + + assert resp.status == 302 + assert [location] = get_resp_header(resp, "location") + + redirect_uri = URI.parse(location) + assert redirect_uri.host == "accounts.google.com" + assert redirect_uri.path == "/o/oauth2/v2/auth" + + assert %{ + "client_id" => "client_id", + "redirect_uri" => "http://www.example.com/auth/google/callback", + "response_type" => "code", + "scope" => "email openid", + "hd" => "example.com", + "hl" => "es" + } = Plug.Conn.Query.decode(redirect_uri.query) + end + + test "handle_callback! assigns required fields on successful auth", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do + conn = + conn(:get, "/auth/google/callback", %{code: "success_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + + routes = Ueberauth.init([]) + assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) + assert auth.credentials.token == "success_token" + assert auth.info.name == "Fred Jones" + assert auth.info.email == "fred_jones@example.com" + assert auth.uid == "1234_fred" + end + + test "uid_field is picked according to the specified option", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do + conn = conn(:get, "/auth/google/callback", %{code: "uid_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + routes = Ueberauth.init() |> set_options(conn, uid_field: "uid_field") + assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) + assert auth.info.name == "Daphne Blake" + assert auth.uid == "1234_daphne" + end + + test "userinfo is fetched according to userinfo_endpoint", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do + conn = + conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + + routes = Ueberauth.init() |> set_options(conn, userinfo_endpoint: "example.com/shaggy") + assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) + assert auth.info.name == "Norville Rogers" + end + + test "userinfo can be set via runtime config with default", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do + conn = + conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + + routes = Ueberauth.init() |> set_options(conn, userinfo_endpoint: {:system, "NOT_SET", "example.com/shaggy"}) + assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) + assert auth.info.name == "Norville Rogers" + end + + test "userinfo uses default library value if runtime env not found", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do + conn = + conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + + routes = Ueberauth.init() |> set_options(conn, userinfo_endpoint: {:system, "NOT_SET"}) + assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) + assert auth.info.name == "Velma Dinkley" + end + + test "userinfo can be set via runtime config", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do + conn = + conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + + routes = Ueberauth.init() |> set_options(conn, userinfo_endpoint: {:system, "UEBERAUTH_SCOOBY_DOO"}) + System.put_env("UEBERAUTH_SCOOBY_DOO", "example.com/scooby") + assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) + assert auth.info.name == "Scooby Doo" + System.delete_env("UEBERAUTH_SCOOBY_DOO") + end + + test "state param is present in the redirect uri" do + conn = conn(:get, "/auth/google", %{}) + + routes = Ueberauth.init() + resp = Ueberauth.call(conn, routes) + + assert [location] = get_resp_header(resp, "location") + + redirect_uri = URI.parse(location) + + assert redirect_uri.query =~ "state=" + end + + describe "error handling" do + test "handle_callback! handles Oauth2.Error", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do + conn = + conn(:get, "/auth/google/callback", %{code: "oauth2_error", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + + routes = Ueberauth.init([]) + assert %Plug.Conn{assigns: %{ueberauth_failure: failure}} = Ueberauth.call(conn, routes) + assert %Ueberauth.Failure{errors: [%Ueberauth.Failure.Error{message: "timeout", message_key: "error"}]} = failure + end + + test "handle_callback! handles error response", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do + conn = + conn(:get, "/auth/google/callback", %{code: "error_response", state: csrf_state}) |> set_csrf_cookies(csrf_conn) + + routes = Ueberauth.init([]) + assert %Plug.Conn{assigns: %{ueberauth_failure: failure}} = Ueberauth.call(conn, routes) + + assert %Ueberauth.Failure{ + errors: [%Ueberauth.Failure.Error{message: "something went wrong", message_key: "some error"}] + } = failure + end + + test "handle_callback! handles error response without error_description", %{ + csrf_state: csrf_state, + csrf_conn: csrf_conn + } do + conn = + conn(:get, "/auth/google/callback", %{code: "error_response_no_description", state: csrf_state}) + |> set_csrf_cookies(csrf_conn) + + routes = Ueberauth.init([]) + assert %Plug.Conn{assigns: %{ueberauth_failure: failure}} = Ueberauth.call(conn, routes) + + assert %Ueberauth.Failure{ + errors: [%Ueberauth.Failure.Error{message: "", message_key: "internal_failure"}] + } = failure + end + end +end diff --git a/test/ueber_google_test.exs b/test/ueber_google_test.exs deleted file mode 100644 index 377d513..0000000 --- a/test/ueber_google_test.exs +++ /dev/null @@ -1,8 +0,0 @@ -defmodule UeberauthGoogleTest do - use ExUnit.Case - doctest UeberauthGoogle - - test "the truth" do - assert 1 + 1 == 2 - end -end diff --git a/test/ueberauth_google_test.exs b/test/ueberauth_google_test.exs new file mode 100644 index 0000000..d43bb38 --- /dev/null +++ b/test/ueberauth_google_test.exs @@ -0,0 +1,4 @@ +defmodule UeberauthGoogleTest do + use ExUnit.Case, async: true + doctest UeberauthGoogle +end