├── test ├── test_helper.exs ├── ueberauth_google_test.exs └── strategy │ ├── google │ └── oauth_test.exs │ └── google_test.exs ├── lib ├── ueberauth_google.ex └── ueberauth │ └── strategy │ ├── google │ └── oauth.ex │ └── google.ex ├── .formatter.exs ├── .github ├── CODEOWNERS └── workflows │ ├── release.yml │ └── ci.yml ├── .travis.yml ├── config └── config.exs ├── CONTRIBUTING.md ├── .gitignore ├── LICENSE ├── mix.exs ├── CHANGELOG.md ├── README.md └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /lib/ueberauth_google.ex: -------------------------------------------------------------------------------- 1 | defmodule UeberauthGoogle do 2 | @moduledoc false 3 | end 4 | -------------------------------------------------------------------------------- /test/ueberauth_google_test.exs: -------------------------------------------------------------------------------- 1 | defmodule UeberauthGoogleTest do 2 | use ExUnit.Case, async: true 3 | doctest UeberauthGoogle 4 | end 5 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | line_length: 120 5 | ] 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Order is important. The last matching pattern takes the most precedence. 2 | # Default owners for everything in the repo. 3 | * @ueberauth/developers 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | 3 | cache: 4 | directories: 5 | - ~/.hex 6 | - ~/.mix 7 | - deps 8 | 9 | elixir: 10 | - '1.8' 11 | - '1.9' 12 | 13 | script: 14 | - mix test 15 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :ueberauth, Ueberauth, 4 | providers: [ 5 | google: {Ueberauth.Strategy.Google, []} 6 | ] 7 | 8 | config :ueberauth, Ueberauth.Strategy.Google.OAuth, 9 | client_id: "client_id", 10 | client_secret: "client_secret", 11 | token_url: "token_url" 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Pull Requests Welcome 4 | 5 | 1. Fork ueberauth_google 6 | 2. Create a topic branch 7 | 3. Make logically-grouped commits with clear commit messages 8 | 4. Push commits to your fork 9 | 5. Open a pull request against ueberauth_google/master 10 | 11 | ## Issues 12 | 13 | If you believe there to be a bug, please provide the maintainers with enough 14 | detail to reproduce or a link to an app exhibiting unexpected behavior. For 15 | help, please start with Stack Overflow. 16 | -------------------------------------------------------------------------------- /test/strategy/google/oauth_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ueberauth.Strategy.Google.OAuthTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Ueberauth.Strategy.Google.OAuth 5 | 6 | defmodule MyApp.Google do 7 | def client_secret(_opts), do: "custom_client_secret" 8 | end 9 | 10 | describe "client/1" do 11 | test "uses client secret in the config when it is not a tuple" do 12 | assert %OAuth2.Client{client_secret: "client_secret"} = OAuth.client() 13 | end 14 | 15 | test "generates client secret when it is using a tuple config" do 16 | options = [client_secret: {MyApp.Google, :client_secret}] 17 | assert %OAuth2.Client{client_secret: "custom_client_secret"} = OAuth.client(options) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Hexpm Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | name: Publish 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Elixir 14 | uses: erlef/setup-beam@v1 15 | with: 16 | elixir-version: '1.11' 17 | otp-version: '22.3' 18 | - name: Restore dependencies cache 19 | uses: actions/cache@v3 20 | with: 21 | path: deps 22 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 23 | restore-keys: ${{ runner.os }}-mix- 24 | - name: Install dependencies 25 | run: | 26 | mix local.rebar --force 27 | mix local.hex --force 28 | mix deps.get 29 | - name: Run Hex Publish 30 | run: mix hex.publish --yes 31 | env: 32 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/elixir,osx,vim 3 | 4 | ### Elixir ### 5 | /_build 6 | /cover 7 | /deps 8 | /tmp 9 | /.fetch 10 | erl_crash.dump 11 | ueberauth_google-*.tar 12 | *.ez 13 | *.beam 14 | 15 | ### Elixir Patch ### 16 | /doc 17 | ### OSX ### 18 | *.DS_Store 19 | .AppleDouble 20 | .LSOverride 21 | 22 | # Icon must end with two \r 23 | Icon 24 | 25 | # Thumbnails 26 | ._* 27 | 28 | # Files that might appear in the root of a volume 29 | .DocumentRevisions-V100 30 | .fseventsd 31 | .Spotlight-V100 32 | .TemporaryItems 33 | .Trashes 34 | .VolumeIcon.icns 35 | .com.apple.timemachine.donotpresent 36 | 37 | # Directories potentially created on remote AFP share 38 | .AppleDB 39 | .AppleDesktop 40 | Network Trash Folder 41 | Temporary Items 42 | .apdisk 43 | 44 | ### Vim ### 45 | # swap 46 | [._]*.s[a-v][a-z] 47 | [._]*.sw[a-p] 48 | [._]s[a-v][a-z] 49 | [._]sw[a-p] 50 | # session 51 | Session.vim 52 | # temporary 53 | .netrwhist 54 | *~ 55 | # auto-generated tag files 56 | tags 57 | 58 | # End of https://www.gitignore.io/api/elixir,osx,vim 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize] 6 | push: 7 | branches: 8 | - 'master' 9 | jobs: 10 | Test: 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - name: Checkout Code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Elixir 17 | uses: erlef/setup-beam@v1 18 | with: 19 | elixir-version: '1.11' 20 | otp-version: '22.3' 21 | 22 | - name: Install Dependencies 23 | run: | 24 | mix local.rebar --force 25 | mix local.hex --force 26 | mix deps.get 27 | - name: Run Tests 28 | run: mix test 29 | 30 | Linting: 31 | runs-on: ubuntu-20.04 32 | steps: 33 | - name: Checkout Code 34 | uses: actions/checkout@v3 35 | 36 | - name: Set up Elixir 37 | uses: erlef/setup-beam@v1 38 | with: 39 | elixir-version: '1.11' 40 | otp-version: '22.3' 41 | 42 | - name: Install Dependencies 43 | run: | 44 | mix local.rebar --force 45 | mix local.hex --force 46 | mix deps.get 47 | - name: Run Formatter 48 | run: mix format --check-formatted 49 | 50 | - name: Run Credo 51 | run: mix credo 52 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule UeberauthGoogle.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/ueberauth/ueberauth_google" 5 | @version "0.12.1" 6 | 7 | def project do 8 | [ 9 | app: :ueberauth_google, 10 | version: @version, 11 | name: "Üeberauth Google", 12 | elixir: "~> 1.8", 13 | start_permanent: Mix.env() == :prod, 14 | package: package(), 15 | deps: deps(), 16 | docs: docs() 17 | ] 18 | end 19 | 20 | def application do 21 | [ 22 | extra_applications: [:logger, :oauth2, :ueberauth] 23 | ] 24 | end 25 | 26 | defp deps do 27 | [ 28 | {:oauth2, "~> 1.0 or ~> 2.0"}, 29 | {:ueberauth, "~> 0.10.0"}, 30 | {:credo, ">= 0.0.0", only: [:dev, :test], runtime: false}, 31 | {:ex_doc, ">= 0.0.0", only: [:dev], runtime: false}, 32 | {:mock, "~> 0.3", only: :test} 33 | ] 34 | end 35 | 36 | defp docs do 37 | [ 38 | extras: ["CHANGELOG.md", "CONTRIBUTING.md", "README.md"], 39 | main: "readme", 40 | source_url: @source_url, 41 | homepage_url: @source_url, 42 | formatters: ["html"] 43 | ] 44 | end 45 | 46 | defp package do 47 | [ 48 | description: "An Uberauth strategy for Google authentication.", 49 | files: ["lib", "mix.exs", "README.md", "CHANGELOG.md", "CONTRIBUTING.md", "LICENSE"], 50 | maintainers: ["Sean Callan"], 51 | licenses: ["MIT"], 52 | links: %{ 53 | Changelog: "https://hexdocs.pm/ueberauth_google/changelog.html", 54 | GitHub: @source_url 55 | } 56 | ] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## (Unreleased) 4 | 5 | ## v0.12.1 6 | 7 | - Fix scope splitting [104](https://github.com/ueberauth/ueberauth_google/pull/104) 8 | 9 | ## v0.12.0 10 | 11 | - Add support to hl param in handle_request! [102](https://github.com/ueberauth/ueberauth_google/pull/102) 12 | 13 | ## v0.11.0 14 | 15 | - Allow using a function to generate the client secret [101](https://github.com/ueberauth/ueberauth_google/pull/101) 16 | 17 | ## v0.10.3 18 | 19 | - Handle `%OAuth2.Response{status_code: 503}` with no `error_description` in `get_access_token` [99](https://github.com/ueberauth/ueberauth_google/pull/99) 20 | 21 | ## v0.10.2 22 | 23 | - Prefer Local Over Global Configuration [95](https://github.com/ueberauth/ueberauth_google/pull/95) 24 | 25 | ## v0.10.1 26 | 27 | - Misc doc changes [81](https://github.com/ueberauth/ueberauth_google/pull/81) 28 | - Upgrade Ueberauth and Refactor CSRF State Logic [82](https://github.com/ueberauth/ueberauth_google/pull/82) 29 | 30 | ## v0.10.0 - 2020-10-20 31 | 32 | ### Enhancement 33 | 34 | - Updated docs [#69](https://github.com/ueberauth/ueberauth_google/pull/69) [#70](https://github.com/ueberauth/ueberauth_google/pull/70) 35 | - Support for birthday [#73](https://github.com/ueberauth/ueberauth_google/pull/73) 36 | - Allow for userinfo endpoint to be configured [#75](https://github.com/ueberauth/ueberauth_google/pull/75) 37 | - Updated plug and ueberauth packages [#76](https://github.com/ueberauth/ueberauth_google/pull/76) 38 | 39 | Thanks goes to all the contributes 40 | 41 | ## v0.9.0 - 2019-08-21 42 | 43 | ### Enhancement 44 | 45 | - Add support for optional login_hint param [#61](https://github.com/ueberauth/ueberauth_google/pull/61) 46 | - Use `json_library` method from Ueberauth config [#58](https://github.com/ueberauth/ueberauth_google/pull/58) 47 | - Allows specifying `{m, f, a}` tuples for things such as Client ID 48 | and Client Secret [#60](https://github.com/ueberauth/ueberauth_google/pull/60) 49 | - Allows the newest oauth2 package versions with potential security fixes [#68](https://github.com/ueberauth/ueberauth_google/pull/68) 50 | 51 | ## v0.6.0 - 2017-07-18 52 | 53 | - Add support for `access_type` per request using `url` parameter. 54 | 55 | ## v0.5.0 - 2016-12-27 56 | 57 | - Add support for new params: `access_type`, `approval_prompt`, `state`. 58 | - Fix Elixir warnings. 59 | 60 | ## v0.4.0 - 2016-09-21 61 | 62 | - Target Elixir 1.3 and greater. 63 | - Fix OAuth bug with 0.6.0 pin. 64 | 65 | ## v0.3.0 - 2016-08-15 66 | 67 | - Use OpenID endpoint for profile information. 68 | - Update authorize and token URLs. 69 | 70 | ## v0.2.0 - 2016-12-10 71 | 72 | - Release 0.2.0 to follow Ueberauth. 73 | -------------------------------------------------------------------------------- /lib/ueberauth/strategy/google/oauth.ex: -------------------------------------------------------------------------------- 1 | defmodule Ueberauth.Strategy.Google.OAuth do 2 | @moduledoc """ 3 | OAuth2 for Google. 4 | 5 | Add `client_id` and `client_secret` to your configuration: 6 | 7 | config :ueberauth, Ueberauth.Strategy.Google.OAuth, 8 | client_id: System.get_env("GOOGLE_APP_ID"), 9 | client_secret: System.get_env("GOOGLE_APP_SECRET") 10 | 11 | """ 12 | use OAuth2.Strategy 13 | 14 | @defaults [ 15 | strategy: __MODULE__, 16 | site: "https://accounts.google.com", 17 | authorize_url: "/o/oauth2/v2/auth", 18 | token_url: "https://www.googleapis.com/oauth2/v4/token" 19 | ] 20 | 21 | @doc """ 22 | Construct a client for requests to Google. 23 | 24 | This will be setup automatically for you in `Ueberauth.Strategy.Google`. 25 | 26 | These options are only useful for usage outside the normal callback phase of Ueberauth. 27 | """ 28 | def client(opts \\ []) do 29 | config = Application.get_env(:ueberauth, __MODULE__, []) 30 | json_library = Ueberauth.json_library() 31 | 32 | @defaults 33 | |> Keyword.merge(config) 34 | |> Keyword.merge(opts) 35 | |> resolve_values() 36 | |> generate_secret() 37 | |> OAuth2.Client.new() 38 | |> OAuth2.Client.put_serializer("application/json", json_library) 39 | end 40 | 41 | @doc """ 42 | Provides the authorize url for the request phase of Ueberauth. No need to call this usually. 43 | """ 44 | def authorize_url!(params \\ [], opts \\ []) do 45 | opts 46 | |> client 47 | |> OAuth2.Client.authorize_url!(params) 48 | end 49 | 50 | def get(token, url, headers \\ [], opts \\ []) do 51 | [token: token] 52 | |> client 53 | |> put_param("client_secret", client().client_secret) 54 | |> OAuth2.Client.get(url, headers, opts) 55 | end 56 | 57 | def get_access_token(params \\ [], opts \\ []) do 58 | case opts |> client |> OAuth2.Client.get_token(params) do 59 | {:error, %OAuth2.Response{body: %{"error" => error}} = response} -> 60 | description = Map.get(response.body, "error_description", "") 61 | {:error, {error, description}} 62 | 63 | {:error, %OAuth2.Error{reason: reason}} -> 64 | {:error, {"error", to_string(reason)}} 65 | 66 | {:ok, %OAuth2.Client{token: %{access_token: nil} = token}} -> 67 | %{"error" => error, "error_description" => description} = token.other_params 68 | {:error, {error, description}} 69 | 70 | {:ok, %OAuth2.Client{token: token}} -> 71 | {:ok, token} 72 | end 73 | end 74 | 75 | # Strategy Callbacks 76 | 77 | def authorize_url(client, params) do 78 | OAuth2.Strategy.AuthCode.authorize_url(client, params) 79 | end 80 | 81 | def get_token(client, params, headers) do 82 | client 83 | |> put_param("client_secret", client.client_secret) 84 | |> put_header("Accept", "application/json") 85 | |> OAuth2.Strategy.AuthCode.get_token(params, headers) 86 | end 87 | 88 | defp resolve_values(list) do 89 | for {key, value} <- list do 90 | {key, resolve_value(value)} 91 | end 92 | end 93 | 94 | defp resolve_value({m, f, a}) when is_atom(m) and is_atom(f), do: apply(m, f, a) 95 | defp resolve_value(v), do: v 96 | 97 | defp generate_secret(opts) do 98 | if is_tuple(opts[:client_secret]) do 99 | {module, fun} = opts[:client_secret] 100 | secret = apply(module, fun, [opts]) 101 | Keyword.put(opts, :client_secret, secret) 102 | else 103 | opts 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Überauth Google 2 | 3 | [![Continuous Integration](https://github.com/ueberauth/ueberauth_google/actions/workflows/ci.yml/badge.svg)](https://github.com/ueberauth/ueberauth_google/actions/workflows/ci.yml) 4 | [![Build Status](https://travis-ci.org/ueberauth/ueberauth_google.svg?branch=master)](https://travis-ci.org/ueberauth/ueberauth_google) 5 | [![Module Version](https://img.shields.io/hexpm/v/ueberauth_google.svg)](https://hex.pm/packages/ueberauth_google) 6 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ueberauth_google/) 7 | [![Total Download](https://img.shields.io/hexpm/dt/ueberauth_google.svg)](https://hex.pm/packages/ueberauth_google) 8 | [![License](https://img.shields.io/hexpm/l/ueberauth_google.svg)](https://github.com/ueberauth/ueberauth_google/blob/master/LICENSE) 9 | [![Last Updated](https://img.shields.io/github/last-commit/ueberauth/ueberauth_google.svg)](https://github.com/ueberauth/ueberauth_google/commits/master) 10 | 11 | 12 | > Google OAuth2 strategy for Überauth. 13 | 14 | ## Installation 15 | 16 | 1. Setup your application at [Google Developer Console](https://console.developers.google.com/home). 17 | 18 | 2. Add `:ueberauth_google` to your list of dependencies in `mix.exs`: 19 | 20 | ```elixir 21 | def deps do 22 | [ 23 | {:ueberauth_google, "~> 0.10"} 24 | ] 25 | end 26 | ``` 27 | 28 | 3. Add Google to your Überauth configuration: 29 | 30 | ```elixir 31 | config :ueberauth, Ueberauth, 32 | providers: [ 33 | google: {Ueberauth.Strategy.Google, []} 34 | ] 35 | ``` 36 | 37 | 4. Update your provider configuration: 38 | 39 | Use that if you want to read client ID/secret from the environment 40 | variables in the compile time: 41 | 42 | ```elixir 43 | config :ueberauth, Ueberauth.Strategy.Google.OAuth, 44 | client_id: System.get_env("GOOGLE_CLIENT_ID"), 45 | client_secret: System.get_env("GOOGLE_CLIENT_SECRET") 46 | ``` 47 | 48 | Use that if you want to read client ID/secret from the environment 49 | variables in the run time: 50 | 51 | ```elixir 52 | config :ueberauth, Ueberauth.Strategy.Google.OAuth, 53 | client_id: {System, :get_env, ["GOOGLE_CLIENT_ID"]}, 54 | client_secret: {System, :get_env, ["GOOGLE_CLIENT_SECRET"]} 55 | ``` 56 | 57 | 5. Include the Überauth plug in your controller: 58 | 59 | ```elixir 60 | defmodule MyApp.AuthController do 61 | use MyApp.Web, :controller 62 | plug Ueberauth 63 | ... 64 | end 65 | ``` 66 | 67 | 6. Create the request and callback routes if you haven't already: 68 | 69 | ```elixir 70 | scope "/auth", MyApp do 71 | pipe_through :browser 72 | 73 | get "/:provider", AuthController, :request 74 | get "/:provider/callback", AuthController, :callback 75 | end 76 | ``` 77 | 78 | 7. Your controller needs to implement callbacks to deal with `Ueberauth.Auth` and `Ueberauth.Failure` responses. 79 | 80 | For an example implementation see the [Überauth Example](https://github.com/ueberauth/ueberauth_example) application. 81 | 82 | ## Calling 83 | 84 | Depending on the configured url you can initiate the request through: 85 | 86 | /auth/google 87 | 88 | Or with options: 89 | 90 | /auth/google?scope=email%20profile 91 | 92 | By default the requested scope is "email". Scope can be configured either explicitly as a `scope` query value on the request path or in your configuration: 93 | 94 | ```elixir 95 | config :ueberauth, Ueberauth, 96 | providers: [ 97 | google: {Ueberauth.Strategy.Google, [default_scope: "email profile plus.me"]} 98 | ] 99 | ``` 100 | 101 | 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). 102 | 103 | ```elixir 104 | config :ueberauth, Ueberauth, 105 | providers: [ 106 | google: {Ueberauth.Strategy.Google, [hd: "example.com", prompt: "select_account", access_type: "offline", include_granted_scopes: true]} 107 | ] 108 | ``` 109 | 110 | 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. 111 | 112 | ```elixir 113 | config :ueberauth, Ueberauth, 114 | providers: [ 115 | google: {Ueberauth.Strategy.Google, [userinfo_endpoint: "https://www.googleapis.cn/oauth2/v3/userinfo"]} 116 | ] 117 | ``` 118 | 119 | 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. 120 | A third argument may be passed representing a default value if the environment variable is not found, otherwise the library default will be used. 121 | 122 | ```elixir 123 | config :ueberauth, Ueberauth, 124 | providers: [ 125 | google: {Ueberauth.Strategy.Google, [ 126 | userinfo_endpoint: {:system, "GOOGLE_USERINFO_ENDPOINT", "https://www.googleapis.cn/oauth2/v3/userinfo"} 127 | ]} 128 | ] 129 | ``` 130 | 131 | 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. 132 | 133 | ## Copyright and License 134 | 135 | Copyright (c) 2015 Sean Callan 136 | 137 | Released under the MIT License, which can be found in the repository in [LICENSE](https://github.com/ueberauth/ueberauth_google/blob/master/LICENSE). 138 | -------------------------------------------------------------------------------- /lib/ueberauth/strategy/google.ex: -------------------------------------------------------------------------------- 1 | defmodule Ueberauth.Strategy.Google do 2 | @moduledoc """ 3 | Google Strategy for Überauth. 4 | """ 5 | 6 | use Ueberauth.Strategy, 7 | uid_field: :sub, 8 | default_scope: "email", 9 | hd: nil, 10 | userinfo_endpoint: "https://www.googleapis.com/oauth2/v3/userinfo" 11 | 12 | alias Ueberauth.Auth.Info 13 | alias Ueberauth.Auth.Credentials 14 | alias Ueberauth.Auth.Extra 15 | 16 | @doc """ 17 | Handles initial request for Google authentication. 18 | """ 19 | def handle_request!(conn) do 20 | scopes = conn.params["scope"] || option(conn, :default_scope) 21 | 22 | params = 23 | [scope: scopes] 24 | |> with_optional(:hd, conn) 25 | |> with_optional(:prompt, conn) 26 | |> with_optional(:access_type, conn) 27 | |> with_optional(:login_hint, conn) 28 | |> with_optional(:include_granted_scopes, conn) 29 | |> with_param(:access_type, conn) 30 | |> with_param(:prompt, conn) 31 | |> with_param(:login_hint, conn) 32 | |> with_param(:hl, conn) 33 | |> with_state_param(conn) 34 | 35 | opts = oauth_client_options_from_conn(conn) 36 | redirect!(conn, Ueberauth.Strategy.Google.OAuth.authorize_url!(params, opts)) 37 | end 38 | 39 | @doc """ 40 | Handles the callback from Google. 41 | """ 42 | def handle_callback!(%Plug.Conn{params: %{"code" => code}} = conn) do 43 | params = [code: code] 44 | opts = oauth_client_options_from_conn(conn) 45 | 46 | case Ueberauth.Strategy.Google.OAuth.get_access_token(params, opts) do 47 | {:ok, token} -> 48 | fetch_user(conn, token) 49 | 50 | {:error, {error_code, error_description}} -> 51 | set_errors!(conn, [error(error_code, error_description)]) 52 | end 53 | end 54 | 55 | @doc false 56 | def handle_callback!(conn) do 57 | set_errors!(conn, [error("missing_code", "No code received")]) 58 | end 59 | 60 | @doc false 61 | def handle_cleanup!(conn) do 62 | conn 63 | |> put_private(:google_user, nil) 64 | |> put_private(:google_token, nil) 65 | end 66 | 67 | @doc """ 68 | Fetches the uid field from the response. 69 | """ 70 | def uid(conn) do 71 | uid_field = 72 | conn 73 | |> option(:uid_field) 74 | |> to_string 75 | 76 | conn.private.google_user[uid_field] 77 | end 78 | 79 | @doc """ 80 | Includes the credentials from the google response. 81 | """ 82 | def credentials(conn) do 83 | token = conn.private.google_token 84 | scope_string = token.other_params["scope"] || "" 85 | scopes = String.split(scope_string, " ") 86 | 87 | %Credentials{ 88 | expires: !!token.expires_at, 89 | expires_at: token.expires_at, 90 | scopes: scopes, 91 | token_type: Map.get(token, :token_type), 92 | refresh_token: token.refresh_token, 93 | token: token.access_token 94 | } 95 | end 96 | 97 | @doc """ 98 | Fetches the fields to populate the info section of the `Ueberauth.Auth` struct. 99 | """ 100 | def info(conn) do 101 | user = conn.private.google_user 102 | 103 | %Info{ 104 | email: user["email"], 105 | first_name: user["given_name"], 106 | image: user["picture"], 107 | last_name: user["family_name"], 108 | name: user["name"], 109 | birthday: user["birthday"], 110 | urls: %{ 111 | profile: user["profile"], 112 | website: user["hd"] 113 | } 114 | } 115 | end 116 | 117 | @doc """ 118 | Stores the raw information (including the token) obtained from the google callback. 119 | """ 120 | def extra(conn) do 121 | %Extra{ 122 | raw_info: %{ 123 | token: conn.private.google_token, 124 | user: conn.private.google_user 125 | } 126 | } 127 | end 128 | 129 | defp fetch_user(conn, token) do 130 | conn = put_private(conn, :google_token, token) 131 | 132 | # userinfo_endpoint from https://accounts.google.com/.well-known/openid-configuration 133 | # the userinfo_endpoint may be overridden in options when necessary. 134 | resp = Ueberauth.Strategy.Google.OAuth.get(token, get_userinfo_endpoint(conn)) 135 | 136 | case resp do 137 | {:ok, %OAuth2.Response{status_code: 401, body: body}} -> 138 | set_errors!(conn, [error("token", "unauthorized" <> body)]) 139 | 140 | {:ok, %OAuth2.Response{status_code: status_code, body: user}} 141 | when status_code in 200..399 -> 142 | put_private(conn, :google_user, user) 143 | 144 | {:error, %OAuth2.Response{status_code: status_code}} -> 145 | set_errors!(conn, [error("OAuth2", status_code)]) 146 | 147 | {:error, %OAuth2.Error{reason: reason}} -> 148 | set_errors!(conn, [error("OAuth2", reason)]) 149 | end 150 | end 151 | 152 | defp get_userinfo_endpoint(conn) do 153 | case option(conn, :userinfo_endpoint) do 154 | {:system, varname, default} -> 155 | System.get_env(varname) || default 156 | 157 | {:system, varname} -> 158 | System.get_env(varname) || Keyword.get(default_options(), :userinfo_endpoint) 159 | 160 | other -> 161 | other 162 | end 163 | end 164 | 165 | defp with_param(opts, key, conn) do 166 | if value = conn.params[to_string(key)], do: Keyword.put(opts, key, value), else: opts 167 | end 168 | 169 | defp with_optional(opts, key, conn) do 170 | if option(conn, key), do: Keyword.put(opts, key, option(conn, key)), else: opts 171 | end 172 | 173 | defp oauth_client_options_from_conn(conn) do 174 | base_options = [redirect_uri: callback_url(conn)] 175 | request_options = conn.private[:ueberauth_request_options].options 176 | 177 | case {request_options[:client_id], request_options[:client_secret]} do 178 | {nil, _} -> base_options 179 | {_, nil} -> base_options 180 | {id, secret} -> [client_id: id, client_secret: secret] ++ base_options 181 | end 182 | end 183 | 184 | defp option(conn, key) do 185 | Keyword.get(options(conn), key, Keyword.get(default_options(), key)) 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, 4 | "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"}, 5 | "earmark": {:hex, :earmark, "1.3.5", "0db71c8290b5bc81cb0101a2a507a76dca659513984d683119ee722828b424f6", [:mix], [], "hexpm", "762b999fd414fb41e297944228aa1de2cd4a3876a07f968c8b11d1e9a2190d07"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"}, 7 | "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"}, 8 | "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"}, 9 | "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, 10 | "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"}, 11 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 12 | "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"}, 13 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 14 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, 15 | "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, 16 | "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"}, 17 | "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"}, 18 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 19 | "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, 20 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 21 | "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, 22 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 23 | "mock": {:hex, :mock, "0.3.7", "75b3bbf1466d7e486ea2052a73c6e062c6256fb429d6797999ab02fa32f29e03", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "4da49a4609e41fd99b7836945c26f373623ea968cfb6282742bcb94440cf7e5c"}, 24 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 25 | "oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"}, 26 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 27 | "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"}, 28 | "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, 29 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 30 | "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, 31 | "ueberauth": {:hex, :ueberauth, "0.10.1", "6706b410ee6bd9d67eac983ed9dc7fdc1f06b18677d7b8ba71d5725e07cc8826", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bb715b562395c4cc26b2d8e637c6bb0eb8c67d50c0ea543c0f78f06b7e8efdb1"}, 32 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 33 | } 34 | -------------------------------------------------------------------------------- /test/strategy/google_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ueberauth.Strategy.GoogleTest do 2 | use ExUnit.Case, async: true 3 | use Plug.Test 4 | 5 | import Mock 6 | import Plug.Conn 7 | import Ueberauth.Strategy.Helpers 8 | 9 | setup_with_mocks([ 10 | {OAuth2.Client, [:passthrough], 11 | [ 12 | get_token: &oauth2_get_token/2, 13 | get: &oauth2_get/4 14 | ]} 15 | ]) do 16 | # Create a connection with Ueberauth's CSRF cookies so they can be recycled during tests 17 | routes = Ueberauth.init([]) 18 | csrf_conn = conn(:get, "/auth/google", %{}) |> Ueberauth.call(routes) 19 | csrf_state = with_state_param([], csrf_conn) |> Keyword.get(:state) 20 | 21 | {:ok, csrf_conn: csrf_conn, csrf_state: csrf_state} 22 | end 23 | 24 | def set_options(routes, conn, opt) do 25 | case Enum.find_index(routes, &(elem(&1, 0) == {conn.request_path, conn.method})) do 26 | nil -> 27 | routes 28 | 29 | idx -> 30 | update_in(routes, [Access.at(idx), Access.elem(1), Access.elem(2)], &%{&1 | options: opt}) 31 | end 32 | end 33 | 34 | defp token(client, opts), do: {:ok, %{client | token: OAuth2.AccessToken.new(opts)}} 35 | defp response(body, code \\ 200), do: {:ok, %OAuth2.Response{status_code: code, body: body}} 36 | 37 | def oauth2_get_token(client, code: "success_code"), do: token(client, "success_token") 38 | def oauth2_get_token(client, code: "uid_code"), do: token(client, "uid_token") 39 | def oauth2_get_token(client, code: "userinfo_code"), do: token(client, "userinfo_token") 40 | def oauth2_get_token(_client, code: "oauth2_error"), do: {:error, %OAuth2.Error{reason: :timeout}} 41 | 42 | def oauth2_get_token(_client, code: "error_response"), 43 | do: {:error, %OAuth2.Response{body: %{"error" => "some error", "error_description" => "something went wrong"}}} 44 | 45 | def oauth2_get_token(_client, code: "error_response_no_description"), 46 | do: {:error, %OAuth2.Response{body: %{"error" => "internal_failure"}}} 47 | 48 | def oauth2_get(%{token: %{access_token: "success_token"}}, _url, _, _), 49 | do: response(%{"sub" => "1234_fred", "name" => "Fred Jones", "email" => "fred_jones@example.com"}) 50 | 51 | def oauth2_get(%{token: %{access_token: "uid_token"}}, _url, _, _), 52 | do: response(%{"uid_field" => "1234_daphne", "name" => "Daphne Blake"}) 53 | 54 | def oauth2_get(%{token: %{access_token: "userinfo_token"}}, "https://www.googleapis.com/oauth2/v3/userinfo", _, _), 55 | do: response(%{"sub" => "1234_velma", "name" => "Velma Dinkley"}) 56 | 57 | def oauth2_get(%{token: %{access_token: "userinfo_token"}}, "example.com/shaggy", _, _), 58 | do: response(%{"sub" => "1234_shaggy", "name" => "Norville Rogers"}) 59 | 60 | def oauth2_get(%{token: %{access_token: "userinfo_token"}}, "example.com/scooby", _, _), 61 | do: response(%{"sub" => "1234_scooby", "name" => "Scooby Doo"}) 62 | 63 | defp set_csrf_cookies(conn, csrf_conn) do 64 | conn 65 | |> init_test_session(%{}) 66 | |> recycle_cookies(csrf_conn) 67 | |> fetch_cookies() 68 | end 69 | 70 | test "handle_request! redirects to appropriate auth uri" do 71 | conn = conn(:get, "/auth/google", %{hl: "es"}) 72 | # Make sure the hd and scope params are included for good measure 73 | routes = Ueberauth.init() |> set_options(conn, hd: "example.com", default_scope: "email openid") 74 | 75 | resp = Ueberauth.call(conn, routes) 76 | 77 | assert resp.status == 302 78 | assert [location] = get_resp_header(resp, "location") 79 | 80 | redirect_uri = URI.parse(location) 81 | assert redirect_uri.host == "accounts.google.com" 82 | assert redirect_uri.path == "/o/oauth2/v2/auth" 83 | 84 | assert %{ 85 | "client_id" => "client_id", 86 | "redirect_uri" => "http://www.example.com/auth/google/callback", 87 | "response_type" => "code", 88 | "scope" => "email openid", 89 | "hd" => "example.com", 90 | "hl" => "es" 91 | } = Plug.Conn.Query.decode(redirect_uri.query) 92 | end 93 | 94 | test "handle_callback! assigns required fields on successful auth", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do 95 | conn = 96 | conn(:get, "/auth/google/callback", %{code: "success_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) 97 | 98 | routes = Ueberauth.init([]) 99 | assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) 100 | assert auth.credentials.token == "success_token" 101 | assert auth.info.name == "Fred Jones" 102 | assert auth.info.email == "fred_jones@example.com" 103 | assert auth.uid == "1234_fred" 104 | end 105 | 106 | test "uid_field is picked according to the specified option", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do 107 | conn = conn(:get, "/auth/google/callback", %{code: "uid_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) 108 | routes = Ueberauth.init() |> set_options(conn, uid_field: "uid_field") 109 | assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) 110 | assert auth.info.name == "Daphne Blake" 111 | assert auth.uid == "1234_daphne" 112 | end 113 | 114 | test "userinfo is fetched according to userinfo_endpoint", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do 115 | conn = 116 | conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) 117 | 118 | routes = Ueberauth.init() |> set_options(conn, userinfo_endpoint: "example.com/shaggy") 119 | assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) 120 | assert auth.info.name == "Norville Rogers" 121 | end 122 | 123 | test "userinfo can be set via runtime config with default", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do 124 | conn = 125 | conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) 126 | 127 | routes = Ueberauth.init() |> set_options(conn, userinfo_endpoint: {:system, "NOT_SET", "example.com/shaggy"}) 128 | assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) 129 | assert auth.info.name == "Norville Rogers" 130 | end 131 | 132 | test "userinfo uses default library value if runtime env not found", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do 133 | conn = 134 | conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) 135 | 136 | routes = Ueberauth.init() |> set_options(conn, userinfo_endpoint: {:system, "NOT_SET"}) 137 | assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) 138 | assert auth.info.name == "Velma Dinkley" 139 | end 140 | 141 | test "userinfo can be set via runtime config", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do 142 | conn = 143 | conn(:get, "/auth/google/callback", %{code: "userinfo_code", state: csrf_state}) |> set_csrf_cookies(csrf_conn) 144 | 145 | routes = Ueberauth.init() |> set_options(conn, userinfo_endpoint: {:system, "UEBERAUTH_SCOOBY_DOO"}) 146 | System.put_env("UEBERAUTH_SCOOBY_DOO", "example.com/scooby") 147 | assert %Plug.Conn{assigns: %{ueberauth_auth: auth}} = Ueberauth.call(conn, routes) 148 | assert auth.info.name == "Scooby Doo" 149 | System.delete_env("UEBERAUTH_SCOOBY_DOO") 150 | end 151 | 152 | test "state param is present in the redirect uri" do 153 | conn = conn(:get, "/auth/google", %{}) 154 | 155 | routes = Ueberauth.init() 156 | resp = Ueberauth.call(conn, routes) 157 | 158 | assert [location] = get_resp_header(resp, "location") 159 | 160 | redirect_uri = URI.parse(location) 161 | 162 | assert redirect_uri.query =~ "state=" 163 | end 164 | 165 | describe "error handling" do 166 | test "handle_callback! handles Oauth2.Error", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do 167 | conn = 168 | conn(:get, "/auth/google/callback", %{code: "oauth2_error", state: csrf_state}) |> set_csrf_cookies(csrf_conn) 169 | 170 | routes = Ueberauth.init([]) 171 | assert %Plug.Conn{assigns: %{ueberauth_failure: failure}} = Ueberauth.call(conn, routes) 172 | assert %Ueberauth.Failure{errors: [%Ueberauth.Failure.Error{message: "timeout", message_key: "error"}]} = failure 173 | end 174 | 175 | test "handle_callback! handles error response", %{csrf_state: csrf_state, csrf_conn: csrf_conn} do 176 | conn = 177 | conn(:get, "/auth/google/callback", %{code: "error_response", state: csrf_state}) |> set_csrf_cookies(csrf_conn) 178 | 179 | routes = Ueberauth.init([]) 180 | assert %Plug.Conn{assigns: %{ueberauth_failure: failure}} = Ueberauth.call(conn, routes) 181 | 182 | assert %Ueberauth.Failure{ 183 | errors: [%Ueberauth.Failure.Error{message: "something went wrong", message_key: "some error"}] 184 | } = failure 185 | end 186 | 187 | test "handle_callback! handles error response without error_description", %{ 188 | csrf_state: csrf_state, 189 | csrf_conn: csrf_conn 190 | } do 191 | conn = 192 | conn(:get, "/auth/google/callback", %{code: "error_response_no_description", state: csrf_state}) 193 | |> set_csrf_cookies(csrf_conn) 194 | 195 | routes = Ueberauth.init([]) 196 | assert %Plug.Conn{assigns: %{ueberauth_failure: failure}} = Ueberauth.call(conn, routes) 197 | 198 | assert %Ueberauth.Failure{ 199 | errors: [%Ueberauth.Failure.Error{message: "", message_key: "internal_failure"}] 200 | } = failure 201 | end 202 | end 203 | end 204 | --------------------------------------------------------------------------------